shinestacker 0.5.0__py3-none-any.whl → 1.0.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 (57) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +37 -16
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +3 -4
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +275 -371
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
  28. shinestacker/gui/project_converter.py +6 -6
  29. shinestacker/gui/project_editor.py +248 -165
  30. shinestacker/gui/project_model.py +2 -7
  31. shinestacker/gui/tab_widget.py +81 -0
  32. shinestacker/gui/time_progress_bar.py +95 -0
  33. shinestacker/retouch/base_filter.py +173 -40
  34. shinestacker/retouch/brush_preview.py +0 -10
  35. shinestacker/retouch/brush_tool.py +2 -5
  36. shinestacker/retouch/denoise_filter.py +5 -44
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +318 -40
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +34 -30
  43. shinestacker/retouch/layer_collection.py +2 -0
  44. shinestacker/retouch/shortcuts_help.py +12 -0
  45. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  46. shinestacker/retouch/vignetting_filter.py +69 -0
  47. shinestacker/retouch/white_balance_filter.py +46 -14
  48. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
  49. shinestacker-1.0.1.dist-info/RECORD +91 -0
  50. shinestacker/app/app_config.py +0 -22
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
  57. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,60 +1,153 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
2
- import os.path
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, R0912, R0904, R0915, W0718
3
2
  import os
4
- # import traceback
3
+ import os.path
4
+ import traceback
5
5
  import json
6
6
  import jsonpickle
7
+ from PySide6.QtCore import Signal, QObject
7
8
  from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
8
- from .. core.core_utils import get_app_base_path
9
9
  from .. config.constants import constants
10
+ from .. core.core_utils import get_app_base_path
10
11
  from .project_model import ActionConfig
11
- from .project_editor import ProjectEditor
12
12
  from .new_project import NewProjectDialog
13
13
  from .project_model import Project
14
+ from .project_editor import ProjectEditor
14
15
 
15
16
 
16
- class ActionsWindow(ProjectEditor):
17
- def __init__(self):
18
- super().__init__()
19
- self.update_title()
17
+ class ProjectController(QObject):
18
+ update_title_requested = Signal()
19
+ refresh_ui_requested = Signal(int, int)
20
+ activate_window_requested = Signal()
21
+ enable_save_actions_requested = Signal(bool)
22
+
23
+ def __init__(self, parent):
24
+ super().__init__(parent)
25
+ self.parent = parent
26
+ self.project_editor = ProjectEditor(parent)
27
+
28
+ def refresh_ui(self, job_row=-1, action_row=-1):
29
+ self.refresh_ui_requested.emit(job_row, action_row)
30
+
31
+ def mark_as_modified(self, modified=True):
32
+ self.project_editor.mark_as_modified(modified)
33
+
34
+ def modified(self):
35
+ return self.project_editor.modified()
36
+
37
+ def set_project(self, project):
38
+ self.project_editor.set_project(project)
39
+
40
+ def project(self):
41
+ return self.project_editor.project()
42
+
43
+ def project_jobs(self):
44
+ return self.project_editor.project_jobs()
45
+
46
+ def project_job(self, i):
47
+ return self.project_editor.project_job(i)
48
+
49
+ def add_job_to_project(self, job):
50
+ self.project_editor.add_job_to_project(job)
51
+
52
+ def num_project_jobs(self):
53
+ return self.project_editor.num_project_jobs()
54
+
55
+ def save_actions_set_enabled(self, enabled):
56
+ self.enable_save_actions_requested.emit(enabled)
57
+
58
+ def current_file_path(self):
59
+ return self.project_editor.current_file_path()
60
+
61
+ def current_file_directory(self):
62
+ return self.project_editor.current_file_directory()
63
+
64
+ def current_file_name(self):
65
+ return self.project_editor.current_file_name()
66
+
67
+ def set_current_file_path(self, path):
68
+ self.project_editor.set_current_file_path(path)
69
+
70
+ def job_list(self):
71
+ return self.project_editor.job_list()
72
+
73
+ def action_list(self):
74
+ return self.project_editor.action_list()
75
+
76
+ def current_job_index(self):
77
+ return self.project_editor.current_job_index()
78
+
79
+ def current_action_index(self):
80
+ return self.project_editor.current_action_index()
81
+
82
+ def set_current_job(self, index):
83
+ return self.project_editor.set_current_job(index)
84
+
85
+ def set_current_action(self, index):
86
+ return self.project_editor.set_current_action(index)
87
+
88
+ def job_list_count(self):
89
+ return self.project_editor.job_list_count()
90
+
91
+ def action_list_count(self):
92
+ return self.project_editor.action_list_count()
93
+
94
+ def job_list_item(self, index):
95
+ return self.project_editor.job_list_item(index)
96
+
97
+ def action_list_item(self, index):
98
+ return self.project_editor.action_list_item(index)
99
+
100
+ def job_list_has_focus(self):
101
+ return self.project_editor.job_list_has_focus()
102
+
103
+ def action_list_has_focus(self):
104
+ return self.project_editor.action_list_has_focus()
105
+
106
+ def clear_job_list(self):
107
+ self.project_editor.clear_job_list()
108
+
109
+ def clear_action_list(self):
110
+ self.project_editor.clear_action_list()
111
+
112
+ def num_selected_jobs(self):
113
+ return self.project_editor.num_selected_jobs()
114
+
115
+ def num_selected_actions(self):
116
+ return self.project_editor.num_selected_actions()
117
+
118
+ def get_current_action_at(self, job, action_index):
119
+ return self.project_editor.get_current_action_at(job, action_index)
120
+
121
+ def action_config_dialog(self, action):
122
+ return self.project_editor.action_config_dialog(action)
123
+
124
+ def on_job_selected(self, index):
125
+ return self.project_editor.on_job_selected(index)
20
126
 
21
127
  def update_title(self):
22
- title = constants.APP_TITLE
23
- file_name = self.current_file_name()
24
- if file_name:
25
- title += f" - {file_name}"
26
- if self._modified_project:
27
- title += " *"
28
- self.window().setWindowTitle(title)
29
-
30
- def mark_as_modified(self):
31
- self._modified_project = True
32
- self.project_buffer.append(self.project.clone())
33
- self.save_actions_set_enabled(True)
34
- self.update_title()
128
+ self.update_title_requested.emit()
35
129
 
36
130
  def close_project(self):
37
- if self._check_unsaved_changes():
131
+ if self.check_unsaved_changes():
38
132
  self.set_project(Project())
39
133
  self.set_current_file_path('')
40
134
  self.update_title()
41
- self.job_list.clear()
42
- self.action_list.clear()
43
- self._modified_project = False
44
- self.save_actions_set_enabled(False)
135
+ self.clear_job_list()
136
+ self.clear_action_list()
137
+ self.mark_as_modified(False)
45
138
 
46
139
  def new_project(self):
47
- if not self._check_unsaved_changes():
140
+ if not self.check_unsaved_changes():
48
141
  return
49
142
  os.chdir(get_app_base_path())
50
143
  self.set_current_file_path('')
51
- self._modified_project = False
144
+ self.mark_as_modified(False)
52
145
  self.update_title()
53
- self.job_list.clear()
54
- self.action_list.clear()
146
+ self.clear_job_list()
147
+ self.clear_action_list()
55
148
  self.set_project(Project())
56
149
  self.save_actions_set_enabled(False)
57
- dialog = NewProjectDialog(self)
150
+ dialog = NewProjectDialog(self.parent)
58
151
  if dialog.exec() == QDialog.Accepted:
59
152
  self.save_actions_set_enabled(True)
60
153
  input_folder = dialog.get_input_folder().split('/')
@@ -67,7 +160,7 @@ class ActionsWindow(ProjectEditor):
67
160
  noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
68
161
  {'name': 'detect-noise'})
69
162
  job_noise.add_sub_action(noise_detection)
70
- self.project.jobs.append(job_noise)
163
+ self.add_job_to_project(job_noise)
71
164
  job = ActionConfig(constants.ACTION_JOB,
72
165
  {'name': 'focus-stack', 'working_path': working_path,
73
166
  'input_path': input_path})
@@ -116,16 +209,16 @@ class ActionsWindow(ProjectEditor):
116
209
  {'name': 'multi-layer',
117
210
  'input_path': ','.join(input_path)})
118
211
  job.add_sub_action(multi_layer)
119
- self.project.jobs.append(job)
120
- self._modified_project = True
212
+ self.add_job_to_project(job)
213
+ self.mark_as_modified(True)
121
214
  self.refresh_ui(0, -1)
122
215
 
123
216
  def open_project(self, file_path=False):
124
- if not self._check_unsaved_changes():
217
+ if not self.check_unsaved_changes():
125
218
  return
126
219
  if file_path is False:
127
220
  file_path, _ = QFileDialog.getOpenFileName(
128
- self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
221
+ self.parent, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
129
222
  if file_path:
130
223
  try:
131
224
  self.set_current_file_path(file_path)
@@ -135,24 +228,24 @@ class ActionsWindow(ProjectEditor):
135
228
  if project is None:
136
229
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
137
230
  self.set_project(project)
138
- self._modified_project = False
139
- self.update_title()
231
+ self.mark_as_modified(False)
140
232
  self.refresh_ui(0, -1)
141
- if self.job_list.count() > 0:
142
- self.job_list.setCurrentRow(0)
233
+ if self.job_list_count() > 0:
234
+ self.set_current_job(0)
143
235
  except Exception as e:
144
- # traceback.print_tb(e.__traceback__)
145
- QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
146
- if len(self.project.jobs) > 0:
147
- self.job_list.setCurrentRow(0)
148
- self.activateWindow()
236
+ traceback.print_tb(e.__traceback__)
237
+ QMessageBox.critical(
238
+ self.parent, "Error", f"Cannot open file {file_path}:\n{str(e)}")
239
+ if self.num_project_jobs() > 0:
240
+ self.set_current_job(0)
241
+ self.activate_window_requested.emit()
149
242
  self.save_actions_set_enabled(True)
150
- for job in self.project.jobs:
243
+ for job in self.project_jobs():
151
244
  if 'working_path' in job.params.keys():
152
245
  working_path = job.params['working_path']
153
246
  if not os.path.isdir(working_path):
154
247
  QMessageBox.warning(
155
- self, "Working path not found",
248
+ self.parent, "Working path not found",
156
249
  f'''The working path specified in the project file for the job:
157
250
  "{job.params['name']}"
158
251
  was not found.\n
@@ -163,7 +256,7 @@ class ActionsWindow(ProjectEditor):
163
256
  working_path = job.params['working_path']
164
257
  if working_path != '' and not os.path.isdir(working_path):
165
258
  QMessageBox.warning(
166
- self, "Working path not found",
259
+ self.parent, "Working path not found",
167
260
  f'''The working path specified in the project file for the job:
168
261
  "{job.params['name']}"
169
262
  was not found.\n
@@ -179,32 +272,29 @@ class ActionsWindow(ProjectEditor):
179
272
 
180
273
  def save_project_as(self):
181
274
  file_path, _ = QFileDialog.getSaveFileName(
182
- self, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
275
+ self.parent, "Save Project As", "", "Project Files (*.fsp);;All Files (*)")
183
276
  if file_path:
184
277
  if not file_path.endswith('.fsp'):
185
278
  file_path += '.fsp'
186
279
  self.do_save(file_path)
187
280
  self.set_current_file_path(file_path)
188
- self._modified_project = False
189
- self.update_title()
190
281
  os.chdir(os.path.dirname(file_path))
191
282
 
192
283
  def do_save(self, file_path):
193
284
  try:
194
285
  json_obj = jsonpickle.encode({
195
- 'project': self.project.to_dict(),
196
- 'version': 1
286
+ 'project': self.project().to_dict(), 'version': 1
197
287
  })
198
288
  with open(file_path, 'w', encoding="utf-8") as f:
199
289
  f.write(json_obj)
200
- self._modified_project = False
290
+ self.mark_as_modified(False)
201
291
  except Exception as e:
202
- QMessageBox.critical(self, "Error", f"Cannot save file:\n{str(e)}")
292
+ QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
203
293
 
204
- def _check_unsaved_changes(self) -> bool:
205
- if self._modified_project:
294
+ def check_unsaved_changes(self) -> bool:
295
+ if self.modified():
206
296
  reply = QMessageBox.question(
207
- self, "Unsaved Changes",
297
+ self.parent, "Unsaved Changes",
208
298
  "The project has unsaved changes. Do you want to continue?",
209
299
  QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
210
300
  )
@@ -215,21 +305,21 @@ class ActionsWindow(ProjectEditor):
215
305
  return True
216
306
 
217
307
  def on_job_edit(self, item):
218
- index = self.job_list.row(item)
219
- if 0 <= index < len(self.project.jobs):
220
- job = self.project.jobs[index]
308
+ index = self.job_list().row(item)
309
+ if 0 <= index < self.num_project_jobs():
310
+ job = self.project_job(index)
221
311
  dialog = self.action_config_dialog(job)
222
312
  if dialog.exec() == QDialog.Accepted:
223
- current_row = self.job_list.currentRow()
313
+ current_row = self.current_job_index()
224
314
  if current_row >= 0:
225
- self.job_list.item(current_row).setText(job.params['name'])
315
+ self.job_list_item(current_row).setText(job.params['name'])
226
316
  self.refresh_ui()
227
317
 
228
318
  def on_action_edit(self, item):
229
- job_index = self.job_list.currentRow()
230
- if 0 <= job_index < len(self.project.jobs):
231
- job = self.project.jobs[job_index]
232
- action_index = self.action_list.row(item)
319
+ job_index = self.current_job_index()
320
+ if 0 <= job_index < self.num_project_jobs():
321
+ job = self.project_job(job_index)
322
+ action_index = self.action_list().row(item)
233
323
  current_action, is_sub_action = self.get_current_action_at(job, action_index)
234
324
  if current_action:
235
325
  if not is_sub_action:
@@ -239,17 +329,17 @@ class ActionsWindow(ProjectEditor):
239
329
  if dialog.exec() == QDialog.Accepted:
240
330
  self.on_job_selected(job_index)
241
331
  self.refresh_ui()
242
- self.job_list.setCurrentRow(job_index)
243
- self.action_list.setCurrentRow(action_index)
332
+ self.set_current_job(job_index)
333
+ self.set_current_action(action_index)
244
334
 
245
335
  def edit_current_action(self):
246
336
  current_action = None
247
- job_row = self.job_list.currentRow()
248
- if 0 <= job_row < len(self.project.jobs):
249
- job = self.project.jobs[job_row]
250
- if self.job_list.hasFocus():
337
+ job_row = self.current_job_index()
338
+ if 0 <= job_row < self.num_project_jobs():
339
+ job = self.project_job(job_row)
340
+ if self.job_list_has_focus():
251
341
  current_action = job
252
- elif self.action_list.hasFocus():
342
+ elif self.action_list_has_focus():
253
343
  job_row, _action_row, pos = self.get_current_action()
254
344
  if pos.actions is not None:
255
345
  current_action = pos.action if not pos.is_sub_action else pos.sub_action
@@ -259,8 +349,5 @@ class ActionsWindow(ProjectEditor):
259
349
  def edit_action(self, action):
260
350
  dialog = self.action_config_dialog(action)
261
351
  if dialog.exec() == QDialog.Accepted:
262
- self.on_job_selected(self.job_list.currentRow())
352
+ self.on_job_selected(self.current_job_index())
263
353
  self.mark_as_modified()
264
-
265
- def save_actions_set_enabled(self, enabled):
266
- pass
@@ -36,10 +36,10 @@ class ProjectConverter:
36
36
  logger.error(f"=== job: {job.name} failed: {msg} ===")
37
37
  return constants.RUN_FAILED, msg
38
38
 
39
- def run_project(self, project: Project, logger_name=None, callbacks=None):
39
+ def run_project(self, proj: Project, logger_name=None, callbacks=None):
40
40
  logger = self.get_logger(logger_name)
41
41
  try:
42
- jobs = self.project(project, logger_name, callbacks)
42
+ jobs = self.project(proj, logger_name, callbacks)
43
43
  except Exception as e:
44
44
  traceback.print_tb(e.__traceback__)
45
45
  return constants.RUN_FAILED, str(e)
@@ -60,12 +60,12 @@ class ProjectConverter:
60
60
  status = self.run(job, logger)
61
61
  return status
62
62
 
63
- def project(self, project: Project, logger_name=None, callbacks=None):
63
+ def project(self, proj: Project, logger_name=None, callbacks=None):
64
64
  jobs = []
65
- for j in project.jobs:
65
+ for j in proj.jobs:
66
66
  job = self.job(j, logger_name, callbacks)
67
67
  if job is None:
68
- raise RuntimeError("Job instantiation failed.")
68
+ raise RuntimeError("Job creation failed.")
69
69
  jobs.append(job)
70
70
  return jobs
71
71
 
@@ -143,6 +143,6 @@ class ProjectConverter:
143
143
  except Exception as e:
144
144
  msg = str(e)
145
145
  logger = self.get_logger(logger_name)
146
- logger.error(msg=f"=== can't instantiate job: {name}: {msg} ===")
146
+ logger.error(msg=f"=== can't create job: {name}: {msg} ===")
147
147
  traceback.print_tb(e.__traceback__)
148
148
  raise e