shinestacker 1.1.0__py3-none-any.whl → 1.2.1__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 (38) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +149 -34
  4. shinestacker/algorithms/balance.py +364 -166
  5. shinestacker/algorithms/base_stack_algo.py +6 -0
  6. shinestacker/algorithms/depth_map.py +1 -1
  7. shinestacker/algorithms/multilayer.py +22 -13
  8. shinestacker/algorithms/noise_detection.py +7 -8
  9. shinestacker/algorithms/pyramid.py +3 -2
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +199 -44
  12. shinestacker/algorithms/stack.py +20 -20
  13. shinestacker/algorithms/stack_framework.py +136 -156
  14. shinestacker/algorithms/utils.py +175 -1
  15. shinestacker/algorithms/vignetting.py +26 -8
  16. shinestacker/config/constants.py +31 -6
  17. shinestacker/core/framework.py +12 -12
  18. shinestacker/gui/action_config.py +59 -7
  19. shinestacker/gui/action_config_dialog.py +427 -283
  20. shinestacker/gui/base_form_dialog.py +11 -6
  21. shinestacker/gui/gui_images.py +10 -10
  22. shinestacker/gui/gui_run.py +1 -1
  23. shinestacker/gui/main_window.py +6 -5
  24. shinestacker/gui/menu_manager.py +16 -2
  25. shinestacker/gui/new_project.py +26 -22
  26. shinestacker/gui/project_controller.py +43 -27
  27. shinestacker/gui/project_converter.py +2 -8
  28. shinestacker/gui/project_editor.py +50 -27
  29. shinestacker/gui/tab_widget.py +3 -3
  30. shinestacker/retouch/exif_data.py +5 -5
  31. shinestacker/retouch/shortcuts_help.py +4 -4
  32. shinestacker/retouch/vignetting_filter.py +12 -8
  33. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/METADATA +1 -1
  34. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/RECORD +38 -37
  35. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/licenses/LICENSE +0 -0
  38. {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/top_level.txt +0 -0
@@ -3,16 +3,21 @@ from PySide6.QtWidgets import QFormLayout, QDialog
3
3
  from PySide6.QtCore import Qt
4
4
 
5
5
 
6
+ def create_form_layout(parent):
7
+ layout = QFormLayout(parent)
8
+ layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
9
+ layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
10
+ layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
11
+ layout.setLabelAlignment(Qt.AlignLeft)
12
+ return layout
13
+
14
+
6
15
  class BaseFormDialog(QDialog):
7
16
  def __init__(self, title, parent=None):
8
17
  super().__init__(parent)
9
18
  self.setWindowTitle(title)
10
19
  self.resize(500, self.height())
11
- self.layout = QFormLayout(self)
12
- self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
13
- self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
14
- self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
15
- self.layout.setLabelAlignment(Qt.AlignLeft)
20
+ self.form_layout = create_form_layout(self)
16
21
 
17
22
  def add_row_to_layout(self, item):
18
- self.layout.addRow(item)
23
+ self.form_layout.addRow(item)
@@ -67,13 +67,13 @@ class GuiImageView(QWidget):
67
67
  super().__init__(parent)
68
68
  self.file_path = file_path
69
69
  self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
70
- self.layout = QVBoxLayout()
71
- self.layout.setContentsMargins(0, 0, 0, 0)
72
- self.layout.setSpacing(0)
70
+ self.main_layout = QVBoxLayout()
71
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
72
+ self.main_layout.setSpacing(0)
73
73
  self.image_label = QLabel()
74
74
  self.image_label.setAlignment(Qt.AlignCenter)
75
- self.layout.addWidget(self.image_label)
76
- self.setLayout(self.layout)
75
+ self.main_layout.addWidget(self.image_label)
76
+ self.setLayout(self.main_layout)
77
77
  pixmap = QPixmap(file_path)
78
78
  if pixmap:
79
79
  scaled_pixmap = pixmap.scaledToWidth(
@@ -105,13 +105,13 @@ class GuiOpenApp(QWidget):
105
105
  self.file_path = file_path
106
106
  self.app = app
107
107
  self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
108
- self.layout = QVBoxLayout()
109
- self.layout.setContentsMargins(0, 0, 0, 0)
110
- self.layout.setSpacing(0)
108
+ self.main_layout = QVBoxLayout()
109
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
110
+ self.main_layout.setSpacing(0)
111
111
  self.image_label = QLabel()
112
112
  self.image_label.setAlignment(Qt.AlignCenter)
113
- self.layout.addWidget(self.image_label)
114
- self.setLayout(self.layout)
113
+ self.main_layout.addWidget(self.image_label)
114
+ self.setLayout(self.main_layout)
115
115
  pixmap = QPixmap(file_path)
116
116
  if pixmap:
117
117
  scaled_pixmap = pixmap.scaledToWidth(
@@ -328,7 +328,7 @@ class RunWorker(LogWorker):
328
328
  self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
329
329
  self.html_signal.emit(f'''
330
330
  <div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
331
- <span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weigt: bold;">{self.tag} begins</span>
331
+ <span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weight: bold;">{self.tag} begins</span>
332
332
  </div>
333
333
  ''') # noqa
334
334
  status, error_message = self.do_run()
@@ -135,6 +135,8 @@ class MainWindow(QMainWindow, LogManager):
135
135
  self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
136
136
  self.project_editor.enable_delete_action_signal.connect(
137
137
  self.menu_manager.delete_element_action.setEnabled)
138
+ self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
139
+ self.menu_manager.set_enabled_undo_action)
138
140
  self.project_controller.update_title_requested.connect(self.update_title)
139
141
  self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
140
142
  self.project_controller.activate_window_requested.connect(self.activateWindow)
@@ -146,8 +148,8 @@ class MainWindow(QMainWindow, LogManager):
146
148
  def modified(self):
147
149
  return self.project_editor.modified()
148
150
 
149
- def mark_as_modified(self, modified=True):
150
- self.project_editor.mark_as_modified(modified)
151
+ def mark_as_modified(self, modified=True, description=''):
152
+ self.project_editor.mark_as_modified(modified, description)
151
153
 
152
154
  def set_project(self, project):
153
155
  self.project_editor.set_project(project)
@@ -407,13 +409,12 @@ class MainWindow(QMainWindow, LogManager):
407
409
  self.menu_manager.add_action_entry_action.setEnabled(False)
408
410
  self.menu_manager.action_selector.setEnabled(False)
409
411
  self.menu_manager.run_job_action.setEnabled(False)
410
- self.menu_manager.run_all_jobs_action.setEnabled(False)
411
412
  else:
412
413
  self.menu_manager.add_action_entry_action.setEnabled(True)
413
414
  self.menu_manager.action_selector.setEnabled(True)
414
415
  self.menu_manager.delete_element_action.setEnabled(True)
415
416
  self.menu_manager.run_job_action.setEnabled(True)
416
- self.menu_manager.run_all_jobs_action.setEnabled(True)
417
+ self.menu_manager.set_enabled_run_all_jobs(self.job_list_count() > 1)
417
418
 
418
419
  def quit(self):
419
420
  if self.project_controller.check_unsaved_changes():
@@ -425,7 +426,7 @@ class MainWindow(QMainWindow, LogManager):
425
426
  self.expert_options = self.menu_manager.expert_options_action.isChecked()
426
427
 
427
428
  def set_expert_options(self):
428
- self.expert_options_action.setChecked(True)
429
+ self.menu_manager.expert_options_action.setChecked(True)
429
430
  self.expert_options = True
430
431
 
431
432
  def before_thread_begins(self):
@@ -82,7 +82,10 @@ class MenuManager:
82
82
 
83
83
  def add_edit_menu(self):
84
84
  menu = self.menubar.addMenu("&Edit")
85
- for name in ["&Undo", "&Cut", "Cop&y", "&Paste", "Duplicate"]:
85
+ self.undo_action = self.action("&Undo")
86
+ self.undo_action.setEnabled(False)
87
+ menu.addAction(self.undo_action)
88
+ for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
86
89
  menu.addAction(self.action(name))
87
90
  self.delete_element_action = self.action("Delete")
88
91
  self.delete_element_action.setEnabled(False)
@@ -113,7 +116,7 @@ class MenuManager:
113
116
  self.run_job_action.setEnabled(False)
114
117
  menu.addAction(self.run_job_action)
115
118
  self.run_all_jobs_action = self.action("Run All Jobs")
116
- self.run_all_jobs_action.setEnabled(False)
119
+ self.set_enabled_run_all_jobs(False)
117
120
  menu.addAction(self.run_all_jobs_action)
118
121
 
119
122
  def add_actions_menu(self):
@@ -234,3 +237,14 @@ class MenuManager:
234
237
  self.sub_action_selector.setEnabled(enabled)
235
238
  for a in self.sub_action_menu_entries:
236
239
  a.setEnabled(enabled)
240
+
241
+ def set_enabled_run_all_jobs(self, enabled):
242
+ tooltip = self.tooltips["Run All Jobs"]
243
+ self.run_all_jobs_action.setEnabled(enabled)
244
+ if not enabled:
245
+ tooltip += " (requires more tha one job)"
246
+ self.run_all_jobs_action.setToolTip(tooltip)
247
+
248
+ def set_enabled_undo_action(self, enabled, description):
249
+ self.undo_action.setEnabled(enabled)
250
+ self.undo_action.setText(f"&Undo {description}")
@@ -36,11 +36,11 @@ class NewProjectDialog(BaseFormDialog):
36
36
  def add_bold_label(self, label):
37
37
  label = QLabel(label)
38
38
  label.setStyleSheet("font-weight: bold")
39
- self.layout.addRow(label)
39
+ self.form_layout.addRow(label)
40
40
 
41
41
  def add_label(self, label):
42
42
  label = QLabel(label)
43
- self.layout.addRow(label)
43
+ self.form_layout.addRow(label)
44
44
 
45
45
  def create_form(self):
46
46
  icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
@@ -49,10 +49,10 @@ class NewProjectDialog(BaseFormDialog):
49
49
  icon_label = QLabel()
50
50
  icon_label.setPixmap(icon_pixmap)
51
51
  icon_label.setAlignment(Qt.AlignCenter)
52
- self.layout.addRow(icon_label)
52
+ self.form_layout.addRow(icon_label)
53
53
  spacer = QLabel("")
54
54
  spacer.setFixedHeight(10)
55
- self.layout.addRow(spacer)
55
+ self.form_layout.addRow(spacer)
56
56
 
57
57
  self.input_folder, container = create_select_file_paths_widget(
58
58
  '', 'input files folder', 'input files folder')
@@ -92,26 +92,26 @@ class NewProjectDialog(BaseFormDialog):
92
92
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
93
93
 
94
94
  self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
95
- self.layout.addRow("Input folder:", container)
96
- self.layout.addRow("Number of frames: ", self.frames_label)
95
+ self.form_layout.addRow("Input folder:", container)
96
+ self.form_layout.addRow("Number of frames: ", self.frames_label)
97
97
  self.add_label("")
98
98
  self.add_bold_label("2️⃣ Select basic options.")
99
99
  if self.expert():
100
- self.layout.addRow("Automatic noise detection:", self.noise_detection)
101
- self.layout.addRow("Vignetting correction:", self.vignetting_correction)
102
- self.layout.addRow("Align layers:", self.align_frames)
103
- self.layout.addRow("Balance layers:", self.balance_frames)
104
- self.layout.addRow("Bunch stack:", self.bunch_stack)
105
- self.layout.addRow("Bunch frames:", self.bunch_frames)
106
- self.layout.addRow("Bunch overlap:", self.bunch_overlap)
107
- self.layout.addRow("Number of bunches: ", self.bunches_label)
100
+ self.form_layout.addRow("Automatic noise detection:", self.noise_detection)
101
+ self.form_layout.addRow("Vignetting correction:", self.vignetting_correction)
102
+ self.form_layout.addRow("Align layers:", self.align_frames)
103
+ self.form_layout.addRow("Balance layers:", self.balance_frames)
104
+ self.form_layout.addRow("Bunch stack:", self.bunch_stack)
105
+ self.form_layout.addRow("Bunch frames:", self.bunch_frames)
106
+ self.form_layout.addRow("Bunch overlap:", self.bunch_overlap)
107
+ self.form_layout.addRow("Number of bunches: ", self.bunches_label)
108
108
  if self.expert():
109
- self.layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
110
- self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
109
+ self.form_layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
110
+ self.form_layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
111
111
  else:
112
- self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
112
+ self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
113
113
  if self.expert():
114
- self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
114
+ self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
115
115
  self.add_label("")
116
116
  self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
117
117
  self.add_label("")
@@ -182,8 +182,8 @@ class NewProjectDialog(BaseFormDialog):
182
182
  height, width = img.shape[:2]
183
183
  n_bytes = 1 if img.dtype == np.uint8 else 2
184
184
  n_bits = 8 if img.dtype == np.uint8 else 16
185
- n_mbytes = float(n_bytes * height * width * self.n_image_files) / 10**6
186
- if n_mbytes > 500 and not self.bunch_stack.isChecked():
185
+ n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
186
+ if n_gbytes > 1 and not self.bunch_stack.isChecked():
187
187
  msg = QMessageBox()
188
188
  msg.setStyleSheet("""
189
189
  QMessageBox {
@@ -201,11 +201,15 @@ class NewProjectDialog(BaseFormDialog):
201
201
  msg.setWindowTitle("Too many frames")
202
202
  msg.setText(f"You selected {self.n_image_files} images "
203
203
  f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
204
- "Processing may require a significant amount of memory.\n\n"
204
+ "Processing may require a significant amount "
205
+ "of memory or I/O buffering.\n\n"
205
206
  "Continue anyway?")
206
207
  msg.setInformativeText("You may consider to split the processing "
207
208
  " using a bunch stack to reduce memory usage.\n\n"
208
- '✅ Check the option "Bunch stack".')
209
+ '✅ Check the option "Bunch stack".\n\n'
210
+ "➡️ Check expert options for the stacking algorithm."
211
+ 'Go to "View" > "Expert Options".'
212
+ )
209
213
  msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
210
214
  msg.setDefaultButton(QMessageBox.Cancel)
211
215
  if msg.exec_() != QMessageBox.Ok:
@@ -29,8 +29,8 @@ class ProjectController(QObject):
29
29
  def refresh_ui(self, job_row=-1, action_row=-1):
30
30
  self.refresh_ui_requested.emit(job_row, action_row)
31
31
 
32
- def mark_as_modified(self, modified=True):
33
- self.project_editor.mark_as_modified(modified)
32
+ def mark_as_modified(self, modified=True, description=''):
33
+ self.project_editor.mark_as_modified(modified, description)
34
34
 
35
35
  def modified(self):
36
36
  return self.project_editor.modified()
@@ -136,6 +136,7 @@ class ProjectController(QObject):
136
136
  self.clear_job_list()
137
137
  self.clear_action_list()
138
138
  self.mark_as_modified(False)
139
+ self.project_editor.reset_undo()
139
140
 
140
141
  def new_project(self):
141
142
  if not self.check_unsaved_changes():
@@ -151,68 +152,82 @@ class ProjectController(QObject):
151
152
  dialog = NewProjectDialog(self.parent)
152
153
  if dialog.exec() == QDialog.Accepted:
153
154
  self.save_actions_set_enabled(True)
155
+ self.project_editor.reset_undo()
154
156
  input_folder = dialog.get_input_folder().split('/')
155
157
  working_path = '/'.join(input_folder[:-1])
156
158
  input_path = input_folder[-1]
157
159
  if dialog.get_noise_detection():
158
- job_noise = ActionConfig(constants.ACTION_JOB,
159
- {'name': 'detect-noise', 'working_path': working_path,
160
- 'input_path': input_path})
160
+ job_noise = ActionConfig(
161
+ constants.ACTION_JOB,
162
+ {'name': f'{input_path}-detect-noise', 'working_path': working_path,
163
+ 'input_path': input_path})
161
164
  noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
162
- {'name': 'detect-noise'})
165
+ {'name': f'{input_path}-detect-noise'})
163
166
  job_noise.add_sub_action(noise_detection)
164
167
  self.add_job_to_project(job_noise)
165
168
  job = ActionConfig(constants.ACTION_JOB,
166
- {'name': 'focus-stack', 'working_path': working_path,
169
+ {'name': f'{input_path}-focus-stack',
170
+ 'working_path': working_path,
167
171
  'input_path': input_path})
172
+ preprocess_name = ''
168
173
  if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
169
174
  dialog.get_align_frames() or dialog.get_balance_frames():
170
- combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
175
+ preprocess_name = f'{input_path}-preprocess'
176
+ combo_action = ActionConfig(
177
+ constants.ACTION_COMBO, {'name': preprocess_name})
171
178
  if dialog.get_noise_detection():
172
- mask_noise = ActionConfig(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
179
+ mask_noise = ActionConfig(
180
+ constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
173
181
  combo_action.add_sub_action(mask_noise)
174
182
  if dialog.get_vignetting_correction():
175
- vignetting = ActionConfig(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
183
+ vignetting = ActionConfig(
184
+ constants.ACTION_VIGNETTING, {'name': 'vignetting'})
176
185
  combo_action.add_sub_action(vignetting)
177
186
  if dialog.get_align_frames():
178
- align = ActionConfig(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
187
+ align = ActionConfig(
188
+ constants.ACTION_ALIGNFRAMES, {'name': 'align'})
179
189
  combo_action.add_sub_action(align)
180
190
  if dialog.get_balance_frames():
181
- balance = ActionConfig(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
191
+ balance = ActionConfig(
192
+ constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
182
193
  combo_action.add_sub_action(balance)
183
194
  job.add_sub_action(combo_action)
184
195
  if dialog.get_bunch_stack():
185
- bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
186
- {'name': 'bunches', 'frames': dialog.get_bunch_frames(),
187
- 'overlap': dialog.get_bunch_overlap()})
196
+ bunch_stack_name = f'{input_path}-bunches'
197
+ bunch_stack = ActionConfig(
198
+ constants.ACTION_FOCUSSTACKBUNCH,
199
+ {'name': bunch_stack_name, 'frames': dialog.get_bunch_frames(),
200
+ 'overlap': dialog.get_bunch_overlap()})
188
201
  job.add_sub_action(bunch_stack)
189
202
  if dialog.get_focus_stack_pyramid():
203
+ focus_pyramid_name = f'{input_path}-focus-stack-pyramid'
190
204
  focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
191
- {'name': 'focus-stack-pyramid',
205
+ {'name': focus_pyramid_name,
192
206
  'stacker': constants.STACK_ALGO_PYRAMID})
193
207
  job.add_sub_action(focus_pyramid)
194
208
  if dialog.get_focus_stack_depth_map():
209
+ focus_depth_map_name = f'{input_path}-focus-stack-depth-map'
195
210
  focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
196
- {'name': 'focus-stack-depth-map',
211
+ {'name': focus_depth_map_name,
197
212
  'stacker': constants.STACK_ALGO_DEPTH_MAP})
198
213
  job.add_sub_action(focus_depth_map)
199
214
  if dialog.get_multi_layer():
200
- input_path = []
215
+ multi_input_path = []
201
216
  if dialog.get_focus_stack_pyramid():
202
- input_path.append("focus-stack-pyramid")
217
+ multi_input_path.append(focus_pyramid_name)
203
218
  if dialog.get_focus_stack_depth_map():
204
- input_path.append("focus-stack-depth-map")
219
+ multi_input_path.append(focus_depth_map_name)
205
220
  if dialog.get_bunch_stack():
206
- input_path.append("bunches")
221
+ multi_input_path.append(bunch_stack_name)
222
+ elif preprocess_name:
223
+ multi_input_path.append(preprocess_name)
207
224
  multi_layer = ActionConfig(
208
225
  constants.ACTION_MULTILAYER,
209
- {
210
- 'name': 'multi-layer',
211
- 'input_path': constants.PATH_SEPARATOR.join(input_path)
212
- })
226
+ {'name': f'{input_path}-multi-layer',
227
+ 'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
213
228
  job.add_sub_action(multi_layer)
214
229
  self.add_job_to_project(job)
215
- self.mark_as_modified(True)
230
+ self.mark_as_modified(True, "New Project")
216
231
  self.refresh_ui(0, -1)
217
232
 
218
233
  def open_project(self, file_path=False):
@@ -231,6 +246,7 @@ class ProjectController(QObject):
231
246
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
232
247
  self.set_project(project)
233
248
  self.mark_as_modified(False)
249
+ self.project_editor.reset_undo()
234
250
  self.refresh_ui(0, -1)
235
251
  if self.job_list_count() > 0:
236
252
  self.set_current_job(0)
@@ -353,4 +369,4 @@ class ProjectController(QObject):
353
369
  dialog = self.action_config_dialog(action)
354
370
  if dialog.exec() == QDialog.Accepted:
355
371
  self.on_job_selected(self.current_job_index())
356
- self.mark_as_modified()
372
+ # self.mark_as_modified(True. "Edit Action") <-- done by dialog
@@ -9,8 +9,7 @@ from .. algorithms.vignetting import Vignetting
9
9
  from .. algorithms.align import AlignFrames
10
10
  from .. algorithms.balance import BalanceFrames
11
11
  from .. algorithms.stack import FocusStack, FocusStackBunch
12
- from .. algorithms.pyramid import PyramidStack
13
- from .. algorithms.pyramid_tiles import PyramidTilesStack
12
+ from .. algorithms.pyramid_auto import PyramidAutoStack
14
13
  from .. algorithms.depth_map import DepthMapStack
15
14
  from .. algorithms.multilayer import MultiLayer
16
15
  from .project_model import Project, ActionConfig
@@ -107,11 +106,7 @@ class ProjectConverter:
107
106
  if stacker == constants.STACK_ALGO_PYRAMID:
108
107
  algo_dict, module_dict = self.filter_dict_keys(
109
108
  action_config.params, 'pyramid_')
110
- stack_algo = PyramidStack(**algo_dict)
111
- elif stacker == constants.STACK_ALGO_PYRAMID_TILES:
112
- algo_dict, module_dict = self.filter_dict_keys(
113
- action_config.params, 'tiles_pyramid_')
114
- stack_algo = PyramidTilesStack(**algo_dict)
109
+ stack_algo = PyramidAutoStack(**algo_dict)
115
110
  elif stacker == constants.STACK_ALGO_DEPTH_MAP:
116
111
  algo_dict, module_dict = self.filter_dict_keys(
117
112
  action_config.params, 'depthmap_')
@@ -119,7 +114,6 @@ class ProjectConverter:
119
114
  else:
120
115
  raise InvalidOptionError('stacker', stacker, f"valid options are: "
121
116
  f"{constants.STACK_ALGO_PYRAMID}, "
122
- f"{constants.STACK_ALGO_PYRAMID_TILES}, "
123
117
  f"{constants.STACK_ALGO_DEPTH_MAP}")
124
118
  if action_config.type_name == constants.ACTION_FOCUSSTACK:
125
119
  return FocusStack(**module_dict, stack_algo=stack_algo)
@@ -74,19 +74,32 @@ def new_row_after_clone(job, action_row, is_sub_action, cloned):
74
74
  for action in job.sub_actions[:job.sub_actions.index(cloned)])
75
75
 
76
76
 
77
- class ProjectUndoManager:
78
- def __init__(self):
77
+ class ProjectUndoManager(QObject):
78
+ set_enabled_undo_action_requested = Signal(bool, str)
79
+
80
+ def __init__(self, parent=None):
81
+ super().__init__(parent)
79
82
  self._undo_buffer = []
80
83
 
81
- def add(self, item):
82
- self._undo_buffer.append(item)
84
+ def add(self, item, description):
85
+ self._undo_buffer.append((item, description))
86
+ self.set_enabled_undo_action_requested.emit(True, description)
83
87
 
84
88
  def pop(self):
85
- return self._undo_buffer.pop()
89
+ last = self._undo_buffer.pop()
90
+ if len(self._undo_buffer) == 0:
91
+ self.set_enabled_undo_action_requested.emit(False, '')
92
+ else:
93
+ self.set_enabled_undo_action_requested.emit(True, self._undo_buffer[-1][1])
94
+ return last[0]
86
95
 
87
96
  def filled(self):
88
97
  return len(self._undo_buffer) != 0
89
98
 
99
+ def reset(self):
100
+ self._undo_buffer = []
101
+ self.set_enabled_undo_action_requested.emit(False, '')
102
+
90
103
 
91
104
  class ProjectEditor(QObject):
92
105
  INDENT_SPACE = "&nbsp;&nbsp;&nbsp;↪&nbsp;&nbsp;&nbsp;"
@@ -99,7 +112,7 @@ class ProjectEditor(QObject):
99
112
 
100
113
  def __init__(self, parent=None):
101
114
  super().__init__(parent)
102
- self._undo_manager = ProjectUndoManager()
115
+ self.undo_manager = ProjectUndoManager()
103
116
  self._modified = False
104
117
  self._project = None
105
118
  self._copy_buffer = None
@@ -108,19 +121,25 @@ class ProjectEditor(QObject):
108
121
  self._action_list = QListWidget()
109
122
  self.dialog = None
110
123
 
111
- def add_undo(self, item):
112
- self._undo_manager.add(item)
124
+ def reset_undo(self):
125
+ self.undo_manager.reset()
126
+
127
+ def add_undo(self, item, description=''):
128
+ self.undo_manager.add(item, description)
113
129
 
114
130
  def pop_undo(self):
115
- return self._undo_manager.pop()
131
+ return self.undo_manager.pop()
116
132
 
117
133
  def filled_undo(self):
118
- return self._undo_manager.filled()
134
+ return self.undo_manager.filled()
119
135
 
120
- def mark_as_modified(self, modified=True):
136
+ def set_modified(self, modified):
137
+ self._modified = modified
138
+
139
+ def mark_as_modified(self, modified=True, description=''):
121
140
  self._modified = modified
122
141
  if modified:
123
- self.add_undo(self._project.clone())
142
+ self.add_undo(self._project.clone(), description)
124
143
  self.modified_signal.emit(modified)
125
144
 
126
145
  def modified(self):
@@ -306,7 +325,7 @@ class ProjectEditor(QObject):
306
325
  new_index = job_index + delta
307
326
  if 0 <= new_index < self.num_project_jobs():
308
327
  jobs = self.project_jobs()
309
- self.mark_as_modified()
328
+ self.mark_as_modified(True, "Shift Job")
310
329
  jobs.insert(new_index, jobs.pop(job_index))
311
330
  self.refresh_ui_signal.emit(new_index, -1)
312
331
 
@@ -316,12 +335,12 @@ class ProjectEditor(QObject):
316
335
  if not pos.is_sub_action:
317
336
  new_index = pos.action_index + delta
318
337
  if 0 <= new_index < len(pos.actions):
319
- self.mark_as_modified()
338
+ self.mark_as_modified(True, "Shift Action")
320
339
  pos.actions.insert(new_index, pos.actions.pop(pos.action_index))
321
340
  else:
322
341
  new_index = pos.sub_action_index + delta
323
342
  if 0 <= new_index < len(pos.sub_actions):
324
- self.mark_as_modified()
343
+ self.mark_as_modified(True, "Shift Sub-action")
325
344
  pos.sub_actions.insert(new_index, pos.sub_actions.pop(pos.sub_action_index))
326
345
  new_row = new_row_after_insert(action_row, pos, delta)
327
346
  self.refresh_ui_signal.emit(job_row, new_row)
@@ -343,7 +362,7 @@ class ProjectEditor(QObject):
343
362
  if 0 <= job_index < self.num_project_jobs():
344
363
  job_clone = self.project_job(job_index).clone(self.CLONE_POSTFIX)
345
364
  new_job_index = job_index + 1
346
- self.mark_as_modified()
365
+ self.mark_as_modified(True, "Duplicate Job")
347
366
  self.project_jobs().insert(new_job_index, job_clone)
348
367
  self.set_current_job(new_job_index)
349
368
  self.set_current_action(new_job_index)
@@ -353,7 +372,7 @@ class ProjectEditor(QObject):
353
372
  job_row, action_row, pos = self.get_current_action()
354
373
  if not pos.actions:
355
374
  return
356
- self.mark_as_modified()
375
+ self.mark_as_modified(True, "Duplicate Action")
357
376
  job = self.project_job(job_row)
358
377
  if pos.is_sub_action:
359
378
  cloned = pos.sub_action.clone(self.CLONE_POSTFIX)
@@ -384,7 +403,7 @@ class ProjectEditor(QObject):
384
403
  reply = None
385
404
  if not confirm or reply == QMessageBox.Yes:
386
405
  self.take_job(current_index)
387
- self.mark_as_modified()
406
+ self.mark_as_modified(True, "Delete Job")
388
407
  current_job = self.project_jobs().pop(current_index)
389
408
  self.clear_action_list()
390
409
  self.refresh_ui_signal.emit(-1, -1)
@@ -406,10 +425,11 @@ class ProjectEditor(QObject):
406
425
  else:
407
426
  reply = None
408
427
  if not confirm or reply == QMessageBox.Yes:
409
- self.mark_as_modified()
410
428
  if pos.is_sub_action:
429
+ self.mark_as_modified(True, "Delete Action")
411
430
  pos.action.pop_sub_action(pos.sub_action_index)
412
431
  else:
432
+ self.mark_as_modified(True, "Delete Sub-action")
413
433
  self.project_job(job_row).pop_sub_action(pos.action_index)
414
434
  new_row = new_row_after_delete(action_row, pos)
415
435
  self.refresh_ui_signal.emit(job_row, new_row)
@@ -432,7 +452,7 @@ class ProjectEditor(QObject):
432
452
  job_action = ActionConfig("Job")
433
453
  self.dialog = self.action_config_dialog(job_action)
434
454
  if self.dialog.exec() == QDialog.Accepted:
435
- self.mark_as_modified()
455
+ self.mark_as_modified(True, "Add Job")
436
456
  self.project_jobs().append(job_action)
437
457
  self.add_list_item(self.job_list(), job_action, False)
438
458
  self.set_current_job(self.job_list_count() - 1)
@@ -453,7 +473,7 @@ class ProjectEditor(QObject):
453
473
  action.parent = self.get_current_job()
454
474
  self.dialog = self.action_config_dialog(action)
455
475
  if self.dialog.exec() == QDialog.Accepted:
456
- self.mark_as_modified()
476
+ self.mark_as_modified("Add Action")
457
477
  self.project_job(current_index).add_sub_action(action)
458
478
  self.add_list_item(self.action_list(), action, False)
459
479
  self.enable_delete_action_signal.emit(False)
@@ -493,7 +513,7 @@ class ProjectEditor(QObject):
493
513
  sub_action = ActionConfig(type_name)
494
514
  self.dialog = self.action_config_dialog(sub_action)
495
515
  if self.dialog.exec() == QDialog.Accepted:
496
- self.mark_as_modified()
516
+ self.mark_as_modified("Add Sub-action")
497
517
  action.add_sub_action(sub_action)
498
518
  self.on_job_selected(current_job_index)
499
519
  self.set_current_action(current_action_index)
@@ -521,7 +541,7 @@ class ProjectEditor(QObject):
521
541
  job_index = self.current_job_index()
522
542
  if 0 <= job_index < self.num_project_jobs():
523
543
  new_job_index = job_index
524
- self.mark_as_modified()
544
+ self.mark_as_modified(True, "Paste Job")
525
545
  self.project_jobs().insert(new_job_index, self.copy_buffer())
526
546
  self.set_current_job(new_job_index)
527
547
  self.set_current_action(new_job_index)
@@ -533,13 +553,13 @@ class ProjectEditor(QObject):
533
553
  if not pos.is_sub_action:
534
554
  if self.copy_buffer().type_name not in constants.ACTION_TYPES:
535
555
  return
536
- self.mark_as_modified()
556
+ self.mark_as_modified(True, "Paste Action")
537
557
  pos.actions.insert(pos.action_index, self.copy_buffer())
538
558
  else:
539
559
  if pos.action.type_name != constants.ACTION_COMBO or \
540
560
  self.copy_buffer().type_name not in constants.SUB_ACTION_TYPES:
541
561
  return
542
- self.mark_as_modified()
562
+ self.mark_as_modified(True, "Paste Sub-action")
543
563
  pos.sub_actions.insert(pos.sub_action_index, self.copy_buffer())
544
564
  new_row = new_row_after_paste(action_row, pos)
545
565
  self.refresh_ui_signal.emit(job_row, new_row)
@@ -583,7 +603,10 @@ class ProjectEditor(QObject):
583
603
  action_row = -1
584
604
  if current_action:
585
605
  if current_action.enabled() != enabled:
586
- self.mark_as_modified()
606
+ if enabled:
607
+ self.mark_as_modified(True, "Enable")
608
+ else:
609
+ self.mark_as_modified(True, "Disable")
587
610
  current_action.set_enabled(enabled)
588
611
  self.refresh_ui_signal.emit(job_row, action_row)
589
612
 
@@ -594,7 +617,7 @@ class ProjectEditor(QObject):
594
617
  self.set_enabled(False)
595
618
 
596
619
  def set_enabled_all(self, enable=True):
597
- self.mark_as_modified()
620
+ self.mark_as_modified(True, "Enable All")
598
621
  job_row = self.current_job_index()
599
622
  action_row = self.current_action_index()
600
623
  for j in self.project_jobs():
@@ -12,10 +12,10 @@ class TabWidgetWithPlaceholder(QWidget):
12
12
 
13
13
  def __init__(self, parent=None):
14
14
  super().__init__(parent)
15
- self.layout = QVBoxLayout(self)
16
- self.layout.setContentsMargins(0, 0, 0, 0)
15
+ self.main_layout = QVBoxLayout(self)
16
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
17
17
  self.stacked_widget = QStackedWidget()
18
- self.layout.addWidget(self.stacked_widget)
18
+ self.main_layout.addWidget(self.stacked_widget)
19
19
  self.tab_widget = QTabWidget()
20
20
  self.stacked_widget.addWidget(self.tab_widget)
21
21
  self.placeholder = QLabel()