inksim 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {inksim-0.2.3 → inksim-0.2.4}/PKG-INFO +1 -1
  2. {inksim-0.2.3 → inksim-0.2.4}/pyproject.toml +1 -1
  3. {inksim-0.2.3 → inksim-0.2.4}/pyproject.toml.orig +1 -1
  4. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/cli.py +1 -1
  5. inksim-0.2.4/src/inksim/formats.py +97 -0
  6. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/frame.py +216 -3
  7. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/shortcuts.py +12 -1
  8. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/viewer.py +288 -30
  9. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/density.py +6 -5
  10. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/grid.py +47 -8
  11. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/viewport.py +2 -2
  12. inksim-0.2.3/src/inksim/formats.py +0 -27
  13. {inksim-0.2.3 → inksim-0.2.4}/LICENSE +0 -0
  14. {inksim-0.2.3 → inksim-0.2.4}/README.md +0 -0
  15. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/__init__.py +0 -0
  16. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/__main__.py +0 -0
  17. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/assets/InkSim_colorful_small.png +0 -0
  18. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/assets/app_icons/inksim.svg +0 -0
  19. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/constants.py +0 -0
  20. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/debug.py +0 -0
  21. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/__init__.py +0 -0
  22. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/about.py +0 -0
  23. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/dialogs.py +0 -0
  24. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/help.py +0 -0
  25. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/renderer_picker.py +0 -0
  26. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/settings.py +0 -0
  27. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/splash.py +0 -0
  28. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/status.py +0 -0
  29. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/timeline.py +0 -0
  30. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/interconnect.py +0 -0
  31. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/__init__.py +0 -0
  32. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/export.py +0 -0
  33. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/fabric.py +0 -0
  34. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/previews.py +0 -0
  35. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/registry.py +0 -0
  36. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches.py +0 -0
  37. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches_numba.py +0 -0
  38. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches_qt.py +0 -0
  39. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/vintage_qt.py +0 -0
  40. {inksim-0.2.3 → inksim-0.2.4}/src/inksim/runtime.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: inksim
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Interactive embroidery simulator
5
5
  Author: Tony Karnigen
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "inksim"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Interactive embroidery simulator"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "inksim"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Interactive embroidery simulator"
5
5
  authors = [{ name = "Tony Karnigen" }]
6
6
  readme = "README.md"
@@ -136,7 +136,7 @@ def main():
136
136
  "--grid",
137
137
  dest="export_grid",
138
138
  action="store_true",
139
- help="Add a 10 mm grid to exported PNG",
139
+ help="Add a measurement grid to exported PNG",
140
140
  )
141
141
  parser.add_argument(
142
142
  "-y", "--yes",
@@ -0,0 +1,97 @@
1
+ """Embroidery input-format helpers."""
2
+
3
+ import pystitch as emb
4
+
5
+
6
+ def get_supported_input_wildcard():
7
+ """Build a file filter from the formats readable by pystitch."""
8
+ extensions = get_supported_input_extensions()
9
+ patterns = ";".join(f"*.{ext}" for ext in sorted(extensions))
10
+ return f"Embroidery files ({patterns})|{patterns}|All files|*.*"
11
+
12
+
13
+ def get_supported_input_extensions():
14
+ """Return lowercase filename extensions readable by pystitch."""
15
+ extensions = set()
16
+ try:
17
+ supported_formats = emb.supported_formats()
18
+ except Exception:
19
+ return extensions
20
+ for file_type in supported_formats:
21
+ if file_type.get("reader") is None:
22
+ continue
23
+ file_extensions = file_type.get("extensions", ())
24
+ if isinstance(file_extensions, str):
25
+ file_extensions = (file_extensions,)
26
+ extensions.update(ext.lstrip(".").lower() for ext in file_extensions)
27
+ return extensions
28
+
29
+
30
+ def get_supported_output_formats():
31
+ """Return writable pystitch formats with their display metadata."""
32
+ formats = []
33
+ try:
34
+ supported_formats = emb.supported_formats()
35
+ except Exception:
36
+ return formats
37
+ seen_extensions = set()
38
+ for file_type in supported_formats:
39
+ if file_type.get("writer") is None:
40
+ continue
41
+ extension = file_type.get("extension", "")
42
+ if not extension:
43
+ continue
44
+ extensions = file_type.get("extensions", (extension,))
45
+ if isinstance(extensions, str):
46
+ extensions = (extensions,)
47
+ clean_extensions = tuple(
48
+ ext.lstrip(".").lower() for ext in extensions if ext
49
+ )
50
+ if not clean_extensions:
51
+ clean_extensions = (extension.lstrip(".").lower(),)
52
+ primary_extension = extension.lstrip(".").lower()
53
+ if primary_extension in seen_extensions:
54
+ continue
55
+ seen_extensions.add(primary_extension)
56
+ formats.append(
57
+ {
58
+ "description": file_type.get("description", primary_extension.upper()),
59
+ "extension": primary_extension,
60
+ "extensions": clean_extensions,
61
+ }
62
+ )
63
+ return sorted(formats, key=lambda item: (item["description"], item["extension"]))
64
+
65
+
66
+ def get_supported_output_filter():
67
+ """Build a Qt file filter for all formats writable by pystitch."""
68
+ filters = []
69
+ for file_type in get_supported_output_formats():
70
+ patterns = " ".join(f"*.{ext}" for ext in file_type["extensions"])
71
+ filters.append(
72
+ f"{file_type['description']} - .{file_type['extension']} ({patterns})"
73
+ )
74
+ filters.append("All files (*)")
75
+ return ";;".join(filters)
76
+
77
+
78
+ def extension_from_output_filter(selected_filter):
79
+ """Return the primary extension encoded in a Qt output-format filter."""
80
+ suffix_marker = " - ."
81
+ suffix_start = selected_filter.find(suffix_marker)
82
+ if suffix_start >= 0:
83
+ suffix_start += len(suffix_marker)
84
+ suffix_end = selected_filter.find(" ", suffix_start)
85
+ if suffix_end < 0:
86
+ suffix_end = selected_filter.find("(", suffix_start)
87
+ if suffix_end > suffix_start:
88
+ return selected_filter[suffix_start:suffix_end].strip().lower()
89
+ marker_start = selected_filter.find("(*.")
90
+ if marker_start < 0:
91
+ return ""
92
+ marker_start += 3
93
+ marker_end = selected_filter.find(")", marker_start)
94
+ if marker_end < 0:
95
+ return ""
96
+ pattern = selected_filter[marker_start:marker_end].split()[0]
97
+ return pattern.lstrip("*. ").lower()
@@ -1,19 +1,30 @@
1
1
  from pathlib import Path
2
2
  import time
3
3
 
4
- from PySide6.QtCore import QEvent, QSettings, QTimer, Qt
5
- from PySide6.QtGui import QAction, QIcon, QKeySequence
4
+ import pystitch as emb
5
+ from PySide6.QtCore import QEvent, QSettings, QSignalBlocker, QTimer, Qt
6
+ from PySide6.QtGui import QAction, QColor, QIcon, QKeySequence
6
7
  from PySide6.QtWidgets import (
8
+ QAbstractItemView,
7
9
  QApplication,
8
10
  QDialog,
11
+ QDockWidget,
9
12
  QFileDialog,
13
+ QHeaderView,
10
14
  QLabel,
11
15
  QMainWindow,
16
+ QTableWidget,
17
+ QTableWidgetItem,
12
18
  QVBoxLayout,
13
19
  QWidget,
14
20
  )
15
21
 
16
22
  from ..constants import *
23
+ from ..formats import (
24
+ extension_from_output_filter,
25
+ get_supported_output_filter,
26
+ get_supported_output_formats,
27
+ )
17
28
  from ..render import render_export_image
18
29
  from .dialogs import EmbroideryOpenDialog
19
30
  from .about import show_about
@@ -65,6 +76,9 @@ class MainWindow(QMainWindow):
65
76
  layout.addWidget(self.progress)
66
77
  self.setCentralWidget(main_panel)
67
78
  self._main_panel = main_panel
79
+ self._updating_command_panel = False
80
+ self._build_command_dock()
81
+ self.viewer.cursor_changed.connect(self._sync_command_panel_cursor)
68
82
  self.shortcut_filter = ViewerShortcutFilter(self, self.viewer)
69
83
  self._build_menus()
70
84
  self.statusBar().showMessage(DEFAULT_STATUS_TEXT)
@@ -167,6 +181,7 @@ class MainWindow(QMainWindow):
167
181
  def _build_menus(self):
168
182
  file_menu = self.menuBar().addMenu("&File")
169
183
  self._action(file_menu, "Open embroidery file", self.open_file_dialog, "Ctrl+O")
184
+ self._action(file_menu, "Save as embroidery...", self.save_as_embroidery)
170
185
  export_menu = file_menu.addMenu("Export")
171
186
  self._action(export_menu, "Shaded PNG for print...", self.export_shaded_png)
172
187
  self._action(export_menu, "Preview PNG...", self.export_icon_png)
@@ -174,7 +189,7 @@ class MainWindow(QMainWindow):
174
189
  self._action(file_menu, "Center design", self.viewer.center_design)
175
190
  self._action(file_menu, "Fit design to window", self.viewer.fit_to_screen)
176
191
  self._action(file_menu, "Fullscreen", self.toggle_full_screen)
177
- self.grid_action = self._action(file_menu, "Show 1cm grid", self.toggle_grid, "G", True)
192
+ self.grid_action = self._action(file_menu, "Show measurement grid", self.toggle_grid, "G", True)
178
193
  self.grid_action.setChecked(True)
179
194
  self.realistic_action = self._action(file_menu, "Realistic thread render", self.toggle_realistic, checkable=True)
180
195
  self.viewer.grid_toggled.connect(self.grid_action.setChecked)
@@ -191,6 +206,15 @@ class MainWindow(QMainWindow):
191
206
  self._action(file_menu, "Rotate right 90 deg", lambda: self.viewer.rotate_design(1))
192
207
  file_menu.addSeparator()
193
208
  self._action(file_menu, "Quit", self.request_quit, "Ctrl+Q")
209
+ view_menu = self.menuBar().addMenu("&View")
210
+ self.command_panel_action = self._action(
211
+ view_menu,
212
+ "Command list",
213
+ self.toggle_command_panel,
214
+ "Ctrl+L",
215
+ True,
216
+ )
217
+ self.command_panel_action.setChecked(False)
194
218
  playback = self.menuBar().addMenu("&Playback")
195
219
  for step in (1, 10, 50, 100, 500):
196
220
  action = self._action(playback, f"Step {step}", lambda checked=False, s=step: self.viewer.set_step_size(s))
@@ -225,6 +249,117 @@ class MainWindow(QMainWindow):
225
249
  self.viewer.update()
226
250
  self.progress.update()
227
251
 
252
+ def _build_command_dock(self):
253
+ self.command_table = QTableWidget(0, 4, self)
254
+ command_font = self.command_table.font()
255
+ command_font.setPointSize(max(8, command_font.pointSize() - 1))
256
+ self.command_table.setFont(command_font)
257
+ self.command_table.horizontalHeader().hide()
258
+ self.command_table.verticalHeader().hide()
259
+ self.command_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
260
+ for column in range(1, 4):
261
+ self.command_table.horizontalHeader().setSectionResizeMode(
262
+ column,
263
+ QHeaderView.ResizeToContents,
264
+ )
265
+ self.command_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
266
+ self.command_table.setWordWrap(False)
267
+ self.command_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
268
+ self.command_table.setSelectionBehavior(QAbstractItemView.SelectRows)
269
+ self.command_table.setSelectionMode(QAbstractItemView.SingleSelection)
270
+ self.command_table.setAlternatingRowColors(True)
271
+ self.command_table.currentCellChanged.connect(
272
+ self._command_panel_current_cell_changed
273
+ )
274
+
275
+ self.command_dock = QDockWidget("Commands", self)
276
+ self.command_dock.setObjectName("commandDock")
277
+ self.command_dock.setWidget(self.command_table)
278
+ self.command_dock.setMinimumWidth(360)
279
+ self.command_dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
280
+ self.addDockWidget(Qt.RightDockWidgetArea, self.command_dock)
281
+ self.command_dock.hide()
282
+ self.command_dock.visibilityChanged.connect(self._command_dock_visibility_changed)
283
+
284
+ def _command_dock_visibility_changed(self, visible):
285
+ if hasattr(self, "command_panel_action"):
286
+ self.command_panel_action.setChecked(visible)
287
+ if visible:
288
+ self.refresh_command_panel()
289
+
290
+ def toggle_command_panel(self, checked):
291
+ self.command_dock.setVisible(checked)
292
+
293
+ def refresh_command_panel(self):
294
+ table = self.command_table
295
+ with QSignalBlocker(table):
296
+ table.setRowCount(len(self.viewer.command_timeline))
297
+ for row, (label, position, stitch_index, x, y) in enumerate(
298
+ self.viewer.command_timeline
299
+ ):
300
+ position_text = str(position) if stitch_index >= 0 else f"after {position}"
301
+ color = self._command_panel_color(label)
302
+ values = (
303
+ label,
304
+ position_text,
305
+ f"{x:.2f}",
306
+ f"{y:.2f}",
307
+ )
308
+ for column, value in enumerate(values):
309
+ item = table.item(row, column)
310
+ if item is None:
311
+ item = QTableWidgetItem()
312
+ table.setItem(row, column, item)
313
+ item.setText(value)
314
+ item.setData(Qt.UserRole, row)
315
+ item.setForeground(color)
316
+ self._sync_command_panel_cursor()
317
+
318
+ def _command_panel_color(self, label):
319
+ if label == "STITCH":
320
+ return QColor(35, 35, 35)
321
+ if label == "JUMP":
322
+ return QColor(110, 110, 110)
323
+ if label.startswith("COLOR CHANGE"):
324
+ return QColor(190, 45, 45)
325
+ if label == "TRIM":
326
+ return QColor(210, 125, 20)
327
+ return QColor(55, 95, 160)
328
+
329
+ def _sync_command_panel_cursor(self):
330
+ if self._updating_command_panel:
331
+ return
332
+ if not self.command_dock.isVisible():
333
+ return
334
+ command_index = self.viewer.current_command_index()
335
+ if command_index < 0:
336
+ return
337
+ with QSignalBlocker(self.command_table):
338
+ self.command_table.setCurrentCell(command_index, 0)
339
+ self.command_table.scrollToItem(
340
+ self.command_table.item(command_index, 0),
341
+ QAbstractItemView.PositionAtCenter,
342
+ )
343
+
344
+ def _command_panel_current_cell_changed(self, current_row, current_column,
345
+ _previous_row, _previous_column):
346
+ if current_row < 0:
347
+ return
348
+ item = self.command_table.item(current_row, current_column)
349
+ if item is None:
350
+ item = self.command_table.item(current_row, 0)
351
+ if item is None:
352
+ return
353
+ command_index = item.data(Qt.UserRole)
354
+ if command_index is None:
355
+ return
356
+ self._updating_command_panel = True
357
+ try:
358
+ self.viewer._set_visible_count_from_command_index(int(command_index))
359
+ self.progress.update()
360
+ finally:
361
+ self._updating_command_panel = False
362
+
228
363
  def show_window(self, focus=True):
229
364
  """Show the window and optionally request keyboard focus."""
230
365
  if self.isMinimized():
@@ -292,6 +427,78 @@ class MainWindow(QMainWindow):
292
427
  selected_path = Path(path)
293
428
  return selected_path.with_suffix(extension)
294
429
 
430
+ def _default_save_name(self):
431
+ base_name = self.current_file_path.stem if self.current_file_path else "inksim"
432
+ current_extension = (
433
+ self.current_file_path.suffix.lstrip(".").lower()
434
+ if self.current_file_path else ""
435
+ )
436
+ writable_extensions = {
437
+ file_type["extension"] for file_type in get_supported_output_formats()
438
+ }
439
+ if current_extension not in writable_extensions:
440
+ current_extension = "dst"
441
+ return f"{base_name}.{current_extension or 'dst'}"
442
+
443
+ def _choose_save_as_path(self):
444
+ output_filter = get_supported_output_filter()
445
+ export_directory = Path(self.last_directory or Path.cwd())
446
+ if self.current_file_path is not None:
447
+ export_directory = self.current_file_path.parent
448
+ default_path = export_directory / self._default_save_name()
449
+ dialog = QFileDialog(self, "Save embroidery as", str(default_path))
450
+ dialog.setAcceptMode(QFileDialog.AcceptSave)
451
+ dialog.setNameFilters(output_filter.split(";;"))
452
+ dialog.selectFile(str(default_path))
453
+
454
+ def on_filter_selected(selected_filter):
455
+ extension = extension_from_output_filter(selected_filter)
456
+ if not extension:
457
+ return
458
+ selected_files = dialog.selectedFiles()
459
+ selected_file = selected_files[0] if selected_files else str(default_path)
460
+ dialog.selectFile(self._path_with_output_extension(selected_file, extension))
461
+
462
+ dialog.filterSelected.connect(on_filter_selected)
463
+ if dialog.exec() != QDialog.Accepted:
464
+ return None
465
+ selected_files = dialog.selectedFiles()
466
+ if not selected_files:
467
+ return None
468
+ path = selected_files[0]
469
+ selected_filter = dialog.selectedNameFilter()
470
+ if not path:
471
+ return None
472
+ selected_path = Path(path)
473
+ if selected_path.suffix:
474
+ return selected_path
475
+ extension = extension_from_output_filter(selected_filter)
476
+ if not extension:
477
+ extension = "dst"
478
+ return selected_path.with_suffix(f".{extension}")
479
+
480
+ def _path_with_output_extension(self, path, extension):
481
+ return str(Path(path).with_suffix(f".{extension}"))
482
+
483
+ def save_as_embroidery(self):
484
+ path = self._choose_save_as_path()
485
+ if path is None:
486
+ return False
487
+ return self.save_embroidery_to_path(path)
488
+
489
+ def save_embroidery_to_path(self, path):
490
+ pattern = self.viewer.pattern
491
+ if pattern is None:
492
+ QMessageBox.warning(self, "Save embroidery", "No embroidery file is loaded.")
493
+ return False
494
+ try:
495
+ emb.write(pattern, str(path))
496
+ except (OSError, RuntimeError, ValueError) as error:
497
+ QMessageBox.critical(self, "Save embroidery", f"Failed to save file: {error}")
498
+ return False
499
+ self.statusBar().showMessage(f"Saved {path}", 3000)
500
+ return True
501
+
295
502
  def export_png(
296
503
  self,
297
504
  path,
@@ -370,8 +577,14 @@ class MainWindow(QMainWindow):
370
577
  self.setWindowTitle(f"{APP_TITLE} - {selected_path.name} - {total} sts - "
371
578
  f"{bounds[2] - bounds[0]:.1f}x{bounds[3] - bounds[1]:.1f}mm")
372
579
  self.progress.update()
580
+ self.refresh_command_panel()
373
581
  return True
374
582
 
583
+ def show_command_panel(self):
584
+ self.command_dock.show()
585
+ self.command_dock.raise_()
586
+ self.refresh_command_panel()
587
+
375
588
  def toggle_full_screen(self):
376
589
  self.is_fullscreen = not self.is_fullscreen
377
590
  self.mode_status.setVisible(not self.is_fullscreen)
@@ -46,6 +46,7 @@ class ViewerShortcutFilter(QObject):
46
46
  total = viewer.stitches_np.shape[0]
47
47
  is_shift = bool(event.modifiers() & Qt.ShiftModifier)
48
48
  changed = False
49
+ cursor_changed = False
49
50
  highlight_needle = False
50
51
  step = 1 if is_alt else viewer.step_size
51
52
 
@@ -54,6 +55,7 @@ class ViewerShortcutFilter(QObject):
54
55
  Qt.Key_Left,
55
56
  ):
56
57
  changed = viewer.jump_to_command(1 if key == Qt.Key_Right else -1)
58
+ cursor_changed = changed
57
59
  highlight_needle = changed
58
60
  if changed and viewer.is_playing:
59
61
  viewer.play_timer.stop()
@@ -68,6 +70,7 @@ class ViewerShortcutFilter(QObject):
68
70
  viewer.jump_to_color(1 if key == Qt.Key_Right else -1)
69
71
  viewer._last_dir = 1 if key == Qt.Key_Right else -1
70
72
  changed = True
73
+ cursor_changed = True
71
74
  highlight_needle = True
72
75
  elif not is_alt and not is_ctrl and key in (
73
76
  Qt.Key_W, Qt.Key_A, Qt.Key_S, Qt.Key_D
@@ -87,25 +90,31 @@ class ViewerShortcutFilter(QObject):
87
90
  viewer.visible_count = min(total, viewer.visible_count + step)
88
91
  viewer._last_dir = 1
89
92
  changed = True
93
+ cursor_changed = True
90
94
  elif key == Qt.Key_Left:
91
95
  if viewer.visible_count > 0:
92
96
  viewer.visible_count = max(0, viewer.visible_count - step)
93
97
  viewer._last_dir = -1
94
98
  changed = True
99
+ cursor_changed = True
95
100
  elif key == Qt.Key_Up:
96
101
  viewer.visible_count = min(total, viewer.visible_count + step * 10)
97
102
  viewer._last_dir = 1
98
103
  changed = True
104
+ cursor_changed = True
99
105
  elif key == Qt.Key_Down:
100
106
  viewer.visible_count = max(0, viewer.visible_count - step * 10)
101
107
  viewer._last_dir = -1
102
108
  changed = True
109
+ cursor_changed = True
103
110
  elif key == Qt.Key_Home:
104
111
  viewer.visible_count = 0
105
112
  changed = True
113
+ cursor_changed = True
106
114
  elif key == Qt.Key_End:
107
115
  viewer.visible_count = total
108
116
  changed = True
117
+ cursor_changed = True
109
118
  elif key == Qt.Key_Space:
110
119
  viewer.toggle_auto_play()
111
120
  return True
@@ -153,7 +162,7 @@ class ViewerShortcutFilter(QObject):
153
162
  )
154
163
  changed = True
155
164
  elif key == Qt.Key_C and not is_alt and not is_ctrl:
156
- viewer.center_design()
165
+ viewer.center_needle()
157
166
  return True
158
167
  elif key == Qt.Key_F and not is_alt and not is_ctrl:
159
168
  viewer.fit_to_screen()
@@ -206,6 +215,8 @@ class ViewerShortcutFilter(QObject):
206
215
  return False
207
216
 
208
217
  if changed:
218
+ if cursor_changed and hasattr(viewer, "notify_cursor_changed"):
219
+ viewer.notify_cursor_changed()
209
220
  if highlight_needle:
210
221
  viewer.highlight_needle()
211
222
  if (