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.
- {inksim-0.2.3 → inksim-0.2.4}/PKG-INFO +1 -1
- {inksim-0.2.3 → inksim-0.2.4}/pyproject.toml +1 -1
- {inksim-0.2.3 → inksim-0.2.4}/pyproject.toml.orig +1 -1
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/cli.py +1 -1
- inksim-0.2.4/src/inksim/formats.py +97 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/frame.py +216 -3
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/shortcuts.py +12 -1
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/viewer.py +288 -30
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/density.py +6 -5
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/grid.py +47 -8
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/viewport.py +2 -2
- inksim-0.2.3/src/inksim/formats.py +0 -27
- {inksim-0.2.3 → inksim-0.2.4}/LICENSE +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/README.md +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/__init__.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/__main__.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/assets/InkSim_colorful_small.png +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/assets/app_icons/inksim.svg +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/constants.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/debug.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/__init__.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/about.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/dialogs.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/help.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/renderer_picker.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/settings.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/splash.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/status.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/gui/timeline.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/interconnect.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/__init__.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/export.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/fabric.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/previews.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/registry.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches_numba.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/stitches_qt.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/render/vintage_qt.py +0 -0
- {inksim-0.2.3 → inksim-0.2.4}/src/inksim/runtime.py +0 -0
|
@@ -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
|
-
|
|
5
|
-
from PySide6.
|
|
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
|
|
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.
|
|
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 (
|