shinestacker 0.2.0.post1.dev1__py3-none-any.whl

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.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,599 @@
1
+ import os
2
+ import subprocess
3
+ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QMessageBox,
4
+ QSplitter, QToolBar, QMenu, QComboBox, QStackedWidget)
5
+ from PySide6.QtCore import Qt, Signal
6
+ from PySide6.QtGui import QGuiApplication, QAction, QIcon, QPixmap
7
+ from .. config.constants import constants
8
+ from .. core.core_utils import running_under_windows, running_under_macos, get_app_base_path
9
+ from .colors import ColorPalette
10
+ from .project_model import Project
11
+ from .actions_window import ActionsWindow
12
+ from .gui_logging import LogManager
13
+ from .gui_run import RunWindow, RunWorker
14
+ from .project_converter import ProjectConverter
15
+ from .project_model import get_action_working_path, get_action_input_path, get_action_output_path
16
+
17
+
18
+ class JobLogWorker(RunWorker):
19
+ def __init__(self, job, id_str):
20
+ super().__init__(id_str)
21
+ self.job = job
22
+ self.tag = "Job"
23
+
24
+ def do_run(self):
25
+ converter = ProjectConverter()
26
+ return converter.run_job(self.job, self.id_str, self.callbacks)
27
+
28
+
29
+ class ProjectLogWorker(RunWorker):
30
+ def __init__(self, project, id_str):
31
+ super().__init__(id_str)
32
+ self.project = project
33
+ self.tag = "Project"
34
+
35
+ def do_run(self):
36
+ converter = ProjectConverter()
37
+ return converter.run_project(self.project, self.id_str, self.callbacks)
38
+
39
+
40
+ LIST_STYLE_SHEET = f"""
41
+ QListWidget::item:selected {{
42
+ background-color: #{ColorPalette.LIGHT_BLUE.hex()};;
43
+ }}
44
+ """
45
+
46
+
47
+ class TabWidgetWithPlaceholder(QWidget):
48
+ # Segnali aggiuntivi per mantenere la compatibilità
49
+ currentChanged = Signal(int)
50
+ tabCloseRequested = Signal(int)
51
+
52
+ def __init__(self, parent=None):
53
+ super().__init__(parent)
54
+ self.layout = QVBoxLayout(self)
55
+ self.layout.setContentsMargins(0, 0, 0, 0)
56
+ self.stacked_widget = QStackedWidget()
57
+ self.layout.addWidget(self.stacked_widget)
58
+ self.tab_widget = QTabWidget()
59
+ self.stacked_widget.addWidget(self.tab_widget)
60
+ self.placeholder = QLabel()
61
+ self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
62
+ rel_path = 'ico/focus_stack_bkg.png'
63
+ icon_path = f'{get_app_base_path()}/{rel_path}'
64
+ if not os.path.exists(icon_path):
65
+ icon_path = f'{get_app_base_path()}/../{rel_path}'
66
+ if os.path.exists(icon_path):
67
+ pixmap = QPixmap(icon_path)
68
+ # Ridimensiona mantenendo le proporzioni (es. max 400x400)
69
+ pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
70
+ Qt.TransformationMode.SmoothTransformation)
71
+ self.placeholder.setPixmap(pixmap)
72
+ else:
73
+ self.placeholder.setText("Run logs will appear here.")
74
+ self.stacked_widget.addWidget(self.placeholder)
75
+ self.tab_widget.currentChanged.connect(self._on_current_changed)
76
+ self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
77
+ self.update_placeholder_visibility()
78
+
79
+ def _on_current_changed(self, index):
80
+ self.currentChanged.emit(index)
81
+ self.update_placeholder_visibility()
82
+
83
+ def _on_tab_close_requested(self, index):
84
+ self.tabCloseRequested.emit(index)
85
+ self.update_placeholder_visibility()
86
+
87
+ def update_placeholder_visibility(self):
88
+ if self.tab_widget.count() == 0:
89
+ self.stacked_widget.setCurrentIndex(1)
90
+ else:
91
+ self.stacked_widget.setCurrentIndex(0)
92
+
93
+ def addTab(self, widget, label):
94
+ result = self.tab_widget.addTab(widget, label)
95
+ self.update_placeholder_visibility()
96
+ return result
97
+
98
+ def removeTab(self, index):
99
+ result = self.tab_widget.removeTab(index)
100
+ self.update_placeholder_visibility()
101
+ return result
102
+
103
+ def count(self):
104
+ return self.tab_widget.count()
105
+
106
+ def setCurrentIndex(self, index):
107
+ return self.tab_widget.setCurrentIndex(index)
108
+
109
+ def currentIndex(self):
110
+ return self.tab_widget.currentIndex()
111
+
112
+ def currentWidget(self):
113
+ return self.tab_widget.currentWidget()
114
+
115
+ def widget(self, index):
116
+ return self.tab_widget.widget(index)
117
+
118
+ def indexOf(self, widget):
119
+ return self.tab_widget.indexOf(widget)
120
+
121
+
122
+ class MainWindow(ActionsWindow, LogManager):
123
+ def __init__(self):
124
+ ActionsWindow.__init__(self)
125
+ LogManager.__init__(self)
126
+ self._windows = []
127
+ self._workers = []
128
+ self.retouch_callback = None
129
+ self.job_list.setStyleSheet(LIST_STYLE_SHEET)
130
+ self.action_list.setStyleSheet(LIST_STYLE_SHEET)
131
+ menubar = self.menuBar()
132
+ self.add_file_menu(menubar)
133
+ self.add_edit_menu(menubar)
134
+ self.add_view_menu(menubar)
135
+ self.add_job_menu(menubar)
136
+ self.add_actions_menu(menubar)
137
+ self.add_help_menu(menubar)
138
+ toolbar = QToolBar(self)
139
+ self.addToolBar(Qt.TopToolBarArea, toolbar)
140
+ self.fill_toolbar(toolbar)
141
+ self.resize(1200, 800)
142
+ center = QGuiApplication.primaryScreen().geometry().center()
143
+ self.move(center - self.rect().center())
144
+ self.set_project(Project())
145
+ self.central_widget = QWidget()
146
+ self.setCentralWidget(self.central_widget)
147
+ layout = QVBoxLayout()
148
+ layout.setContentsMargins(0, 0, 0, 0)
149
+ h_splitter = QSplitter(Qt.Orientation.Vertical)
150
+ h_layout = QHBoxLayout()
151
+ h_layout.setContentsMargins(10, 0, 10, 10)
152
+ top_widget = QWidget()
153
+ top_widget.setLayout(h_layout)
154
+ h_splitter.addWidget(top_widget)
155
+ self.tab_widget = TabWidgetWithPlaceholder()
156
+ self.tab_widget.resize(1000, 500)
157
+ h_splitter.addWidget(self.tab_widget)
158
+ self.job_list.currentRowChanged.connect(self.on_job_selected)
159
+ self.job_list.itemDoubleClicked.connect(self.on_job_edit)
160
+ self.action_list.itemDoubleClicked.connect(self.on_action_edit)
161
+ vbox_left = QVBoxLayout()
162
+ vbox_left.setSpacing(4)
163
+ vbox_left.addWidget(QLabel("Jobs"))
164
+ vbox_left.addWidget(self.job_list)
165
+ vbox_right = QVBoxLayout()
166
+ vbox_right.setSpacing(4)
167
+ vbox_right.addWidget(QLabel("Actions"))
168
+ vbox_right.addWidget(self.action_list)
169
+ self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
170
+ self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
171
+ h_layout.addLayout(vbox_left)
172
+ h_layout.addLayout(vbox_right)
173
+ layout.addWidget(h_splitter)
174
+ self.central_widget.setLayout(layout)
175
+
176
+ def set_retouch_callback(self, callback):
177
+ self.retouch_callback = callback
178
+
179
+ def add_file_menu(self, menubar):
180
+ menu = menubar.addMenu("&File")
181
+ new_action = QAction("&New...", self)
182
+ new_action.setShortcut("Ctrl+N")
183
+ new_action.triggered.connect(self.new_project)
184
+ menu.addAction(new_action)
185
+ open_action = QAction("&Open...", self)
186
+ open_action.setShortcut("Ctrl+O")
187
+ open_action.triggered.connect(self.open_project)
188
+ menu.addAction(open_action)
189
+ save_action = QAction("&Save", self)
190
+ save_action.setShortcut("Ctrl+S")
191
+ save_action.triggered.connect(self.save_project)
192
+ menu.addAction(save_action)
193
+ save_as_action = QAction("Save &As...", self)
194
+ save_as_action.setShortcut("Ctrl+Shift+S")
195
+ save_as_action.triggered.connect(self.save_project_as)
196
+ menu.addAction(save_as_action)
197
+ close_action = QAction("&Close", self)
198
+ close_action.setShortcut("Ctrl+W")
199
+ close_action.triggered.connect(self.close_project)
200
+ menu.addAction(close_action)
201
+
202
+ def add_edit_menu(self, menubar):
203
+ menu = menubar.addMenu("&Edit")
204
+ undo_action = QAction("&Undo", self)
205
+ undo_action.setShortcut("Ctrl+Z")
206
+ undo_action.triggered.connect(self.undo)
207
+ menu.addAction(undo_action)
208
+ menu.addSeparator()
209
+ cut_action = QAction("&Cut", self)
210
+ cut_action.setShortcut("Ctrl+X")
211
+ cut_action.triggered.connect(self.cut_element)
212
+ menu.addAction(cut_action)
213
+ copy_action = QAction("Cop&y", self)
214
+ copy_action.setShortcut("Ctrl+C")
215
+ copy_action.triggered.connect(self.copy_element)
216
+ menu.addAction(copy_action)
217
+ paste_action = QAction("&Paste", self)
218
+ paste_action.setShortcut("Ctrl+V")
219
+ paste_action.triggered.connect(self.paste_element)
220
+ menu.addAction(paste_action)
221
+ clone_action = QAction("Duplicate", self)
222
+ clone_action.setShortcut("Ctrl+D")
223
+ clone_action.triggered.connect(self.clone_element)
224
+ menu.addAction(clone_action)
225
+ self.delete_element_action = QAction("Delete", self)
226
+ self.delete_element_action.setShortcut("Del") # Qt.Key_Backspace
227
+ self.delete_element_action.setIcon(self.get_icon("close-round-line-icon"))
228
+ self.delete_element_action.setToolTip("delete")
229
+ self.delete_element_action.triggered.connect(self.delete_element)
230
+ self.delete_element_action.setEnabled(False)
231
+ menu.addAction(self.delete_element_action)
232
+ menu.addSeparator()
233
+ up_action = QAction("Move &Up", self)
234
+ up_action.setShortcut("Ctrl+Up")
235
+ up_action.triggered.connect(self.move_element_up)
236
+ menu.addAction(up_action)
237
+ down_action = QAction("Move &Down", self)
238
+ down_action.setShortcut("Ctrl+Down")
239
+ down_action.triggered.connect(self.move_element_down)
240
+ menu.addAction(down_action)
241
+ menu.addSeparator()
242
+ self.enable_action = QAction("E&nable", self)
243
+ self.enable_action.setShortcut("Ctrl+E")
244
+ self.enable_action.triggered.connect(self.enable)
245
+ menu.addAction(self.enable_action)
246
+ self.disable_action = QAction("Di&sable", self)
247
+ self.disable_action.setShortcut("Ctrl+B")
248
+ self.disable_action.triggered.connect(self.disable)
249
+ menu.addAction(self.disable_action)
250
+ enable_all_action = QAction("Enable All", self)
251
+ enable_all_action.setShortcut("Ctrl+Shift+E")
252
+ enable_all_action.triggered.connect(self.enable_all)
253
+ menu.addAction(enable_all_action)
254
+ disable_all_action = QAction("Disable All", self)
255
+ disable_all_action.setShortcut("Ctrl+Shift+B")
256
+ disable_all_action.triggered.connect(self.disable_all)
257
+ menu.addAction(disable_all_action)
258
+
259
+ def add_view_menu(self, menubar):
260
+ menu = menubar.addMenu("&View")
261
+ self.expert_options_action = QAction("Expert Options", self)
262
+ self.expert_options_action.setShortcut("Ctrl+Shift+X")
263
+ self.expert_options_action.triggered.connect(self.toggle_expert_options)
264
+ self.expert_options_action.setCheckable(True)
265
+ self.expert_options_action.setChecked(self.expert_options)
266
+ menu.addAction(self.expert_options_action)
267
+
268
+ def add_job_menu(self, menubar):
269
+ menu = menubar.addMenu("&Jobs")
270
+ self.add_job_action = QAction("Add Job", self)
271
+ self.add_job_action.setShortcut("Ctrl+P")
272
+ self.add_job_action.setIcon(self.get_icon("plus-round-line-icon"))
273
+ self.add_job_action.setToolTip("Add job")
274
+ self.add_job_action.triggered.connect(self.add_job)
275
+ menu.addAction(self.add_job_action)
276
+ menu.addSeparator()
277
+ self.run_job_action = QAction("Run Job", self)
278
+ self.run_job_action.setShortcut("Ctrl+J")
279
+ self.run_job_action.setIcon(self.get_icon("play-button-round-icon"))
280
+ self.run_job_action.setToolTip("Run job")
281
+ self.run_job_action.setEnabled(False)
282
+ self.run_job_action.triggered.connect(self.run_job)
283
+ menu.addAction(self.run_job_action)
284
+ self.run_all_jobs_action = QAction("Run All Jobs", self)
285
+ self.run_all_jobs_action.setShortcut("Ctrl+Shift+J")
286
+ self.run_all_jobs_action.setIcon(self.get_icon("forward-button-icon"))
287
+ self.run_all_jobs_action.setToolTip("Run all jobs")
288
+ self.run_all_jobs_action.setEnabled(False)
289
+ self.run_all_jobs_action.triggered.connect(self.run_all_jobs)
290
+ menu.addAction(self.run_all_jobs_action)
291
+
292
+ def add_actions_menu(self, menubar):
293
+ menu = menubar.addMenu("&Actions")
294
+ add_action_menu = QMenu("Add Action", self)
295
+ for action in constants.ACTION_TYPES:
296
+ entry_action = QAction(action, self)
297
+ entry_action.triggered.connect({
298
+ constants.ACTION_COMBO: self.add_action_CombinedActions,
299
+ constants.ACTION_NOISEDETECTION: self.add_action_NoiseDetection,
300
+ constants.ACTION_FOCUSSTACK: self.add_action_FocusStack,
301
+ constants.ACTION_FOCUSSTACKBUNCH: self.add_action_FocusStackBunch,
302
+ constants.ACTION_MULTILAYER: self.add_action_MultiLayer
303
+ }[action])
304
+ add_action_menu.addAction(entry_action)
305
+ menu.addMenu(add_action_menu)
306
+ add_sub_action_menu = QMenu("Add Sub Action", self)
307
+ self.sub_action_menu_entries = []
308
+ for action in constants.SUB_ACTION_TYPES:
309
+ entry_action = QAction(action, self)
310
+ entry_action.triggered.connect({
311
+ constants.ACTION_MASKNOISE: self.add_sub_action_MakeNoise,
312
+ constants.ACTION_VIGNETTING: self.add_sub_action_Vignetting,
313
+ constants.ACTION_ALIGNFRAMES: self.add_sub_action_AlignFrames,
314
+ constants.ACTION_BALANCEFRAMES: self.add_sub_action_BalanceFrames
315
+ }[action])
316
+ entry_action.setEnabled(False)
317
+ self.sub_action_menu_entries.append(entry_action)
318
+ add_sub_action_menu.addAction(entry_action)
319
+ menu.addMenu(add_sub_action_menu)
320
+
321
+ def add_help_menu(self, menubar):
322
+ menu = menubar.addMenu("&Help")
323
+ menu.setObjectName("Help")
324
+
325
+ def fill_toolbar(self, toolbar):
326
+ toolbar.addAction(self.add_job_action)
327
+ toolbar.addSeparator()
328
+ self.action_selector = QComboBox()
329
+ self.action_selector.addItems(constants.ACTION_TYPES)
330
+ self.action_selector.setEnabled(False)
331
+ toolbar.addWidget(self.action_selector)
332
+ self.add_action_entry_action = QAction("Add Action", self)
333
+ self.add_action_entry_action.setIcon(QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
334
+ self.add_action_entry_action.setToolTip("Add action")
335
+ self.add_action_entry_action.triggered.connect(self.add_action)
336
+ self.add_action_entry_action.setEnabled(False)
337
+ toolbar.addAction(self.add_action_entry_action)
338
+ self.sub_action_selector = QComboBox()
339
+ self.sub_action_selector.addItems(constants.SUB_ACTION_TYPES)
340
+ self.sub_action_selector.setEnabled(False)
341
+ toolbar.addWidget(self.sub_action_selector)
342
+ self.add_sub_action_entry_action = QAction("Add Sub Action", self)
343
+ self.add_sub_action_entry_action.setIcon(QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
344
+ self.add_sub_action_entry_action.setToolTip("Add sub action")
345
+ self.add_sub_action_entry_action.triggered.connect(self.add_sub_action)
346
+ self.add_sub_action_entry_action.setEnabled(False)
347
+ toolbar.addAction(self.add_sub_action_entry_action)
348
+ toolbar.addSeparator()
349
+ toolbar.addAction(self.delete_element_action)
350
+ toolbar.addSeparator()
351
+ toolbar.addAction(self.run_job_action)
352
+ toolbar.addAction(self.run_all_jobs_action)
353
+
354
+ def contextMenuEvent(self, event):
355
+ item = self.job_list.itemAt(self.job_list.viewport().mapFrom(self, event.pos()))
356
+ current_action = None
357
+ if item:
358
+ index = self.job_list.row(item)
359
+ current_action = self.get_job_at(index)
360
+ self.job_list.setCurrentRow(index)
361
+ item = self.action_list.itemAt(self.action_list.viewport().mapFrom(self, event.pos()))
362
+ if item:
363
+ index = self.action_list.row(item)
364
+ self.action_list.setCurrentRow(index)
365
+ job_row, action_row, pos = self.get_action_at(index)
366
+ current_action = pos.action if not pos.is_sub_action else pos.sub_action
367
+ if current_action:
368
+ menu = QMenu(self)
369
+ if current_action.enabled():
370
+ menu.addAction(self.disable_action)
371
+ else:
372
+ menu.addAction(self.enable_action)
373
+ edit_config_action = QAction("Edit configuration")
374
+ edit_config_action.triggered.connect(self.edit_current_action)
375
+ menu.addAction(edit_config_action)
376
+ menu.addSeparator()
377
+ self.current_action_working_path, name = get_action_working_path(current_action)
378
+ if self.current_action_working_path != '' and os.path.exists(self.current_action_working_path):
379
+ action_name = "Browse Working Path" + (f" > {name}" if name != '' else '')
380
+ self.browse_working_path_action = QAction(action_name)
381
+ self.browse_working_path_action.triggered.connect(self.browse_working_path_path)
382
+ menu.addAction(self.browse_working_path_action)
383
+ ip, name = get_action_input_path(current_action)
384
+ if ip != '':
385
+ ips = ip.split(constants.PATH_SEPARATOR)
386
+ self.current_action_input_path = constants.PATH_SEPARATOR.join([f"{self.current_action_working_path}/{ip}" for ip in ips])
387
+ p_exists = False
388
+ for p in self.current_action_input_path.split(constants.PATH_SEPARATOR):
389
+ if os.path.exists(p):
390
+ p_exists = True
391
+ break
392
+ if p_exists:
393
+ action_name = "Browse Input Path" + (f" > {name}" if name != '' else '')
394
+ self.browse_input_path_action = QAction(action_name)
395
+ self.browse_input_path_action.triggered.connect(self.browse_input_path_path)
396
+ menu.addAction(self.browse_input_path_action)
397
+ op, name = get_action_output_path(current_action)
398
+ if op != '':
399
+ self.current_action_output_path = f"{self.current_action_working_path}/{op}"
400
+ if os.path.exists(self.current_action_output_path):
401
+ action_name = "Browse Output Path" + (f" > {name}" if name != '' else '')
402
+ self.browse_output_path_action = QAction(action_name)
403
+ self.browse_output_path_action.triggered.connect(self.browse_output_path_path)
404
+ menu.addAction(self.browse_output_path_action)
405
+ menu.addSeparator()
406
+ menu.addAction(self.run_job_action)
407
+ menu.addAction(self.run_all_jobs_action)
408
+ if current_action.type_name == constants.ACTION_JOB:
409
+ retouch_path = self.get_retouch_path(current_action)
410
+ if len(retouch_path) > 0:
411
+ menu.addSeparator()
412
+ self.job_retouch_path_action = QAction("Retouch path")
413
+ self.job_retouch_path_action.triggered.connect(lambda job: self.run_retouch_path(current_action, retouch_path))
414
+ menu.addAction(self.job_retouch_path_action)
415
+ menu.exec(event.globalPos())
416
+
417
+ def get_retouch_path(self, job):
418
+ frames_path = [get_action_output_path(action)[0]
419
+ for action in job.sub_actions if action.type_name == constants.ACTION_COMBO]
420
+ bunches_path = [get_action_output_path(action)[0]
421
+ for action in job.sub_actions if action.type_name == constants.ACTION_FOCUSSTACKBUNCH]
422
+ stack_path = [get_action_output_path(action)[0]
423
+ for action in job.sub_actions if action.type_name == constants.ACTION_FOCUSSTACK]
424
+ if len(bunches_path) > 0:
425
+ stack_path += [bunches_path[0]]
426
+ elif len(frames_path) > 0:
427
+ stack_path += [frames_path[0]]
428
+ wp = get_action_working_path(job)[0]
429
+ if wp == '':
430
+ raise ValueError("Job has no working path specified.")
431
+ stack_path = [f"{wp}/{s}" for s in stack_path]
432
+ return stack_path
433
+
434
+ def run_retouch_path(self, job, retouch_path):
435
+ self.retouch_callback(retouch_path)
436
+
437
+ def browse_path(self, path):
438
+ ps = path.split(constants.PATH_SEPARATOR)
439
+ for p in ps:
440
+ if os.path.exists(p):
441
+ if running_under_windows():
442
+ os.startfile(os.path.normpath(p))
443
+ elif running_under_macos():
444
+ subprocess.run(['open', p])
445
+ else:
446
+ subprocess.run(['xdg-open', p])
447
+
448
+ def browse_working_path_path(self):
449
+ self.browse_path(self.current_action_working_path)
450
+
451
+ def browse_input_path_path(self):
452
+ self.browse_path(self.current_action_input_path)
453
+
454
+ def browse_output_path_path(self):
455
+ self.browse_path(self.current_action_output_path)
456
+
457
+ def refresh_ui(self, job_row=-1, action_row=-1):
458
+ self.job_list.clear()
459
+ for job in self.project.jobs:
460
+ self.add_list_item(self.job_list, job, False)
461
+ if self.project.jobs:
462
+ self.job_list.setCurrentRow(0)
463
+ if job_row >= 0:
464
+ self.job_list.setCurrentRow(job_row)
465
+ if action_row >= 0:
466
+ self.action_list.setCurrentRow(action_row)
467
+ if self.job_list.count() == 0:
468
+ self.add_action_entry_action.setEnabled(False)
469
+ self.action_selector.setEnabled(False)
470
+ self.run_job_action.setEnabled(False)
471
+ self.run_all_jobs_action.setEnabled(False)
472
+ else:
473
+ self.add_action_entry_action.setEnabled(True)
474
+ self.action_selector.setEnabled(True)
475
+ self.delete_element_action.setEnabled(True)
476
+ self.run_job_action.setEnabled(True)
477
+ self.run_all_jobs_action.setEnabled(True)
478
+
479
+ def quit(self):
480
+ if self._check_unsaved_changes():
481
+ for worker in self._workers:
482
+ worker.stop()
483
+ self.close()
484
+
485
+ def toggle_expert_options(self):
486
+ self.expert_options = self.expert_options_action.isChecked()
487
+
488
+ def set_expert_options(self):
489
+ self.expert_options_action.setChecked(True)
490
+ self.expert_options = True
491
+
492
+ def before_thread_begins(self):
493
+ self.run_job_action.setEnabled(False)
494
+ self.run_all_jobs_action.setEnabled(False)
495
+
496
+ def get_tab_and_position(self, id_str):
497
+ for i in range(self.tab_widget.count()):
498
+ w = self.tab_widget.widget(i)
499
+ if w.id_str() == id_str:
500
+ return i, w
501
+
502
+ def get_tab_at_position(self, id_str):
503
+ i, w = self.get_tab_and_position(id_str)
504
+ return w
505
+
506
+ def get_tab_position(self, id_str):
507
+ i, w = self.get_tab_and_position(id_str)
508
+ return i
509
+
510
+ def do_handle_end_message(self, status, id_str, message):
511
+ self.run_job_action.setEnabled(True)
512
+ self.run_all_jobs_action.setEnabled(True)
513
+ tab = self.get_tab_at_position(id_str)
514
+ tab.close_button.setEnabled(True)
515
+ tab.stop_button.setEnabled(False)
516
+ if hasattr(tab, 'retouch_widget') and tab.retouch_widget is not None:
517
+ tab.retouch_widget.setEnabled(True)
518
+
519
+ def create_new_window(self, title, labels, retouch_paths):
520
+ new_window = RunWindow(labels,
521
+ lambda id_str: self.stop_worker(self.get_tab_position(id_str)),
522
+ lambda id_str: self.close_window(self.get_tab_position(id_str)),
523
+ retouch_paths,
524
+ self)
525
+ self.tab_widget.addTab(new_window, title)
526
+ self.tab_widget.setCurrentIndex(self.tab_widget.count() - 1)
527
+ if title is not None:
528
+ new_window.setWindowTitle(title)
529
+ new_window.show()
530
+ self.add_gui_logger(new_window)
531
+ self._windows.append(new_window)
532
+ return new_window, self.last_id_str()
533
+
534
+ def close_window(self, tab_position):
535
+ self._windows.pop(tab_position)
536
+ self._workers.pop(tab_position)
537
+ self.tab_widget.removeTab(tab_position)
538
+
539
+ def stop_worker(self, tab_position):
540
+ worker = self._workers[tab_position]
541
+ worker.stop()
542
+
543
+ def connect_signals(self, worker, window):
544
+ worker.before_action_signal.connect(window.handle_before_action)
545
+ worker.after_action_signal.connect(window.handle_after_action)
546
+ worker.step_counts_signal.connect(window.handle_step_counts)
547
+ worker.begin_steps_signal.connect(window.handle_begin_steps)
548
+ worker.end_steps_signal.connect(window.handle_end_steps)
549
+ worker.after_step_signal.connect(window.handle_after_step)
550
+ worker.save_plot_signal.connect(window.handle_save_plot)
551
+ worker.open_app_signal.connect(window.handle_open_app)
552
+
553
+ def set_enabled_sub_actions_gui(self, enabled):
554
+ self.add_sub_action_entry_action.setEnabled(enabled)
555
+ self.sub_action_selector.setEnabled(enabled)
556
+ for a in self.sub_action_menu_entries:
557
+ a.setEnabled(enabled)
558
+
559
+ def run_job(self):
560
+ current_index = self.job_list.currentRow()
561
+ if current_index < 0:
562
+ if len(self.project.jobs) > 0:
563
+ QMessageBox.warning(self, "No Job Selected", "Please select a job first.")
564
+ else:
565
+ QMessageBox.warning(self, "No Job Added", "Please add a job first.")
566
+ return
567
+ if current_index >= 0:
568
+ job = self.project.jobs[current_index]
569
+ if job.enabled():
570
+ job_name = job.params["name"]
571
+ labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
572
+ r = self.get_retouch_path(job)
573
+ retouch_paths = [] if len(r) == 0 else [(job_name, r)]
574
+ new_window, id_str = self.create_new_window("Job: " + job_name,
575
+ labels, retouch_paths)
576
+ worker = JobLogWorker(job, id_str)
577
+ self.connect_signals(worker, new_window)
578
+ self.start_thread(worker)
579
+ self._workers.append(worker)
580
+ else:
581
+ QMessageBox.warning(self, "Can't run Job", "Job " + job.params["name"] + " is disabled.")
582
+ return
583
+
584
+ def run_all_jobs(self):
585
+ labels = [[(self.action_text(a), a.enabled() and job.enabled()) for a in job.sub_actions] for job in self.project.jobs]
586
+ project_name = ".".join(self.current_file_name().split(".")[:-1])
587
+ if project_name == '':
588
+ project_name = '[new]'
589
+ retouch_paths = []
590
+ for job in self.project.jobs:
591
+ r = self.get_retouch_path(job)
592
+ if len(r) > 0:
593
+ retouch_paths.append((job.params["name"], r))
594
+ new_window, id_str = self.create_new_window("Project: " + project_name,
595
+ labels, retouch_paths)
596
+ worker = ProjectLogWorker(self.project, id_str)
597
+ self.connect_signals(worker, new_window)
598
+ self.start_thread(worker)
599
+ self._workers.append(worker)