shinestacker 0.4.0__py3-none-any.whl → 1.0.0__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 (59) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +101 -12
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +10 -9
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +276 -367
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/project_converter.py +6 -6
  28. shinestacker/gui/project_editor.py +248 -165
  29. shinestacker/gui/project_model.py +2 -7
  30. shinestacker/gui/tab_widget.py +81 -0
  31. shinestacker/gui/time_progress_bar.py +95 -0
  32. shinestacker/retouch/base_filter.py +173 -40
  33. shinestacker/retouch/brush_preview.py +0 -10
  34. shinestacker/retouch/brush_tool.py +25 -11
  35. shinestacker/retouch/denoise_filter.py +5 -44
  36. shinestacker/retouch/display_manager.py +57 -20
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +365 -49
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +96 -43
  43. shinestacker/retouch/io_manager.py +23 -7
  44. shinestacker/retouch/layer_collection.py +2 -0
  45. shinestacker/retouch/shortcuts_help.py +12 -0
  46. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  47. shinestacker/retouch/vignetting_filter.py +69 -0
  48. shinestacker/retouch/white_balance_filter.py +46 -14
  49. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
  50. shinestacker-1.0.0.dist-info/RECORD +90 -0
  51. shinestacker/app/app_config.py +0 -22
  52. shinestacker/gui/actions_window.py +0 -258
  53. shinestacker/retouch/image_editor.py +0 -201
  54. shinestacker/retouch/image_filters.py +0 -69
  55. shinestacker-0.4.0.dist-info/RECORD +0 -87
  56. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
  57. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
  58. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
  59. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,22 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0915, R0904, R0914, R0912, E1101, W0201
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0915, R0904, R0914
2
+ # pylint: disable=R0912, E1101, W0201, E1121, R0913, R0917
2
3
  import os
3
4
  import subprocess
4
- from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QMessageBox,
5
- QSplitter, QToolBar, QMenu, QComboBox, QStackedWidget)
6
- from PySide6.QtCore import Qt, Signal
7
- from PySide6.QtGui import QGuiApplication, QAction, QIcon, QPixmap
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QGuiApplication, QAction, QIcon
7
+ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox,
8
+ QSplitter, QToolBar, QMenu, QMainWindow)
8
9
  from .. config.constants import constants
9
- from .. core.core_utils import running_under_windows, running_under_macos, get_app_base_path
10
+ from .. core.core_utils import running_under_windows, running_under_macos
10
11
  from .colors import ColorPalette
11
12
  from .project_model import Project
12
- from .actions_window import ActionsWindow
13
13
  from .gui_logging import LogManager
14
14
  from .gui_run import RunWindow, RunWorker
15
15
  from .project_converter import ProjectConverter
16
16
  from .project_model import get_action_working_path, get_action_input_path, get_action_output_path
17
+ from .menu_manager import MenuManager
18
+ from .project_controller import ProjectController
19
+ from .tab_widget import TabWidgetWithPlaceholder
17
20
 
18
21
 
19
22
  class JobLogWorker(RunWorker):
@@ -45,105 +48,50 @@ LIST_STYLE_SHEET = f"""
45
48
  """
46
49
 
47
50
 
48
- class TabWidgetWithPlaceholder(QWidget):
49
- # Segnali aggiuntivi per mantenere la compatibilità
50
- currentChanged = Signal(int)
51
- tabCloseRequested = Signal(int)
52
-
53
- def __init__(self, parent=None):
54
- super().__init__(parent)
55
- self.layout = QVBoxLayout(self)
56
- self.layout.setContentsMargins(0, 0, 0, 0)
57
- self.stacked_widget = QStackedWidget()
58
- self.layout.addWidget(self.stacked_widget)
59
- self.tab_widget = QTabWidget()
60
- self.stacked_widget.addWidget(self.tab_widget)
61
- self.placeholder = QLabel()
62
- self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
63
- rel_path = 'ico/focus_stack_bkg.png'
64
- icon_path = f'{get_app_base_path()}/{rel_path}'
65
- if not os.path.exists(icon_path):
66
- icon_path = f'{get_app_base_path()}/../{rel_path}'
67
- if os.path.exists(icon_path):
68
- pixmap = QPixmap(icon_path)
69
- # Ridimensiona mantenendo le proporzioni (es. max 400x400)
70
- pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
71
- Qt.TransformationMode.SmoothTransformation)
72
- self.placeholder.setPixmap(pixmap)
73
- else:
74
- self.placeholder.setText("Run logs will appear here.")
75
- self.stacked_widget.addWidget(self.placeholder)
76
- self.tab_widget.currentChanged.connect(self._on_current_changed)
77
- self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
78
- self.update_placeholder_visibility()
79
-
80
- def _on_current_changed(self, index):
81
- self.currentChanged.emit(index)
82
- self.update_placeholder_visibility()
83
-
84
- def _on_tab_close_requested(self, index):
85
- self.tabCloseRequested.emit(index)
86
- self.update_placeholder_visibility()
87
-
88
- def update_placeholder_visibility(self):
89
- if self.tab_widget.count() == 0:
90
- self.stacked_widget.setCurrentIndex(1)
91
- else:
92
- self.stacked_widget.setCurrentIndex(0)
93
-
94
- # pylint: disable=C0103
95
- def addTab(self, widget, label):
96
- result = self.tab_widget.addTab(widget, label)
97
- self.update_placeholder_visibility()
98
- return result
99
-
100
- def removeTab(self, index):
101
- result = self.tab_widget.removeTab(index)
102
- self.update_placeholder_visibility()
103
- return result
104
-
105
- def count(self):
106
- return self.tab_widget.count()
107
-
108
- def setCurrentIndex(self, index):
109
- return self.tab_widget.setCurrentIndex(index)
110
-
111
- def currentIndex(self):
112
- return self.tab_widget.currentIndex()
113
-
114
- def currentWidget(self):
115
- return self.tab_widget.currentWidget()
116
-
117
- def widget(self, index):
118
- return self.tab_widget.widget(index)
119
-
120
- def indexOf(self, widget):
121
- return self.tab_widget.indexOf(widget)
122
- # pylint: enable=C0103
123
-
124
-
125
- class MainWindow(ActionsWindow, LogManager):
51
+ class MainWindow(QMainWindow, LogManager):
126
52
  def __init__(self):
127
- ActionsWindow.__init__(self)
53
+ QMainWindow.__init__(self)
128
54
  LogManager.__init__(self)
55
+ self.project_controller = ProjectController(self)
56
+ self.project_editor = self.project_controller.project_editor
57
+ actions = {
58
+ "&New...": self.project_controller.new_project,
59
+ "&Open...": self.project_controller.open_project,
60
+ "&Close": self.project_controller.close_project,
61
+ "&Save": self.project_controller.save_project,
62
+ "Save &As...": self.project_controller.save_project_as,
63
+ "&Undo": self.project_editor.undo,
64
+ "&Cut": self.project_editor.cut_element,
65
+ "Cop&y": self.project_editor.copy_element,
66
+ "&Paste": self.project_editor.paste_element,
67
+ "Duplicate": self.project_editor.clone_element,
68
+ "Delete": self.delete_element,
69
+ "Move &Up": self.project_editor.move_element_up,
70
+ "Move &Down": self.project_editor.move_element_down,
71
+ "E&nable": self.project_editor.enable,
72
+ "Di&sable": self.project_editor.disable,
73
+ "Enable All": self.project_editor.enable_all,
74
+ "Disable All": self.project_editor.disable_all,
75
+ "Expert Options": self.toggle_expert_options,
76
+ "Add Job": self.project_editor.add_job,
77
+ "Run Job": self.run_job,
78
+ "Run All Jobs": self.run_all_jobs,
79
+ }
80
+ self.menu_manager = MenuManager(self.menuBar(), actions, self.project_editor, self)
81
+ self.script_dir = os.path.dirname(__file__)
129
82
  self._windows = []
130
83
  self._workers = []
131
84
  self.retouch_callback = None
132
- self.job_list.setStyleSheet(LIST_STYLE_SHEET)
133
- self.action_list.setStyleSheet(LIST_STYLE_SHEET)
134
- menubar = self.menuBar()
135
- self.add_file_menu(menubar)
136
- self.add_edit_menu(menubar)
137
- self.add_view_menu(menubar)
138
- self.add_job_menu(menubar)
139
- self.add_actions_menu(menubar)
140
- self.add_help_menu(menubar)
85
+ self.expert_options = False
86
+ self.job_list().setStyleSheet(LIST_STYLE_SHEET)
87
+ self.action_list().setStyleSheet(LIST_STYLE_SHEET)
88
+ self.menu_manager.add_menus()
141
89
  toolbar = QToolBar(self)
142
90
  self.addToolBar(Qt.TopToolBarArea, toolbar)
143
- self.fill_toolbar(toolbar)
91
+ self.menu_manager.fill_toolbar(toolbar)
144
92
  self.resize(1200, 800)
145
- center = QGuiApplication.primaryScreen().geometry().center()
146
- self.move(center - self.rect().center())
93
+ self.move(QGuiApplication.primaryScreen().geometry().center() -
94
+ self.rect().center())
147
95
  self.set_project(Project())
148
96
  self.central_widget = QWidget()
149
97
  self.setCentralWidget(self.central_widget)
@@ -158,243 +106,181 @@ class MainWindow(ActionsWindow, LogManager):
158
106
  self.tab_widget = TabWidgetWithPlaceholder()
159
107
  self.tab_widget.resize(1000, 500)
160
108
  h_splitter.addWidget(self.tab_widget)
161
- self.job_list.currentRowChanged.connect(self.on_job_selected)
162
- self.job_list.itemDoubleClicked.connect(self.on_job_edit)
163
- self.action_list.itemDoubleClicked.connect(self.on_action_edit)
109
+ self.job_list().currentRowChanged.connect(self.project_editor.on_job_selected)
110
+ self.job_list().itemDoubleClicked.connect(self.on_job_edit)
111
+ self.action_list().itemDoubleClicked.connect(self.on_action_edit)
164
112
  vbox_left = QVBoxLayout()
165
113
  vbox_left.setSpacing(4)
166
114
  vbox_left.addWidget(QLabel("Job"))
167
- vbox_left.addWidget(self.job_list)
115
+ vbox_left.addWidget(self.job_list())
168
116
  vbox_right = QVBoxLayout()
169
117
  vbox_right.setSpacing(4)
170
118
  vbox_right.addWidget(QLabel("Action"))
171
- vbox_right.addWidget(self.action_list)
172
- self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
173
- self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
119
+ vbox_right.addWidget(self.action_list())
120
+ self.job_list().itemSelectionChanged.connect(self.update_delete_action_state)
121
+ self.action_list().itemSelectionChanged.connect(self.update_delete_action_state)
174
122
  h_layout.addLayout(vbox_left)
175
123
  h_layout.addLayout(vbox_right)
176
124
  layout.addWidget(h_splitter)
177
125
  self.central_widget.setLayout(layout)
126
+ self.update_title()
127
+
128
+ def handle_modified(modified):
129
+ self.save_actions_set_enabled(modified)
130
+ self.update_title()
131
+
132
+ self.project_editor.modified_signal.connect(handle_modified)
133
+ self.project_editor.select_signal.connect(self.update_delete_action_state)
134
+ self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
135
+ self.project_editor.enable_delete_action_signal.connect(
136
+ self.menu_manager.delete_element_action.setEnabled)
137
+ self.project_controller.update_title_requested.connect(self.update_title)
138
+ self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
139
+ self.project_controller.activate_window_requested.connect(self.activateWindow)
140
+ self.project_controller.enable_save_actions_requested.connect(
141
+ self.menu_manager.save_actions_set_enabled)
142
+
143
+ def modified(self):
144
+ return self.project_editor.modified()
145
+
146
+ def mark_as_modified(self, modified=True):
147
+ self.project_editor.mark_as_modified(modified)
148
+
149
+ def set_project(self, project):
150
+ self.project_editor.set_project(project)
151
+
152
+ def project(self):
153
+ return self.project_editor.project()
154
+
155
+ def project_jobs(self):
156
+ return self.project_editor.project_jobs()
157
+
158
+ def project_job(self, i):
159
+ return self.project_editor.project_job(i)
160
+
161
+ def add_job_to_project(self, job):
162
+ self.project_editor.add_job_to_project(job)
163
+
164
+ def num_project_jobs(self):
165
+ return self.project_editor.num_project_jobs()
166
+
167
+ def current_file_path(self):
168
+ return self.project_editor.current_file_path()
169
+
170
+ def current_file_directory(self):
171
+ return self.project_editor.current_file_directory()
172
+
173
+ def current_file_name(self):
174
+ return self.project_editor.current_file_name()
175
+
176
+ def set_current_file_path(self, path):
177
+ self.project_editor.set_current_file_path(path)
178
+
179
+ def job_list(self):
180
+ return self.project_editor.job_list()
181
+
182
+ def action_list(self):
183
+ return self.project_editor.action_list()
184
+
185
+ def current_job_index(self):
186
+ return self.project_editor.current_job_index()
187
+
188
+ def current_action_index(self):
189
+ return self.project_editor.current_action_index()
190
+
191
+ def set_current_job(self, index):
192
+ return self.project_editor.set_current_job(index)
193
+
194
+ def set_current_action(self, index):
195
+ return self.project_editor.set_current_action(index)
196
+
197
+ def job_list_count(self):
198
+ return self.project_editor.job_list_count()
199
+
200
+ def action_list_count(self):
201
+ return self.project_editor.action_list_count()
202
+
203
+ def job_list_item(self, index):
204
+ return self.project_editor.job_list_item(index)
205
+
206
+ def action_list_item(self, index):
207
+ return self.project_editor.action_list_item(index)
208
+
209
+ def job_list_has_focus(self):
210
+ return self.project_editor.job_list_has_focus()
211
+
212
+ def action_list_has_focus(self):
213
+ return self.project_editor.action_list_has_focus()
214
+
215
+ def clear_job_list(self):
216
+ self.project_editor.clear_job_list()
217
+
218
+ def clear_action_list(self):
219
+ self.project_editor.clear_action_list()
220
+
221
+ def num_selected_jobs(self):
222
+ return self.project_editor.num_selected_jobs()
223
+
224
+ def num_selected_actions(self):
225
+ return self.project_editor.num_selected_actions()
226
+
227
+ def get_current_action_at(self, job, action_index):
228
+ return self.project_editor.get_current_action_at(job, action_index)
229
+
230
+ def action_config_dialog(self, action):
231
+ return self.project_editor.action_config_dialog(action)
232
+
233
+ def action_text(self, action, is_sub_action=False, indent=True, long_name=False, html=False):
234
+ return self.project_editor.action_text(action, is_sub_action, indent, long_name, html)
235
+
236
+ def job_text(self, job, long_name=False, html=False):
237
+ return self.project_editor.job_text(job, long_name, html)
238
+
239
+ def on_job_selected(self, index):
240
+ return self.project_editor.on_job_selected(index)
241
+
242
+ def get_action_at(self, action_row):
243
+ return self.project_editor.get_action_at(action_row)
244
+
245
+ def on_job_edit(self, item):
246
+ self.project_controller.on_job_edit(item)
247
+
248
+ def on_action_edit(self, item):
249
+ self.project_controller.on_action_edit(item)
250
+
251
+ def edit_current_action(self):
252
+ self.project_controller.edit_current_action()
253
+
254
+ def edit_action(self, action):
255
+ self.project_controller.edit_action(action)
178
256
 
179
257
  def set_retouch_callback(self, callback):
180
258
  self.retouch_callback = callback
181
259
 
182
- def add_file_menu(self, menubar):
183
- menu = menubar.addMenu("&File")
184
- new_action = QAction("&New...", self)
185
- new_action.setShortcut("Ctrl+N")
186
- new_action.triggered.connect(self.new_project)
187
- menu.addAction(new_action)
188
- open_action = QAction("&Open...", self)
189
- open_action.setShortcut("Ctrl+O")
190
- open_action.triggered.connect(self.open_project)
191
- menu.addAction(open_action)
192
- save_action = QAction("&Save", self)
193
- save_action.setShortcut("Ctrl+S")
194
- save_action.triggered.connect(self.save_project)
195
- menu.addAction(save_action)
196
- save_as_action = QAction("Save &As...", self)
197
- save_as_action.setShortcut("Ctrl+Shift+S")
198
- save_as_action.triggered.connect(self.save_project_as)
199
- menu.addAction(save_as_action)
200
- close_action = QAction("&Close", self)
201
- close_action.setShortcut("Ctrl+W")
202
- close_action.triggered.connect(self.close_project)
203
- menu.addAction(close_action)
204
-
205
- def add_edit_menu(self, menubar):
206
- menu = menubar.addMenu("&Edit")
207
- undo_action = QAction("&Undo", self)
208
- undo_action.setShortcut("Ctrl+Z")
209
- undo_action.triggered.connect(self.undo)
210
- menu.addAction(undo_action)
211
- menu.addSeparator()
212
- cut_action = QAction("&Cut", self)
213
- cut_action.setShortcut("Ctrl+X")
214
- cut_action.triggered.connect(self.cut_element)
215
- menu.addAction(cut_action)
216
- copy_action = QAction("Cop&y", self)
217
- copy_action.setShortcut("Ctrl+C")
218
- copy_action.triggered.connect(self.copy_element)
219
- menu.addAction(copy_action)
220
- paste_action = QAction("&Paste", self)
221
- paste_action.setShortcut("Ctrl+V")
222
- paste_action.triggered.connect(self.paste_element)
223
- menu.addAction(paste_action)
224
- clone_action = QAction("Duplicate", self)
225
- clone_action.setShortcut("Ctrl+D")
226
- clone_action.triggered.connect(self.clone_element)
227
- menu.addAction(clone_action)
228
- self.delete_element_action = QAction("Delete", self)
229
- self.delete_element_action.setShortcut("Del") # Qt.Key_Backspace
230
- self.delete_element_action.setIcon(self.get_icon("close-round-line-icon"))
231
- self.delete_element_action.setToolTip("delete")
232
- self.delete_element_action.triggered.connect(self.delete_element)
233
- self.delete_element_action.setEnabled(False)
234
- menu.addAction(self.delete_element_action)
235
- menu.addSeparator()
236
- up_action = QAction("Move &Up", self)
237
- up_action.setShortcut("Ctrl+Up")
238
- up_action.triggered.connect(self.move_element_up)
239
- menu.addAction(up_action)
240
- down_action = QAction("Move &Down", self)
241
- down_action.setShortcut("Ctrl+Down")
242
- down_action.triggered.connect(self.move_element_down)
243
- menu.addAction(down_action)
244
- menu.addSeparator()
245
- self.enable_action = QAction("E&nable", self)
246
- self.enable_action.setShortcut("Ctrl+E")
247
- self.enable_action.triggered.connect(self.enable)
248
- menu.addAction(self.enable_action)
249
- self.disable_action = QAction("Di&sable", self)
250
- self.disable_action.setShortcut("Ctrl+B")
251
- self.disable_action.triggered.connect(self.disable)
252
- menu.addAction(self.disable_action)
253
- enable_all_action = QAction("Enable All", self)
254
- enable_all_action.setShortcut("Ctrl+Shift+E")
255
- enable_all_action.triggered.connect(self.enable_all)
256
- menu.addAction(enable_all_action)
257
- disable_all_action = QAction("Disable All", self)
258
- disable_all_action.setShortcut("Ctrl+Shift+B")
259
- disable_all_action.triggered.connect(self.disable_all)
260
- menu.addAction(disable_all_action)
261
-
262
- def add_view_menu(self, menubar):
263
- menu = menubar.addMenu("&View")
264
- self.expert_options_action = QAction("Expert Options", self)
265
- self.expert_options_action.setShortcut("Ctrl+Shift+X")
266
- self.expert_options_action.triggered.connect(self.toggle_expert_options)
267
- self.expert_options_action.setCheckable(True)
268
- self.expert_options_action.setChecked(self.expert_options)
269
- menu.addAction(self.expert_options_action)
270
-
271
- def add_job_menu(self, menubar):
272
- menu = menubar.addMenu("&Jobs")
273
- self.add_job_action = QAction("Add Job", self)
274
- self.add_job_action.setShortcut("Ctrl+P")
275
- self.add_job_action.setIcon(self.get_icon("plus-round-line-icon"))
276
- self.add_job_action.setToolTip("Add job")
277
- self.add_job_action.triggered.connect(self.add_job)
278
- menu.addAction(self.add_job_action)
279
- menu.addSeparator()
280
- self.run_job_action = QAction("Run Job", self)
281
- self.run_job_action.setShortcut("Ctrl+J")
282
- self.run_job_action.setIcon(self.get_icon("play-button-round-icon"))
283
- self.run_job_action.setToolTip("Run job")
284
- self.run_job_action.setEnabled(False)
285
- self.run_job_action.triggered.connect(self.run_job)
286
- menu.addAction(self.run_job_action)
287
- self.run_all_jobs_action = QAction("Run All Jobs", self)
288
- self.run_all_jobs_action.setShortcut("Ctrl+Shift+J")
289
- self.run_all_jobs_action.setIcon(self.get_icon("forward-button-icon"))
290
- self.run_all_jobs_action.setToolTip("Run all jobs")
291
- self.run_all_jobs_action.setEnabled(False)
292
- self.run_all_jobs_action.triggered.connect(self.run_all_jobs)
293
- menu.addAction(self.run_all_jobs_action)
294
-
295
- def add_action_combined_actions(self):
296
- self.add_action(constants.ACTION_COMBO)
297
-
298
- def add_action_noise_detection(self):
299
- self.add_action(constants.ACTION_NOISEDETECTION)
300
-
301
- def add_action_focus_stack(self):
302
- self.add_action(constants.ACTION_FOCUSSTACK)
303
-
304
- def add_action_focus_stack_bunch(self):
305
- self.add_action(constants.ACTION_FOCUSSTACKBUNCH)
306
-
307
- def add_action_multilayer(self):
308
- self.add_action(constants.ACTION_MULTILAYER)
309
-
310
- def add_sub_action_make_noise(self):
311
- self.add_sub_action(constants.ACTION_MASKNOISE)
312
-
313
- def add_sub_action_vignetting(self):
314
- self.add_sub_action(constants.ACTION_VIGNETTING)
315
-
316
- def add_sub_action_align_frames(self):
317
- self.add_sub_action(constants.ACTION_ALIGNFRAMES)
318
-
319
- def add_sub_action_balance_frames(self):
320
- self.add_sub_action(constants.ACTION_BALANCEFRAMES)
321
-
322
- def add_actions_menu(self, menubar):
323
- menu = menubar.addMenu("&Actions")
324
- add_action_menu = QMenu("Add Action", self)
325
- for action in constants.ACTION_TYPES:
326
- entry_action = QAction(action, self)
327
- entry_action.triggered.connect({
328
- constants.ACTION_COMBO: self.add_action_combined_actions,
329
- constants.ACTION_NOISEDETECTION: self.add_action_noise_detection,
330
- constants.ACTION_FOCUSSTACK: self.add_action_focus_stack,
331
- constants.ACTION_FOCUSSTACKBUNCH: self.add_action_focus_stack_bunch,
332
- constants.ACTION_MULTILAYER: self.add_action_multilayer
333
- }[action])
334
- add_action_menu.addAction(entry_action)
335
- menu.addMenu(add_action_menu)
336
- add_sub_action_menu = QMenu("Add Sub Action", self)
337
- self.sub_action_menu_entries = []
338
- for action in constants.SUB_ACTION_TYPES:
339
- entry_action = QAction(action, self)
340
- entry_action.triggered.connect({
341
- constants.ACTION_MASKNOISE: self.add_sub_action_make_noise,
342
- constants.ACTION_VIGNETTING: self.add_sub_action_vignetting,
343
- constants.ACTION_ALIGNFRAMES: self.add_sub_action_align_frames,
344
- constants.ACTION_BALANCEFRAMES: self.add_sub_action_balance_frames
345
- }[action])
346
- entry_action.setEnabled(False)
347
- self.sub_action_menu_entries.append(entry_action)
348
- add_sub_action_menu.addAction(entry_action)
349
- menu.addMenu(add_sub_action_menu)
350
-
351
- def add_help_menu(self, menubar):
352
- menu = menubar.addMenu("&Help")
353
- menu.setObjectName("Help")
354
-
355
- def fill_toolbar(self, toolbar):
356
- toolbar.addAction(self.add_job_action)
357
- toolbar.addSeparator()
358
- self.action_selector = QComboBox()
359
- self.action_selector.addItems(constants.ACTION_TYPES)
360
- self.action_selector.setEnabled(False)
361
- toolbar.addWidget(self.action_selector)
362
- self.add_action_entry_action = QAction("Add Action", self)
363
- self.add_action_entry_action.setIcon(
364
- QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
365
- self.add_action_entry_action.setToolTip("Add action")
366
- self.add_action_entry_action.triggered.connect(self.add_action)
367
- self.add_action_entry_action.setEnabled(False)
368
- toolbar.addAction(self.add_action_entry_action)
369
- self.sub_action_selector = QComboBox()
370
- self.sub_action_selector.addItems(constants.SUB_ACTION_TYPES)
371
- self.sub_action_selector.setEnabled(False)
372
- toolbar.addWidget(self.sub_action_selector)
373
- self.add_sub_action_entry_action = QAction("Add Sub Action", self)
374
- self.add_sub_action_entry_action.setIcon(
375
- QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
376
- self.add_sub_action_entry_action.setToolTip("Add sub action")
377
- self.add_sub_action_entry_action.triggered.connect(self.add_sub_action)
378
- self.add_sub_action_entry_action.setEnabled(False)
379
- toolbar.addAction(self.add_sub_action_entry_action)
380
- toolbar.addSeparator()
381
- toolbar.addAction(self.delete_element_action)
382
- toolbar.addSeparator()
383
- toolbar.addAction(self.run_job_action)
384
- toolbar.addAction(self.run_all_jobs_action)
260
+ def save_actions_set_enabled(self, enabled):
261
+ self.menu_manager.save_actions_set_enabled(enabled)
262
+
263
+ def update_title(self):
264
+ title = constants.APP_TITLE
265
+ file_name = self.current_file_name()
266
+ if file_name:
267
+ title += f" - {file_name}"
268
+ if self.modified():
269
+ title += " *"
270
+ self.window().setWindowTitle(title)
385
271
 
386
272
  # pylint: disable=C0103
387
273
  def contextMenuEvent(self, event):
388
- item = self.job_list.itemAt(self.job_list.viewport().mapFrom(self, event.pos()))
274
+ item = self.job_list().itemAt(self.job_list().viewport().mapFrom(self, event.pos()))
389
275
  current_action = None
390
276
  if item:
391
- index = self.job_list.row(item)
277
+ index = self.job_list().row(item)
392
278
  current_action = self.get_job_at(index)
393
- self.job_list.setCurrentRow(index)
394
- item = self.action_list.itemAt(self.action_list.viewport().mapFrom(self, event.pos()))
279
+ self.set_current_job(index)
280
+ item = self.action_list().itemAt(self.action_list().viewport().mapFrom(self, event.pos()))
395
281
  if item:
396
- index = self.action_list.row(item)
397
- self.action_list.setCurrentRow(index)
282
+ index = self.action_list().row(item)
283
+ self.set_current_action(index)
398
284
  _job_row, _action_row, pos = self.get_action_at(index)
399
285
  current_action = pos.action if not pos.is_sub_action else pos.sub_action
400
286
  if current_action:
@@ -412,7 +298,7 @@ class MainWindow(ActionsWindow, LogManager):
412
298
  os.path.exists(self.current_action_working_path):
413
299
  action_name = "Browse Working Path" + (f" > {name}" if name != '' else '')
414
300
  self.browse_working_path_action = QAction(action_name)
415
- self.browse_working_path_action.triggered.connect(self.browse_working_path_path)
301
+ self.browse_working_path_action.triggered.connect(self.browse_working_path)
416
302
  menu.addAction(self.browse_working_path_action)
417
303
  ip, name = get_action_input_path(current_action)
418
304
  if ip != '':
@@ -426,16 +312,24 @@ class MainWindow(ActionsWindow, LogManager):
426
312
  break
427
313
  if p_exists:
428
314
  action_name = "Browse Input Path" + (f" > {name}" if name != '' else '')
315
+ n_files = [f"{len(next(os.walk(p))[2])}"
316
+ for p in
317
+ self.current_action_input_path.split(constants.PATH_SEPARATOR)]
318
+ s = "" if len(n_files) == 1 and n_files[0] == 1 else "s"
319
+ action_name += " (" + ", ".join(n_files) + f" file{s})"
429
320
  self.browse_input_path_action = QAction(action_name)
430
- self.browse_input_path_action.triggered.connect(self.browse_input_path_path)
321
+ self.browse_input_path_action.triggered.connect(self.browse_input_path)
431
322
  menu.addAction(self.browse_input_path_action)
432
323
  op, name = get_action_output_path(current_action)
433
324
  if op != '':
434
325
  self.current_action_output_path = f"{self.current_action_working_path}/{op}"
435
326
  if os.path.exists(self.current_action_output_path):
436
327
  action_name = "Browse Output Path" + (f" > {name}" if name != '' else '')
328
+ n_files = len(next(os.walk(op))[2])
329
+ s = "" if n_files == 1 else "s"
330
+ action_name += f" ({n_files} file{s})"
437
331
  self.browse_output_path_action = QAction(action_name)
438
- self.browse_output_path_action.triggered.connect(self.browse_output_path_path)
332
+ self.browse_output_path_action.triggered.connect(self.browse_output_path)
439
333
  menu.addAction(self.browse_output_path_action)
440
334
  menu.addSeparator()
441
335
  menu.addAction(self.run_job_action)
@@ -483,58 +377,57 @@ class MainWindow(ActionsWindow, LogManager):
483
377
  if os.path.exists(p):
484
378
  if running_under_windows():
485
379
  os.startfile(os.path.normpath(p))
486
- elif running_under_macos():
487
- subprocess.run(['open', p], check=True)
488
380
  else:
489
- subprocess.run(['xdg-open', p], check=True)
381
+ cmd = 'open' if running_under_macos() else 'xdg-open'
382
+ subprocess.run([cmd, p], check=True)
490
383
 
491
- def browse_working_path_path(self):
384
+ def browse_working_path(self):
492
385
  self.browse_path(self.current_action_working_path)
493
386
 
494
- def browse_input_path_path(self):
387
+ def browse_input_path(self):
495
388
  self.browse_path(self.current_action_input_path)
496
389
 
497
- def browse_output_path_path(self):
390
+ def browse_output_path(self):
498
391
  self.browse_path(self.current_action_output_path)
499
392
 
500
393
  def refresh_ui(self, job_row=-1, action_row=-1):
501
- self.job_list.clear()
502
- for job in self.project.jobs:
503
- self.add_list_item(self.job_list, job, False)
504
- if self.project.jobs:
505
- self.job_list.setCurrentRow(0)
394
+ self.clear_job_list()
395
+ for job in self.project_jobs():
396
+ self.project_editor.add_list_item(self.job_list(), job, False)
397
+ if self.project_jobs():
398
+ self.set_current_job(0)
506
399
  if job_row >= 0:
507
- self.job_list.setCurrentRow(job_row)
400
+ self.set_current_job(job_row)
508
401
  if action_row >= 0:
509
- self.action_list.setCurrentRow(action_row)
510
- if self.job_list.count() == 0:
511
- self.add_action_entry_action.setEnabled(False)
512
- self.action_selector.setEnabled(False)
402
+ self.set_current_action(action_row)
403
+ if self.job_list_count() == 0:
404
+ self.menu_manager.add_action_entry_action.setEnabled(False)
405
+ self.menu_manager.action_selector.setEnabled(False)
513
406
  self.run_job_action.setEnabled(False)
514
407
  self.run_all_jobs_action.setEnabled(False)
515
408
  else:
516
- self.add_action_entry_action.setEnabled(True)
517
- self.action_selector.setEnabled(True)
518
- self.delete_element_action.setEnabled(True)
519
- self.run_job_action.setEnabled(True)
520
- self.run_all_jobs_action.setEnabled(True)
409
+ self.menu_manager.add_action_entry_action.setEnabled(True)
410
+ self.menu_manager.action_selector.setEnabled(True)
411
+ self.menu_manager.delete_element_action.setEnabled(True)
412
+ self.menu_manager.run_job_action.setEnabled(True)
413
+ self.menu_manager.run_all_jobs_action.setEnabled(True)
521
414
 
522
415
  def quit(self):
523
- if self._check_unsaved_changes():
416
+ if self.project_controller.check_unsaved_changes():
524
417
  for worker in self._workers:
525
418
  worker.stop()
526
419
  self.close()
527
420
 
528
421
  def toggle_expert_options(self):
529
- self.expert_options = self.expert_options_action.isChecked()
422
+ self.expert_options = self.menu_manager.expert_options_action.isChecked()
530
423
 
531
424
  def set_expert_options(self):
532
425
  self.expert_options_action.setChecked(True)
533
426
  self.expert_options = True
534
427
 
535
428
  def before_thread_begins(self):
536
- self.run_job_action.setEnabled(False)
537
- self.run_all_jobs_action.setEnabled(False)
429
+ self.menu_manager.run_job_action.setEnabled(False)
430
+ self.menu_manager.run_all_jobs_action.setEnabled(False)
538
431
 
539
432
  def get_tab_and_position(self, id_str):
540
433
  for i in range(self.tab_widget.count()):
@@ -552,8 +445,8 @@ class MainWindow(ActionsWindow, LogManager):
552
445
  return i
553
446
 
554
447
  def do_handle_end_message(self, status, id_str, message):
555
- self.run_job_action.setEnabled(True)
556
- self.run_all_jobs_action.setEnabled(True)
448
+ self.menu_manager.run_job_action.setEnabled(True)
449
+ self.menu_manager.run_all_jobs_action.setEnabled(True)
557
450
  tab = self.get_tab_at_position(id_str)
558
451
  tab.close_button.setEnabled(True)
559
452
  tab.stop_button.setEnabled(False)
@@ -564,8 +457,7 @@ class MainWindow(ActionsWindow, LogManager):
564
457
  new_window = RunWindow(labels,
565
458
  lambda id_str: self.stop_worker(self.get_tab_position(id_str)),
566
459
  lambda id_str: self.close_window(self.get_tab_position(id_str)),
567
- retouch_paths,
568
- self)
460
+ retouch_paths, self)
569
461
  self.tab_widget.addTab(new_window, title)
570
462
  self.tab_widget.setCurrentIndex(self.tab_widget.count() - 1)
571
463
  if title is not None:
@@ -584,7 +476,7 @@ class MainWindow(ActionsWindow, LogManager):
584
476
  worker = self._workers[tab_position]
585
477
  worker.stop()
586
478
 
587
- def connect_signals(self, worker, window):
479
+ def connect_worker_signals(self, worker, window):
588
480
  worker.before_action_signal.connect(window.handle_before_action)
589
481
  worker.after_action_signal.connect(window.handle_after_action)
590
482
  worker.step_counts_signal.connect(window.handle_step_counts)
@@ -593,23 +485,18 @@ class MainWindow(ActionsWindow, LogManager):
593
485
  worker.after_step_signal.connect(window.handle_after_step)
594
486
  worker.save_plot_signal.connect(window.handle_save_plot)
595
487
  worker.open_app_signal.connect(window.handle_open_app)
596
-
597
- def set_enabled_sub_actions_gui(self, enabled):
598
- self.add_sub_action_entry_action.setEnabled(enabled)
599
- self.sub_action_selector.setEnabled(enabled)
600
- for a in self.sub_action_menu_entries:
601
- a.setEnabled(enabled)
488
+ worker.run_completed_signal.connect(window.handle_run_completed)
489
+ worker.run_stopped_signal.connect(window.handle_run_stopped)
490
+ worker.run_failed_signal.connect(window.handle_run_failed)
602
491
 
603
492
  def run_job(self):
604
- current_index = self.job_list.currentRow()
493
+ current_index = self.current_job_index()
605
494
  if current_index < 0:
606
- if len(self.project.jobs) > 0:
607
- QMessageBox.warning(self, "No Job Selected", "Please select a job first.")
608
- else:
609
- QMessageBox.warning(self, "No Job Added", "Please add a job first.")
495
+ msg = "No Job Selected" if self.num_project_jobs() > 0 else "No Job Added"
496
+ QMessageBox.warning(self, msg, "Please select a job first.")
610
497
  return
611
498
  if current_index >= 0:
612
- job = self.project.jobs[current_index]
499
+ job = self.project_job(current_index)
613
500
  if job.enabled():
614
501
  job_name = job.params["name"]
615
502
  labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
@@ -618,29 +505,51 @@ class MainWindow(ActionsWindow, LogManager):
618
505
  new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
619
506
  labels, retouch_paths)
620
507
  worker = JobLogWorker(job, id_str)
621
- self.connect_signals(worker, new_window)
508
+ self.connect_worker_signals(worker, new_window)
622
509
  self.start_thread(worker)
623
510
  self._workers.append(worker)
624
511
  else:
625
- QMessageBox.warning(
626
- self, "Can't run Job",
627
- "Job " + job.params["name"] + " is disabled.")
512
+ QMessageBox.warning(self, "Can't run Job",
513
+ "Job " + job.params["name"] + " is disabled.")
628
514
  return
629
515
 
630
516
  def run_all_jobs(self):
631
517
  labels = [[(self.action_text(a), a.enabled() and
632
- job.enabled()) for a in job.sub_actions] for job in self.project.jobs]
518
+ job.enabled()) for a in job.sub_actions] for job in self.project_jobs()]
633
519
  project_name = ".".join(self.current_file_name().split(".")[:-1])
634
520
  if project_name == '':
635
521
  project_name = '[new]'
636
522
  retouch_paths = []
637
- for job in self.project.jobs:
523
+ for job in self.project_jobs():
638
524
  r = self.get_retouch_path(job)
639
525
  if len(r) > 0:
640
526
  retouch_paths.append((job.params["name"], r))
641
527
  new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
642
528
  labels, retouch_paths)
643
- worker = ProjectLogWorker(self.project, id_str)
644
- self.connect_signals(worker, new_window)
529
+ worker = ProjectLogWorker(self.project(), id_str)
530
+ self.connect_worker_signals(worker, new_window)
645
531
  self.start_thread(worker)
646
532
  self._workers.append(worker)
533
+
534
+ def delete_element(self):
535
+ self.project_editor.delete_element()
536
+ if self.job_list_count() > 0:
537
+ self.menu_manager.delete_element_action.setEnabled(True)
538
+
539
+ def update_delete_action_state(self):
540
+ has_job_selected = self.num_selected_jobs() > 0
541
+ has_action_selected = self.num_selected_actions() > 0
542
+ self.menu_manager.delete_element_action.setEnabled(
543
+ has_job_selected or has_action_selected)
544
+ if has_action_selected and has_job_selected:
545
+ job_index = min(self.current_job_index(), self.num_project_jobs() - 1)
546
+ action_index = self.current_action_index()
547
+ if job_index >= 0:
548
+ job = self.project_job(job_index)
549
+ current_action, is_sub_action = \
550
+ self.get_current_action_at(job, action_index)
551
+ enable_sub_actions = current_action is not None and \
552
+ not is_sub_action and current_action.type_name == constants.ACTION_COMBO
553
+ self.menu_manager.set_enabled_sub_actions_gui(enable_sub_actions)
554
+ else:
555
+ self.menu_manager.set_enabled_sub_actions_gui(False)