shinestacker 0.5.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 (57) 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 +37 -16
  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 +3 -4
  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 +275 -371
  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 +2 -5
  35. shinestacker/retouch/denoise_filter.py +5 -44
  36. shinestacker/retouch/exif_data.py +10 -13
  37. shinestacker/retouch/file_loader.py +1 -1
  38. shinestacker/retouch/filter_manager.py +1 -4
  39. shinestacker/retouch/image_editor_ui.py +318 -40
  40. shinestacker/retouch/image_viewer.py +34 -11
  41. shinestacker/retouch/io_gui_handler.py +34 -30
  42. shinestacker/retouch/layer_collection.py +2 -0
  43. shinestacker/retouch/shortcuts_help.py +12 -0
  44. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  45. shinestacker/retouch/vignetting_filter.py +69 -0
  46. shinestacker/retouch/white_balance_filter.py +46 -14
  47. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
  48. shinestacker-1.0.0.dist-info/RECORD +90 -0
  49. shinestacker/app/app_config.py +0 -22
  50. shinestacker/gui/actions_window.py +0 -266
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
  57. {shinestacker-0.5.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,248 +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
- self.save_action = QAction("&Save", self)
193
- self.save_action.setShortcut("Ctrl+S")
194
- self.save_action.triggered.connect(self.save_project)
195
- menu.addAction(self.save_action)
196
- self.save_as_action = QAction("Save &As...", self)
197
- self.save_as_action.setShortcut("Ctrl+Shift+S")
198
- self.save_as_action.triggered.connect(self.save_project_as)
199
- menu.addAction(self.save_as_action)
200
- self.save_actions_set_enabled(False)
201
- close_action = QAction("&Close", self)
202
- close_action.setShortcut("Ctrl+W")
203
- close_action.triggered.connect(self.close_project)
204
- menu.addAction(close_action)
205
-
206
260
  def save_actions_set_enabled(self, enabled):
207
- self.save_action.setEnabled(enabled)
208
- self.save_as_action.setEnabled(enabled)
209
-
210
- def add_edit_menu(self, menubar):
211
- menu = menubar.addMenu("&Edit")
212
- undo_action = QAction("&Undo", self)
213
- undo_action.setShortcut("Ctrl+Z")
214
- undo_action.triggered.connect(self.undo)
215
- menu.addAction(undo_action)
216
- menu.addSeparator()
217
- cut_action = QAction("&Cut", self)
218
- cut_action.setShortcut("Ctrl+X")
219
- cut_action.triggered.connect(self.cut_element)
220
- menu.addAction(cut_action)
221
- copy_action = QAction("Cop&y", self)
222
- copy_action.setShortcut("Ctrl+C")
223
- copy_action.triggered.connect(self.copy_element)
224
- menu.addAction(copy_action)
225
- paste_action = QAction("&Paste", self)
226
- paste_action.setShortcut("Ctrl+V")
227
- paste_action.triggered.connect(self.paste_element)
228
- menu.addAction(paste_action)
229
- clone_action = QAction("Duplicate", self)
230
- clone_action.setShortcut("Ctrl+D")
231
- clone_action.triggered.connect(self.clone_element)
232
- menu.addAction(clone_action)
233
- self.delete_element_action = QAction("Delete", self)
234
- self.delete_element_action.setShortcut("Del") # Qt.Key_Backspace
235
- self.delete_element_action.setIcon(self.get_icon("close-round-line-icon"))
236
- self.delete_element_action.setToolTip("delete")
237
- self.delete_element_action.triggered.connect(self.delete_element)
238
- self.delete_element_action.setEnabled(False)
239
- menu.addAction(self.delete_element_action)
240
- menu.addSeparator()
241
- up_action = QAction("Move &Up", self)
242
- up_action.setShortcut("Ctrl+Up")
243
- up_action.triggered.connect(self.move_element_up)
244
- menu.addAction(up_action)
245
- down_action = QAction("Move &Down", self)
246
- down_action.setShortcut("Ctrl+Down")
247
- down_action.triggered.connect(self.move_element_down)
248
- menu.addAction(down_action)
249
- menu.addSeparator()
250
- self.enable_action = QAction("E&nable", self)
251
- self.enable_action.setShortcut("Ctrl+E")
252
- self.enable_action.triggered.connect(self.enable)
253
- menu.addAction(self.enable_action)
254
- self.disable_action = QAction("Di&sable", self)
255
- self.disable_action.setShortcut("Ctrl+B")
256
- self.disable_action.triggered.connect(self.disable)
257
- menu.addAction(self.disable_action)
258
- enable_all_action = QAction("Enable All", self)
259
- enable_all_action.setShortcut("Ctrl+Shift+E")
260
- enable_all_action.triggered.connect(self.enable_all)
261
- menu.addAction(enable_all_action)
262
- disable_all_action = QAction("Disable All", self)
263
- disable_all_action.setShortcut("Ctrl+Shift+B")
264
- disable_all_action.triggered.connect(self.disable_all)
265
- menu.addAction(disable_all_action)
266
-
267
- def add_view_menu(self, menubar):
268
- menu = menubar.addMenu("&View")
269
- self.expert_options_action = QAction("Expert Options", self)
270
- self.expert_options_action.setShortcut("Ctrl+Shift+X")
271
- self.expert_options_action.triggered.connect(self.toggle_expert_options)
272
- self.expert_options_action.setCheckable(True)
273
- self.expert_options_action.setChecked(self.expert_options)
274
- menu.addAction(self.expert_options_action)
275
-
276
- def add_job_menu(self, menubar):
277
- menu = menubar.addMenu("&Jobs")
278
- self.add_job_action = QAction("Add Job", self)
279
- self.add_job_action.setShortcut("Ctrl+P")
280
- self.add_job_action.setIcon(self.get_icon("plus-round-line-icon"))
281
- self.add_job_action.setToolTip("Add job")
282
- self.add_job_action.triggered.connect(self.add_job)
283
- menu.addAction(self.add_job_action)
284
- menu.addSeparator()
285
- self.run_job_action = QAction("Run Job", self)
286
- self.run_job_action.setShortcut("Ctrl+J")
287
- self.run_job_action.setIcon(self.get_icon("play-button-round-icon"))
288
- self.run_job_action.setToolTip("Run job")
289
- self.run_job_action.setEnabled(False)
290
- self.run_job_action.triggered.connect(self.run_job)
291
- menu.addAction(self.run_job_action)
292
- self.run_all_jobs_action = QAction("Run All Jobs", self)
293
- self.run_all_jobs_action.setShortcut("Ctrl+Shift+J")
294
- self.run_all_jobs_action.setIcon(self.get_icon("forward-button-icon"))
295
- self.run_all_jobs_action.setToolTip("Run all jobs")
296
- self.run_all_jobs_action.setEnabled(False)
297
- self.run_all_jobs_action.triggered.connect(self.run_all_jobs)
298
- menu.addAction(self.run_all_jobs_action)
299
-
300
- def add_action_combined_actions(self):
301
- self.add_action(constants.ACTION_COMBO)
302
-
303
- def add_action_noise_detection(self):
304
- self.add_action(constants.ACTION_NOISEDETECTION)
305
-
306
- def add_action_focus_stack(self):
307
- self.add_action(constants.ACTION_FOCUSSTACK)
308
-
309
- def add_action_focus_stack_bunch(self):
310
- self.add_action(constants.ACTION_FOCUSSTACKBUNCH)
311
-
312
- def add_action_multilayer(self):
313
- self.add_action(constants.ACTION_MULTILAYER)
314
-
315
- def add_sub_action_make_noise(self):
316
- self.add_sub_action(constants.ACTION_MASKNOISE)
317
-
318
- def add_sub_action_vignetting(self):
319
- self.add_sub_action(constants.ACTION_VIGNETTING)
320
-
321
- def add_sub_action_align_frames(self):
322
- self.add_sub_action(constants.ACTION_ALIGNFRAMES)
323
-
324
- def add_sub_action_balance_frames(self):
325
- self.add_sub_action(constants.ACTION_BALANCEFRAMES)
326
-
327
- def add_actions_menu(self, menubar):
328
- menu = menubar.addMenu("&Actions")
329
- add_action_menu = QMenu("Add Action", self)
330
- for action in constants.ACTION_TYPES:
331
- entry_action = QAction(action, self)
332
- entry_action.triggered.connect({
333
- constants.ACTION_COMBO: self.add_action_combined_actions,
334
- constants.ACTION_NOISEDETECTION: self.add_action_noise_detection,
335
- constants.ACTION_FOCUSSTACK: self.add_action_focus_stack,
336
- constants.ACTION_FOCUSSTACKBUNCH: self.add_action_focus_stack_bunch,
337
- constants.ACTION_MULTILAYER: self.add_action_multilayer
338
- }[action])
339
- add_action_menu.addAction(entry_action)
340
- menu.addMenu(add_action_menu)
341
- add_sub_action_menu = QMenu("Add Sub Action", self)
342
- self.sub_action_menu_entries = []
343
- for action in constants.SUB_ACTION_TYPES:
344
- entry_action = QAction(action, self)
345
- entry_action.triggered.connect({
346
- constants.ACTION_MASKNOISE: self.add_sub_action_make_noise,
347
- constants.ACTION_VIGNETTING: self.add_sub_action_vignetting,
348
- constants.ACTION_ALIGNFRAMES: self.add_sub_action_align_frames,
349
- constants.ACTION_BALANCEFRAMES: self.add_sub_action_balance_frames
350
- }[action])
351
- entry_action.setEnabled(False)
352
- self.sub_action_menu_entries.append(entry_action)
353
- add_sub_action_menu.addAction(entry_action)
354
- menu.addMenu(add_sub_action_menu)
355
-
356
- def add_help_menu(self, menubar):
357
- menu = menubar.addMenu("&Help")
358
- menu.setObjectName("Help")
359
-
360
- def fill_toolbar(self, toolbar):
361
- toolbar.addAction(self.add_job_action)
362
- toolbar.addSeparator()
363
- self.action_selector = QComboBox()
364
- self.action_selector.addItems(constants.ACTION_TYPES)
365
- self.action_selector.setEnabled(False)
366
- toolbar.addWidget(self.action_selector)
367
- self.add_action_entry_action = QAction("Add Action", self)
368
- self.add_action_entry_action.setIcon(
369
- QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
370
- self.add_action_entry_action.setToolTip("Add action")
371
- self.add_action_entry_action.triggered.connect(self.add_action)
372
- self.add_action_entry_action.setEnabled(False)
373
- toolbar.addAction(self.add_action_entry_action)
374
- self.sub_action_selector = QComboBox()
375
- self.sub_action_selector.addItems(constants.SUB_ACTION_TYPES)
376
- self.sub_action_selector.setEnabled(False)
377
- toolbar.addWidget(self.sub_action_selector)
378
- self.add_sub_action_entry_action = QAction("Add Sub Action", self)
379
- self.add_sub_action_entry_action.setIcon(
380
- QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
381
- self.add_sub_action_entry_action.setToolTip("Add sub action")
382
- self.add_sub_action_entry_action.triggered.connect(self.add_sub_action)
383
- self.add_sub_action_entry_action.setEnabled(False)
384
- toolbar.addAction(self.add_sub_action_entry_action)
385
- toolbar.addSeparator()
386
- toolbar.addAction(self.delete_element_action)
387
- toolbar.addSeparator()
388
- toolbar.addAction(self.run_job_action)
389
- toolbar.addAction(self.run_all_jobs_action)
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)
390
271
 
391
272
  # pylint: disable=C0103
392
273
  def contextMenuEvent(self, event):
393
- 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()))
394
275
  current_action = None
395
276
  if item:
396
- index = self.job_list.row(item)
277
+ index = self.job_list().row(item)
397
278
  current_action = self.get_job_at(index)
398
- self.job_list.setCurrentRow(index)
399
- 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()))
400
281
  if item:
401
- index = self.action_list.row(item)
402
- self.action_list.setCurrentRow(index)
282
+ index = self.action_list().row(item)
283
+ self.set_current_action(index)
403
284
  _job_row, _action_row, pos = self.get_action_at(index)
404
285
  current_action = pos.action if not pos.is_sub_action else pos.sub_action
405
286
  if current_action:
@@ -417,7 +298,7 @@ class MainWindow(ActionsWindow, LogManager):
417
298
  os.path.exists(self.current_action_working_path):
418
299
  action_name = "Browse Working Path" + (f" > {name}" if name != '' else '')
419
300
  self.browse_working_path_action = QAction(action_name)
420
- self.browse_working_path_action.triggered.connect(self.browse_working_path_path)
301
+ self.browse_working_path_action.triggered.connect(self.browse_working_path)
421
302
  menu.addAction(self.browse_working_path_action)
422
303
  ip, name = get_action_input_path(current_action)
423
304
  if ip != '':
@@ -431,16 +312,24 @@ class MainWindow(ActionsWindow, LogManager):
431
312
  break
432
313
  if p_exists:
433
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})"
434
320
  self.browse_input_path_action = QAction(action_name)
435
- self.browse_input_path_action.triggered.connect(self.browse_input_path_path)
321
+ self.browse_input_path_action.triggered.connect(self.browse_input_path)
436
322
  menu.addAction(self.browse_input_path_action)
437
323
  op, name = get_action_output_path(current_action)
438
324
  if op != '':
439
325
  self.current_action_output_path = f"{self.current_action_working_path}/{op}"
440
326
  if os.path.exists(self.current_action_output_path):
441
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})"
442
331
  self.browse_output_path_action = QAction(action_name)
443
- self.browse_output_path_action.triggered.connect(self.browse_output_path_path)
332
+ self.browse_output_path_action.triggered.connect(self.browse_output_path)
444
333
  menu.addAction(self.browse_output_path_action)
445
334
  menu.addSeparator()
446
335
  menu.addAction(self.run_job_action)
@@ -488,58 +377,57 @@ class MainWindow(ActionsWindow, LogManager):
488
377
  if os.path.exists(p):
489
378
  if running_under_windows():
490
379
  os.startfile(os.path.normpath(p))
491
- elif running_under_macos():
492
- subprocess.run(['open', p], check=True)
493
380
  else:
494
- subprocess.run(['xdg-open', p], check=True)
381
+ cmd = 'open' if running_under_macos() else 'xdg-open'
382
+ subprocess.run([cmd, p], check=True)
495
383
 
496
- def browse_working_path_path(self):
384
+ def browse_working_path(self):
497
385
  self.browse_path(self.current_action_working_path)
498
386
 
499
- def browse_input_path_path(self):
387
+ def browse_input_path(self):
500
388
  self.browse_path(self.current_action_input_path)
501
389
 
502
- def browse_output_path_path(self):
390
+ def browse_output_path(self):
503
391
  self.browse_path(self.current_action_output_path)
504
392
 
505
393
  def refresh_ui(self, job_row=-1, action_row=-1):
506
- self.job_list.clear()
507
- for job in self.project.jobs:
508
- self.add_list_item(self.job_list, job, False)
509
- if self.project.jobs:
510
- 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)
511
399
  if job_row >= 0:
512
- self.job_list.setCurrentRow(job_row)
400
+ self.set_current_job(job_row)
513
401
  if action_row >= 0:
514
- self.action_list.setCurrentRow(action_row)
515
- if self.job_list.count() == 0:
516
- self.add_action_entry_action.setEnabled(False)
517
- 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)
518
406
  self.run_job_action.setEnabled(False)
519
407
  self.run_all_jobs_action.setEnabled(False)
520
408
  else:
521
- self.add_action_entry_action.setEnabled(True)
522
- self.action_selector.setEnabled(True)
523
- self.delete_element_action.setEnabled(True)
524
- self.run_job_action.setEnabled(True)
525
- 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)
526
414
 
527
415
  def quit(self):
528
- if self._check_unsaved_changes():
416
+ if self.project_controller.check_unsaved_changes():
529
417
  for worker in self._workers:
530
418
  worker.stop()
531
419
  self.close()
532
420
 
533
421
  def toggle_expert_options(self):
534
- self.expert_options = self.expert_options_action.isChecked()
422
+ self.expert_options = self.menu_manager.expert_options_action.isChecked()
535
423
 
536
424
  def set_expert_options(self):
537
425
  self.expert_options_action.setChecked(True)
538
426
  self.expert_options = True
539
427
 
540
428
  def before_thread_begins(self):
541
- self.run_job_action.setEnabled(False)
542
- 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)
543
431
 
544
432
  def get_tab_and_position(self, id_str):
545
433
  for i in range(self.tab_widget.count()):
@@ -557,8 +445,8 @@ class MainWindow(ActionsWindow, LogManager):
557
445
  return i
558
446
 
559
447
  def do_handle_end_message(self, status, id_str, message):
560
- self.run_job_action.setEnabled(True)
561
- 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)
562
450
  tab = self.get_tab_at_position(id_str)
563
451
  tab.close_button.setEnabled(True)
564
452
  tab.stop_button.setEnabled(False)
@@ -569,8 +457,7 @@ class MainWindow(ActionsWindow, LogManager):
569
457
  new_window = RunWindow(labels,
570
458
  lambda id_str: self.stop_worker(self.get_tab_position(id_str)),
571
459
  lambda id_str: self.close_window(self.get_tab_position(id_str)),
572
- retouch_paths,
573
- self)
460
+ retouch_paths, self)
574
461
  self.tab_widget.addTab(new_window, title)
575
462
  self.tab_widget.setCurrentIndex(self.tab_widget.count() - 1)
576
463
  if title is not None:
@@ -589,7 +476,7 @@ class MainWindow(ActionsWindow, LogManager):
589
476
  worker = self._workers[tab_position]
590
477
  worker.stop()
591
478
 
592
- def connect_signals(self, worker, window):
479
+ def connect_worker_signals(self, worker, window):
593
480
  worker.before_action_signal.connect(window.handle_before_action)
594
481
  worker.after_action_signal.connect(window.handle_after_action)
595
482
  worker.step_counts_signal.connect(window.handle_step_counts)
@@ -598,23 +485,18 @@ class MainWindow(ActionsWindow, LogManager):
598
485
  worker.after_step_signal.connect(window.handle_after_step)
599
486
  worker.save_plot_signal.connect(window.handle_save_plot)
600
487
  worker.open_app_signal.connect(window.handle_open_app)
601
-
602
- def set_enabled_sub_actions_gui(self, enabled):
603
- self.add_sub_action_entry_action.setEnabled(enabled)
604
- self.sub_action_selector.setEnabled(enabled)
605
- for a in self.sub_action_menu_entries:
606
- 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)
607
491
 
608
492
  def run_job(self):
609
- current_index = self.job_list.currentRow()
493
+ current_index = self.current_job_index()
610
494
  if current_index < 0:
611
- if len(self.project.jobs) > 0:
612
- QMessageBox.warning(self, "No Job Selected", "Please select a job first.")
613
- else:
614
- 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.")
615
497
  return
616
498
  if current_index >= 0:
617
- job = self.project.jobs[current_index]
499
+ job = self.project_job(current_index)
618
500
  if job.enabled():
619
501
  job_name = job.params["name"]
620
502
  labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
@@ -623,29 +505,51 @@ class MainWindow(ActionsWindow, LogManager):
623
505
  new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
624
506
  labels, retouch_paths)
625
507
  worker = JobLogWorker(job, id_str)
626
- self.connect_signals(worker, new_window)
508
+ self.connect_worker_signals(worker, new_window)
627
509
  self.start_thread(worker)
628
510
  self._workers.append(worker)
629
511
  else:
630
- QMessageBox.warning(
631
- self, "Can't run Job",
632
- "Job " + job.params["name"] + " is disabled.")
512
+ QMessageBox.warning(self, "Can't run Job",
513
+ "Job " + job.params["name"] + " is disabled.")
633
514
  return
634
515
 
635
516
  def run_all_jobs(self):
636
517
  labels = [[(self.action_text(a), a.enabled() and
637
- 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()]
638
519
  project_name = ".".join(self.current_file_name().split(".")[:-1])
639
520
  if project_name == '':
640
521
  project_name = '[new]'
641
522
  retouch_paths = []
642
- for job in self.project.jobs:
523
+ for job in self.project_jobs():
643
524
  r = self.get_retouch_path(job)
644
525
  if len(r) > 0:
645
526
  retouch_paths.append((job.params["name"], r))
646
527
  new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
647
528
  labels, retouch_paths)
648
- worker = ProjectLogWorker(self.project, id_str)
649
- self.connect_signals(worker, new_window)
529
+ worker = ProjectLogWorker(self.project(), id_str)
530
+ self.connect_worker_signals(worker, new_window)
650
531
  self.start_thread(worker)
651
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)