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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,539 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from PySide6.QtWidgets import QMainWindow, QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
4
+ from PySide6.QtGui import QIcon
5
+ from PySide6.QtCore import Qt
6
+ from .. config.constants import constants
7
+ from .colors import ColorPalette
8
+ from .action_config import ActionConfig, ActionConfigDialog
9
+ from .project_model import get_action_input_path, get_action_output_path
10
+
11
+ INDENT_SPACE = "   ↪   "
12
+ CLONE_POSTFIX = " (clone)"
13
+
14
+
15
+ @dataclass
16
+ class ActionPosition:
17
+ actions: list
18
+ sub_actions: list | None
19
+ action_index: int
20
+ sub_action_index: int = -1
21
+
22
+ @property
23
+ def is_sub_action(self) -> bool:
24
+ return self.sub_action_index != -1
25
+
26
+ @property
27
+ def action(self):
28
+ return None if self.actions is None else self.actions[self.action_index]
29
+
30
+ @property
31
+ def sub_action(self):
32
+ return None if self.sub_actions is None or self.sub_action_index == -1 else self.sub_actions[self.sub_action_index]
33
+
34
+
35
+ def new_row_after_delete(action_row, pos: ActionPosition):
36
+ if pos.is_sub_action:
37
+ new_row = action_row if pos.sub_action_index < len(pos.sub_actions) else action_row - 1
38
+ else:
39
+ if pos.action_index == 0:
40
+ new_row = 0 if len(pos.actions) > 0 else -1
41
+ elif pos.action_index < len(pos.actions):
42
+ new_row = action_row
43
+ elif pos.action_index == len(pos.actions):
44
+ new_row = action_row - len(pos.actions[pos.action_index - 1].sub_actions) - 1
45
+ return new_row
46
+
47
+
48
+ def new_row_after_insert(action_row, pos: ActionPosition, delta):
49
+ new_row = action_row
50
+ if not pos.is_sub_action:
51
+ new_index = pos.action_index + delta
52
+ if 0 <= new_index < len(pos.actions):
53
+ new_row = 0
54
+ for action in pos.actions[:new_index]:
55
+ new_row += 1 + len(action.sub_actions)
56
+ else:
57
+ new_index = pos.sub_action_index + delta
58
+ if 0 <= new_index < len(pos.sub_actions):
59
+ new_row = 1 + new_index
60
+ for action in pos.actions[:pos.action_index]:
61
+ new_row += 1 + len(action.sub_actions)
62
+ return new_row
63
+
64
+
65
+ def new_row_after_paste(action_row, pos: ActionPosition):
66
+ return new_row_after_insert(action_row, pos, 0)
67
+
68
+
69
+ def new_row_after_clone(job, action_row, is_sub_action, cloned):
70
+ return action_row + 1 if is_sub_action else \
71
+ sum(1 + len(action.sub_actions) for action in job.sub_actions[:job.sub_actions.index(cloned)])
72
+
73
+
74
+ class ProjectEditor(QMainWindow):
75
+ def __init__(self):
76
+ super().__init__()
77
+ self._copy_buffer = None
78
+ self._project_buffer = []
79
+ self.job_list = QListWidget()
80
+ self.action_list = QListWidget()
81
+ self.project = None
82
+ self.job_list_model = None
83
+ self.expert_options = False
84
+ self.script_dir = os.path.dirname(__file__)
85
+
86
+ def set_project(self, project):
87
+ self.project = project
88
+
89
+ def job_text(self, job, long_name=False, html=False):
90
+ txt = f"{job.params.get('name', '(job)')}"
91
+ if html:
92
+ txt = f"<b>{txt}</b>"
93
+ in_path = get_action_input_path(job)
94
+ return txt + (f" [⚙️ Job: 📁 {in_path[0]} → 📂 ...]" if long_name else "")
95
+
96
+ def action_text(self, action, is_sub_action=False, indent=True, long_name=False, html=False):
97
+ icon_map = {
98
+ constants.ACTION_COMBO: '⚡',
99
+ constants.ACTION_NOISEDETECTION: '🌫',
100
+ constants.ACTION_FOCUSSTACK: '🎯',
101
+ constants.ACTION_FOCUSSTACKBUNCH: '🖇',
102
+ constants.ACTION_MULTILAYER: '🎞️',
103
+ constants.ACTION_MASKNOISE: '🎭',
104
+ constants.ACTION_VIGNETTING: '⭕️',
105
+ constants.ACTION_ALIGNFRAMES: '📐',
106
+ constants.ACTION_BALANCEFRAMES: '🌈'
107
+ }
108
+ ico = icon_map.get(action.type_name, '')
109
+ if is_sub_action:
110
+ txt = INDENT_SPACE
111
+ if ico == '':
112
+ ico = '🟣'
113
+ else:
114
+ txt = ''
115
+ if ico == '':
116
+ ico = '🔵'
117
+ if action.params.get('name', '') != '':
118
+ txt += f"{action.params['name']}"
119
+ if html:
120
+ txt = f"<b>{txt}</b>"
121
+ in_path, out_path = get_action_input_path(action), get_action_output_path(action)
122
+ return f"{txt} [{ico} {action.type_name}" + (f": 📁 <i>{in_path[0]}</i> → 📂 <i>{out_path[0]}</i>]" if long_name and not is_sub_action else "]")
123
+
124
+ def get_job_at(self, index):
125
+ return None if index < 0 else self.project.jobs[index]
126
+
127
+ def get_current_job(self):
128
+ return self.get_job_at(self.job_list.currentRow())
129
+
130
+ def get_current_action(self):
131
+ return self.get_action_at(self.action_list.currentRow())
132
+
133
+ def get_action_at(self, action_row):
134
+ job_row = self.job_list.currentRow()
135
+ if job_row < 0 or action_row < 0:
136
+ return (job_row, action_row, None)
137
+ action, sub_action, sub_action_index = self.find_action_position(job_row, action_row)
138
+ if not action:
139
+ return (job_row, action_row, None)
140
+ job = self.project.jobs[job_row]
141
+ if sub_action:
142
+ return (job_row, action_row, ActionPosition(job.sub_actions, action.sub_actions, job.sub_actions.index(action), sub_action_index))
143
+ else:
144
+ return (job_row, action_row, ActionPosition(job.sub_actions, None, job.sub_actions.index(action)))
145
+
146
+ def find_action_position(self, job_index, ui_index):
147
+ if not 0 <= job_index < len(self.project.jobs):
148
+ return (None, None, -1)
149
+ actions = self.project.jobs[job_index].sub_actions
150
+ counter = -1
151
+ for action in actions:
152
+ counter += 1
153
+ if counter == ui_index:
154
+ return (action, None, -1)
155
+ for sub_action_index, sub_action in enumerate(action.sub_actions):
156
+ counter += 1
157
+ if counter == ui_index:
158
+ return (action, sub_action, sub_action_index)
159
+ return (None, None, -1)
160
+
161
+ def refresh_ui(self, job_row=-1, action_row=-1):
162
+ pass
163
+
164
+ def shift_job(self, delta):
165
+ job_index = self.job_list.currentRow()
166
+ if job_index < 0:
167
+ return
168
+ new_index = job_index + delta
169
+ if 0 <= new_index < len(self.project.jobs):
170
+ jobs = self.project.jobs
171
+ self.mark_as_modified()
172
+ jobs.insert(new_index, jobs.pop(job_index))
173
+ self.refresh_ui(new_index, -1)
174
+
175
+ def shift_action(self, delta):
176
+ job_row, action_row, pos = self.get_current_action()
177
+ if pos is not None:
178
+ if not pos.is_sub_action:
179
+ new_index = pos.action_index + delta
180
+ if 0 <= new_index < len(pos.actions):
181
+ self.mark_as_modified()
182
+ pos.actions.insert(new_index, pos.actions.pop(pos.action_index))
183
+ else:
184
+ new_index = pos.sub_action_index + delta
185
+ if 0 <= new_index < len(pos.sub_actions):
186
+ self.mark_as_modified()
187
+ pos.sub_actions.insert(new_index, pos.sub_actions.pop(pos.sub_action_index))
188
+ new_row = new_row_after_insert(action_row, pos, delta)
189
+ self.refresh_ui(job_row, new_row)
190
+
191
+ def move_element_up(self):
192
+ if self.job_list.hasFocus():
193
+ self.shift_job(-1)
194
+ elif self.action_list.hasFocus():
195
+ self.shift_action(-1)
196
+
197
+ def move_element_down(self):
198
+ if self.job_list.hasFocus():
199
+ self.shift_job(+1)
200
+ elif self.action_list.hasFocus():
201
+ self.shift_action(+1)
202
+
203
+ def clone_job(self):
204
+ job_index = self.job_list.currentRow()
205
+ if 0 <= job_index < len(self.project.jobs):
206
+ job_clone = self.project.jobs[job_index].clone(CLONE_POSTFIX)
207
+ new_job_index = job_index + 1
208
+ self.mark_as_modified()
209
+ self.project.jobs.insert(new_job_index, job_clone)
210
+ self.job_list.setCurrentRow(new_job_index)
211
+ self.action_list.setCurrentRow(new_job_index)
212
+ self.refresh_ui(new_job_index, -1)
213
+
214
+ def clone_action(self):
215
+ job_row, action_row, pos = self.get_current_action()
216
+ if not pos.actions:
217
+ return
218
+ self.mark_as_modified()
219
+ job = self.project.jobs[job_row]
220
+ if pos.is_sub_action:
221
+ cloned = pos.sub_action.clone(CLONE_POSTFIX)
222
+ pos.sub_actions.insert(pos.sub_action_index + 1, cloned)
223
+ else:
224
+ cloned = pos.action.clone(CLONE_POSTFIX)
225
+ job.sub_actions.insert(pos.action_index + 1, cloned)
226
+ new_row = new_row_after_clone(job, action_row, pos.is_sub_action, cloned)
227
+ self.refresh_ui(job_row, new_row)
228
+
229
+ def clone_element(self):
230
+ if self.job_list.hasFocus():
231
+ self.clone_job()
232
+ elif self.action_list.hasFocus():
233
+ self.clone_action()
234
+
235
+ def delete_job(self, confirm=True):
236
+ current_index = self.job_list.currentRow()
237
+ if 0 <= current_index < len(self.project.jobs):
238
+ if confirm:
239
+ reply = QMessageBox.question(
240
+ self, "Confirm Delete",
241
+ f"Are you sure you want to delete job '{self.project.jobs[current_index].params.get('name', '')}'?",
242
+ QMessageBox.Yes | QMessageBox.No
243
+ )
244
+ if not confirm or reply == QMessageBox.Yes:
245
+ self.job_list.takeItem(current_index)
246
+ self.mark_as_modified()
247
+ current_job = self.project.jobs.pop(current_index)
248
+ self.action_list.clear()
249
+ self.refresh_ui()
250
+ return current_job
251
+ return None
252
+
253
+ def delete_action(self, confirm=True):
254
+ job_row, action_row, pos = self.get_current_action()
255
+ if pos is not None:
256
+ current_action = pos.action if not pos.is_sub_action else pos.sub_action
257
+ if confirm:
258
+ reply = QMessageBox.question(
259
+ self,
260
+ "Confirm Delete",
261
+ f"Are you sure you want to delete action '{self.action_text(current_action, pos.is_sub_action, indent=False)}'?",
262
+ QMessageBox.Yes | QMessageBox.No
263
+ )
264
+ if not confirm or reply == QMessageBox.Yes:
265
+ self.mark_as_modified()
266
+ if pos.is_sub_action:
267
+ pos.action.pop_sub_action(pos.sub_action_index)
268
+ else:
269
+ self.project.jobs[job_row].pop_sub_action(pos.action_index)
270
+ new_row = new_row_after_delete(action_row, pos)
271
+ self.refresh_ui(job_row, new_row)
272
+ return current_action
273
+ return None
274
+
275
+ def delete_element(self, confirm=True):
276
+ if self.job_list.hasFocus():
277
+ element = self.delete_job(confirm)
278
+ elif self.action_list.hasFocus():
279
+ element = self.delete_action(confirm)
280
+ else:
281
+ element = None
282
+ if self.job_list.count() > 0:
283
+ self.delete_element_action.setEnabled(True)
284
+ return element
285
+
286
+ def add_job(self):
287
+ job_action = ActionConfig("Job")
288
+ dialog = ActionConfigDialog(job_action, self)
289
+ if dialog.exec() == QDialog.Accepted:
290
+ self.mark_as_modified()
291
+ self.project.jobs.append(job_action)
292
+ self.add_list_item(self.job_list, job_action, False)
293
+ self.job_list.setCurrentRow(self.job_list.count() - 1)
294
+ self.job_list.item(self.job_list.count() - 1).setSelected(True)
295
+ self.refresh_ui()
296
+
297
+ def add_action(self, type_name=False):
298
+ current_index = self.job_list.currentRow()
299
+ if current_index < 0:
300
+ if len(self.project.jobs) > 0:
301
+ QMessageBox.warning(self, "No Job Selected", "Please select a job first.")
302
+ else:
303
+ QMessageBox.warning(self, "No Job Added", "Please add a job first.")
304
+ return
305
+ if type_name is False:
306
+ type_name = self.action_selector.currentText()
307
+ action = ActionConfig(type_name)
308
+ action.parent = self.get_current_job()
309
+ dialog = ActionConfigDialog(action, self)
310
+ if dialog.exec() == QDialog.Accepted:
311
+ self.mark_as_modified()
312
+ self.project.jobs[current_index].add_sub_action(action)
313
+ self.add_list_item(self.action_list, action, False)
314
+ self.delete_element_action.setEnabled(False)
315
+
316
+ def add_list_item(self, widget_list, action, is_sub_action):
317
+ if action.type_name == constants.ACTION_JOB:
318
+ text = self.job_text(action, long_name=True, html=True)
319
+ else:
320
+ text = self.action_text(action, long_name=True, html=True, is_sub_action=is_sub_action)
321
+ item = QListWidgetItem()
322
+ item.setText('')
323
+ item.setData(Qt.ItemDataRole.UserRole, True)
324
+ widget_list.addItem(item)
325
+ html_text = f"✅ <span style='color:#{ColorPalette.DARK_BLUE.hex()};'>{text}</span>" \
326
+ if action.enabled() \
327
+ else f"🚫 <span style='color:#{ColorPalette.DARK_RED.hex()};'>{text}</span>"
328
+ label = QLabel(html_text)
329
+ widget_list.setItemWidget(item, label)
330
+
331
+ def get_icon(self, icon):
332
+ return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
333
+
334
+ def add_action_CombinedActions(self):
335
+ self.add_action(constants.ACTION_COMBO)
336
+
337
+ def add_action_NoiseDetection(self):
338
+ self.add_action(constants.ACTION_NOISEDETECTION)
339
+
340
+ def add_action_FocusStack(self):
341
+ self.add_action(constants.ACTION_FOCUSSTACK)
342
+
343
+ def add_action_FocusStackBunch(self):
344
+ self.add_action(constants.ACTION_FOCUSSTACKBUNCH)
345
+
346
+ def add_action_MultiLayer(self):
347
+ self.add_action(constants.ACTION_MULTILAYER)
348
+
349
+ def add_sub_action(self, type_name=False):
350
+ current_job_index = self.job_list.currentRow()
351
+ current_action_index = self.action_list.currentRow()
352
+ if (current_job_index < 0 or current_action_index < 0 or current_job_index >= len(self.project.jobs)):
353
+ return
354
+ job = self.project.jobs[current_job_index]
355
+ action = None
356
+ action_counter = -1
357
+ for i, act in enumerate(job.sub_actions):
358
+ action_counter += 1
359
+ if action_counter == current_action_index:
360
+ action = act
361
+ break
362
+ action_counter += len(act.sub_actions)
363
+ if not action or action.type_name != constants.ACTION_COMBO:
364
+ return
365
+ if type_name is False:
366
+ type_name = self.sub_action_selector.currentText()
367
+ sub_action = ActionConfig(type_name)
368
+ dialog = ActionConfigDialog(sub_action, self)
369
+ if dialog.exec() == QDialog.Accepted:
370
+ self.mark_as_modified()
371
+ action.add_sub_action(sub_action)
372
+ self.on_job_selected(current_job_index)
373
+ self.action_list.setCurrentRow(current_action_index)
374
+
375
+ def add_sub_action_MakeNoise(self):
376
+ self.add_sub_action(constants.ACTION_MASKNOISE)
377
+
378
+ def add_sub_action_Vignetting(self):
379
+ self.add_sub_action(constants.ACTION_VIGNETTING)
380
+
381
+ def add_sub_action_AlignFrames(self):
382
+ self.add_sub_action(constants.ACTION_ALIGNFRAMES)
383
+
384
+ def add_sub_action_BalanceFrames(self):
385
+ self.add_sub_action(constants.ACTION_BALANCEFRAMES)
386
+
387
+ def copy_job(self):
388
+ current_index = self.job_list.currentRow()
389
+ if 0 <= current_index < len(self.project.jobs):
390
+ self._copy_buffer = self.project.jobs[current_index].clone()
391
+
392
+ def copy_action(self):
393
+ job_row, action_row, pos = self.get_current_action()
394
+ if pos.actions is not None:
395
+ self._copy_buffer = pos.sub_action.clone() if pos.is_sub_action else pos.action.clone()
396
+
397
+ def copy_element(self):
398
+ if self.job_list.hasFocus():
399
+ self.copy_job()
400
+ elif self.action_list.hasFocus():
401
+ self.copy_action()
402
+
403
+ def paste_job(self):
404
+ if self._copy_buffer.type_name != constants.ACTION_JOB:
405
+ return
406
+ job_index = self.job_list.currentRow()
407
+ if 0 <= job_index < len(self.project.jobs):
408
+ new_job_index = job_index
409
+ self.mark_as_modified()
410
+ self.project.jobs.insert(new_job_index, self._copy_buffer)
411
+ self.job_list.setCurrentRow(new_job_index)
412
+ self.action_list.setCurrentRow(new_job_index)
413
+ self.refresh_ui(new_job_index, -1)
414
+
415
+ def paste_action(self):
416
+ job_row, action_row, pos = self.get_current_action()
417
+ if pos.actions is not None:
418
+ if not pos.is_sub_action:
419
+ if self._copy_buffer.type_name not in constants.ACTION_TYPES:
420
+ return
421
+ self.mark_as_modified()
422
+ pos.actions.insert(pos.action_index, self._copy_buffer)
423
+ else:
424
+ if pos.action.type_name != constants.ACTION_COMBO or \
425
+ self._copy_buffer.type_name not in constants.SUB_ACTION_TYPES:
426
+ return
427
+ self.mark_as_modified()
428
+ pos.sub_actions.insert(pos.sub_action_index, self._copy_buffer)
429
+ new_row = new_row_after_paste(action_row, pos)
430
+ self.refresh_ui(job_row, new_row)
431
+
432
+ def paste_element(self):
433
+ if self._copy_buffer is None:
434
+ return
435
+ if self.job_list.hasFocus():
436
+ self.paste_job()
437
+ elif self.action_list.hasFocus():
438
+ self.paste_action()
439
+
440
+ def cut_element(self):
441
+ self._copy_buffer = self.delete_element(False)
442
+
443
+ def undo(self):
444
+ job_row = self.job_list.currentRow()
445
+ action_row = self.action_list.currentRow()
446
+ if len(self._project_buffer) > 0:
447
+ self.set_project(self._project_buffer.pop())
448
+ self.refresh_ui()
449
+ len_jobs = len(self.project.jobs)
450
+ if len_jobs > 0:
451
+ if job_row >= len_jobs:
452
+ job_row = len_jobs - 1
453
+ self.job_list.setCurrentRow(job_row)
454
+ len_actions = self.action_list.count()
455
+ if len_actions > 0:
456
+ if action_row >= len_actions:
457
+ action_row = len_actions
458
+ self.action_list.setCurrentRow(action_row)
459
+
460
+ def set_enabled(self, enabled):
461
+ current_action = None
462
+ if self.job_list.hasFocus():
463
+ job_row = self.job_list.currentRow()
464
+ if 0 <= job_row < len(self.project.jobs):
465
+ current_action = self.project.jobs[job_row]
466
+ action_row = -1
467
+ elif self.action_list.hasFocus():
468
+ job_row, action_row, pos = self.get_current_action()
469
+ current_action = pos.sub_action if pos.is_sub_action else pos.action
470
+ if current_action:
471
+ if current_action.enabled() != enabled:
472
+ self.mark_as_modified()
473
+ current_action.set_enabled(enabled)
474
+ self.refresh_ui(job_row, action_row)
475
+
476
+ def enable(self):
477
+ self.set_enabled(True)
478
+
479
+ def disable(self):
480
+ self.set_enabled(False)
481
+
482
+ def set_enabled_all(self, enable=True):
483
+ self.mark_as_modified()
484
+ job_row = self.job_list.currentRow()
485
+ action_row = self.action_list.currentRow()
486
+ for j in self.project.jobs:
487
+ j.set_enabled_all(enable)
488
+ self.refresh_ui(job_row, action_row)
489
+
490
+ def enable_all(self):
491
+ self.set_enabled_all(True)
492
+
493
+ def disable_all(self):
494
+ self.set_enabled_all(False)
495
+
496
+ def on_job_selected(self, index):
497
+ self.action_list.clear()
498
+ if 0 <= index < len(self.project.jobs):
499
+ job = self.project.jobs[index]
500
+ for action in job.sub_actions:
501
+ self.add_list_item(self.action_list, action, False)
502
+ if len(action.sub_actions) > 0:
503
+ for sub_action in action.sub_actions:
504
+ self.add_list_item(self.action_list, sub_action, True)
505
+ self.update_delete_action_state()
506
+
507
+ def update_delete_action_state(self):
508
+ has_job_selected = len(self.job_list.selectedItems()) > 0
509
+ has_action_selected = len(self.action_list.selectedItems()) > 0
510
+ self.delete_element_action.setEnabled(has_job_selected or has_action_selected)
511
+ if has_action_selected and has_job_selected:
512
+ job_index = self.job_list.currentRow()
513
+ if job_index >= len(self.project.jobs):
514
+ job_index = len(self.project.jobs) - 1
515
+ action_index = self.action_list.currentRow()
516
+ if job_index >= 0:
517
+ job = self.project.jobs[job_index]
518
+ action_counter = -1
519
+ current_action = None
520
+ is_sub_action = False
521
+ for action in job.sub_actions:
522
+ action_counter += 1
523
+ if action_counter == action_index:
524
+ current_action = action
525
+ break
526
+ if len(action.sub_actions) > 0:
527
+ for sub_action in action.sub_actions:
528
+ action_counter += 1
529
+ if action_counter == action_index:
530
+ current_action = sub_action
531
+ is_sub_action = True
532
+ break
533
+ if current_action:
534
+ break
535
+ enable_sub_actions = current_action is not None and \
536
+ not is_sub_action and current_action.type_name == constants.ACTION_COMBO
537
+ self.set_enabled_sub_actions_gui(enable_sub_actions)
538
+ else:
539
+ self.set_enabled_sub_actions_gui(False)
@@ -0,0 +1,138 @@
1
+ from copy import deepcopy
2
+ from .. config.constants import constants
3
+
4
+
5
+ class ActionConfig:
6
+ def __init__(self, type_name: str, params: dict=None, parent=None): # noqa
7
+ self.type_name = type_name
8
+ self.params = params or {}
9
+ self.parent = parent
10
+ self.sub_actions: list[ActionConfig] = []
11
+
12
+ def enabled(self):
13
+ return self.params.get('enabled', True)
14
+
15
+ def set_enabled(self, enabled):
16
+ self.params['enabled'] = enabled
17
+
18
+ def set_enabled_all(self, enabled):
19
+ self.params['enabled'] = enabled
20
+ for a in self.sub_actions:
21
+ a.set_enabled_all(enabled)
22
+
23
+ def add_sub_action(self, action):
24
+ self.sub_actions.append(action)
25
+ action.parent = self
26
+
27
+ def pop_sub_action(self, index):
28
+ if index < len(self.sub_actions):
29
+ self.sub_actions.pop(index)
30
+ else:
31
+ raise Exception(f"can't pop sub-action {index}, lenght is {len(self.sub_actions)}")
32
+
33
+ def clone(self, name_postfix=''):
34
+ c = ActionConfig(self.type_name, deepcopy(self.params))
35
+ c.sub_actions = [s.clone() for s in self.sub_actions]
36
+ for s in c.sub_actions:
37
+ s.parent = c
38
+ if name_postfix != '':
39
+ c.params['name'] = c.params.get('name', '') + name_postfix
40
+ return c
41
+
42
+ def to_dict(self):
43
+ dict = {
44
+ 'type_name': self.type_name,
45
+ 'params': self.params,
46
+ }
47
+ if len(self.sub_actions) > 0:
48
+ dict['sub_actions'] = [a.to_dict() for a in self.sub_actions]
49
+ return dict
50
+
51
+ @classmethod
52
+ def from_dict(cls, data):
53
+ a = ActionConfig(data['type_name'], data['params'])
54
+ if 'sub_actions' in data.keys():
55
+ a.sub_actions = [ActionConfig.from_dict(s) for s in data['sub_actions']]
56
+ for s in a.sub_actions:
57
+ s.parent = a
58
+ return a
59
+
60
+
61
+ class Project:
62
+ def __init__(self):
63
+ self.jobs: list[ActionConfig] = []
64
+
65
+ def run_all(self):
66
+ for job in self.jobs:
67
+ stack_job = job.to_stack_job()
68
+ stack_job.run()
69
+
70
+ def clone(self):
71
+ c = Project()
72
+ c.jobs = [j.clone() for j in self.jobs]
73
+ return c
74
+
75
+ def to_dict(self):
76
+ return [j.to_dict() for j in self.jobs]
77
+
78
+ @classmethod
79
+ def from_dict(cls, data):
80
+ p = Project()
81
+ p.jobs = [ActionConfig.from_dict(j) for j in data]
82
+ for j in p.jobs:
83
+ for s in j.sub_actions:
84
+ s.parent = j
85
+ return p
86
+
87
+
88
+ def get_action_working_path(action, get_name=False):
89
+ if action is None:
90
+ return '', ''
91
+ if action in constants.SUB_ACTION_TYPES:
92
+ return get_action_working_path(action.parent, True)
93
+ wp = action.params.get('working_path', '')
94
+ if wp != '':
95
+ return wp, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
96
+ else:
97
+ return get_action_working_path(action.parent, True)
98
+
99
+
100
+ def get_action_output_path(action, get_name=False):
101
+ if action is None:
102
+ return '', ''
103
+ if action.type_name in constants.SUB_ACTION_TYPES:
104
+ return get_action_output_path(action.parent, True)
105
+ name = action.params.get('name', '')
106
+ path = action.params.get('output_path', '')
107
+ if path == '':
108
+ path = name
109
+ return path, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
110
+
111
+
112
+ def get_action_input_path(action, get_name=False):
113
+ if action is None:
114
+ return '', ''
115
+ type_name = action.type_name
116
+ if type_name in constants.SUB_ACTION_TYPES:
117
+ return get_action_input_path(action.parent, True)
118
+ path = action.params.get('input_path', '')
119
+ if path == '':
120
+ if action.parent is None:
121
+ if type_name == constants.ACTION_JOB and len(action.sub_actions) > 0:
122
+ action = action.sub_actions[0]
123
+ path = action.params.get('input_path', '')
124
+ return path, f" {action.params.get('name', '')} [{action.type_name}]"
125
+ else:
126
+ return '', ''
127
+ else:
128
+ actions = action.parent.sub_actions
129
+ if action in actions:
130
+ i = actions.index(action)
131
+ if i == 0:
132
+ return get_action_input_path(action.parent, True)
133
+ else:
134
+ return get_action_output_path(actions[i - 1], True)
135
+ else:
136
+ return '', ''
137
+ else:
138
+ return path, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
File without changes
@@ -0,0 +1,9 @@
1
+ from .. config.gui_constants import gui_constants
2
+
3
+
4
+ class Brush:
5
+ def __init__(self):
6
+ self.size = gui_constants.BRUSH_SIZES['default']
7
+ self.hardness = gui_constants.DEFAULT_BRUSH_HARDNESS
8
+ self.opacity = gui_constants.DEFAULT_BRUSH_OPACITY
9
+ self.flow = gui_constants.DEFAULT_BRUSH_FLOW