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.
- shinestacker/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- 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
|