shinestacker 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (40) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +126 -94
  3. shinestacker/algorithms/align_auto.py +64 -0
  4. shinestacker/algorithms/align_parallel.py +296 -0
  5. shinestacker/algorithms/balance.py +3 -1
  6. shinestacker/algorithms/base_stack_algo.py +11 -2
  7. shinestacker/algorithms/multilayer.py +8 -8
  8. shinestacker/algorithms/noise_detection.py +10 -10
  9. shinestacker/algorithms/pyramid.py +4 -4
  10. shinestacker/algorithms/pyramid_auto.py +16 -10
  11. shinestacker/algorithms/pyramid_tiles.py +19 -11
  12. shinestacker/algorithms/stack.py +21 -17
  13. shinestacker/algorithms/stack_framework.py +97 -46
  14. shinestacker/algorithms/vignetting.py +13 -10
  15. shinestacker/app/main.py +7 -3
  16. shinestacker/config/constants.py +60 -25
  17. shinestacker/config/gui_constants.py +1 -1
  18. shinestacker/core/core_utils.py +4 -0
  19. shinestacker/core/framework.py +104 -23
  20. shinestacker/gui/action_config.py +4 -5
  21. shinestacker/gui/action_config_dialog.py +152 -12
  22. shinestacker/gui/base_form_dialog.py +2 -2
  23. shinestacker/gui/folder_file_selection.py +101 -0
  24. shinestacker/gui/gui_run.py +12 -10
  25. shinestacker/gui/main_window.py +6 -1
  26. shinestacker/gui/new_project.py +171 -73
  27. shinestacker/gui/project_controller.py +10 -6
  28. shinestacker/gui/project_converter.py +4 -2
  29. shinestacker/gui/project_editor.py +37 -27
  30. shinestacker/gui/select_path_widget.py +1 -1
  31. shinestacker/gui/sys_mon.py +96 -0
  32. shinestacker/gui/time_progress_bar.py +4 -3
  33. shinestacker/retouch/exif_data.py +1 -1
  34. shinestacker/retouch/image_editor_ui.py +2 -0
  35. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
  36. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/RECORD +40 -36
  37. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
  38. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
  39. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,15 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902, R0914, R0911, R0912, R0904
2
2
  import os
3
3
  import numpy as np
4
4
  from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QLabel, QCheckBox, QSpinBox,
5
- QMessageBox)
5
+ QMessageBox, QGroupBox, QVBoxLayout, QFormLayout, QSizePolicy)
6
6
  from PySide6.QtGui import QIcon
7
7
  from PySide6.QtCore import Qt
8
8
  from .. config.gui_constants import gui_constants
9
9
  from .. config.constants import constants
10
10
  from .. algorithms.utils import read_img, extension_tif_jpg
11
11
  from .. algorithms.stack import get_bunches
12
- from .select_path_widget import create_select_file_paths_widget
12
+ from .folder_file_selection import FolderFileSelectionWidget
13
13
  from .base_form_dialog import BaseFormDialog
14
14
 
15
15
  DEFAULT_NO_COUNT_LABEL = " - "
@@ -17,18 +17,18 @@ DEFAULT_NO_COUNT_LABEL = " - "
17
17
 
18
18
  class NewProjectDialog(BaseFormDialog):
19
19
  def __init__(self, parent=None):
20
- super().__init__("New Project", parent)
20
+ super().__init__("New Project", 600, parent)
21
21
  self.create_form()
22
22
  button_box = QHBoxLayout()
23
- ok_button = QPushButton("OK")
24
- ok_button.setFocus()
23
+ self.ok_button = QPushButton("OK")
25
24
  cancel_button = QPushButton("Cancel")
26
- button_box.addWidget(ok_button)
25
+ button_box.addWidget(self.ok_button)
27
26
  button_box.addWidget(cancel_button)
28
27
  self.add_row_to_layout(button_box)
29
- ok_button.clicked.connect(self.accept)
28
+ self.ok_button.clicked.connect(self.accept)
30
29
  cancel_button.clicked.connect(self.reject)
31
30
  self.n_image_files = 0
31
+ self.selected_filenames = []
32
32
 
33
33
  def expert(self):
34
34
  return self.parent().expert_options
@@ -43,20 +43,8 @@ class NewProjectDialog(BaseFormDialog):
43
43
  self.form_layout.addRow(label)
44
44
 
45
45
  def create_form(self):
46
- icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
47
- app_icon = QIcon(icon_path)
48
- icon_pixmap = app_icon.pixmap(128, 128)
49
- icon_label = QLabel()
50
- icon_label.setPixmap(icon_pixmap)
51
- icon_label.setAlignment(Qt.AlignCenter)
52
- self.form_layout.addRow(icon_label)
53
- spacer = QLabel("")
54
- spacer.setFixedHeight(10)
55
- self.form_layout.addRow(spacer)
56
-
57
- self.input_folder, container = create_select_file_paths_widget(
58
- '', 'input files folder', 'input files folder')
59
- self.input_folder.textChanged.connect(self.update_bunches_label)
46
+ self.input_widget = FolderFileSelectionWidget()
47
+ self.input_widget.text_changed_connect(self.input_submitted)
60
48
  self.noise_detection = QCheckBox()
61
49
  self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
62
50
  self.vignetting_correction = QCheckBox()
@@ -65,7 +53,6 @@ class NewProjectDialog(BaseFormDialog):
65
53
  self.align_frames.setChecked(gui_constants.NEW_PROJECT_ALIGN_FRAMES)
66
54
  self.balance_frames = QCheckBox()
67
55
  self.balance_frames.setChecked(gui_constants.NEW_PROJECT_BALANCE_FRAMES)
68
-
69
56
  self.bunch_stack = QCheckBox()
70
57
  self.bunch_stack.setChecked(gui_constants.NEW_PROJECT_BUNCH_STACK)
71
58
  self.bunch_frames = QSpinBox()
@@ -78,46 +65,121 @@ class NewProjectDialog(BaseFormDialog):
78
65
  self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
79
66
  self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
80
67
  self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
81
-
82
68
  self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
83
69
  self.bunch_stack.toggled.connect(self.update_bunch_options)
84
70
  self.bunch_frames.valueChanged.connect(self.update_bunches_label)
85
71
  self.bunch_overlap.valueChanged.connect(self.update_bunches_label)
86
-
87
72
  self.focus_stack_pyramid = QCheckBox()
88
73
  self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
89
74
  self.focus_stack_depth_map = QCheckBox()
90
75
  self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
91
76
  self.multi_layer = QCheckBox()
92
77
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
93
-
94
- self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
95
- self.form_layout.addRow("Input folder:", container)
96
- self.form_layout.addRow("Number of frames: ", self.frames_label)
97
- self.add_label("")
98
- self.add_bold_label("2️⃣ Select basic options.")
78
+ step1_group = QGroupBox("1️⃣ Select Input")
79
+ step1_layout = QVBoxLayout()
80
+ step1_layout.setContentsMargins(15, 0, 15, 15)
81
+ step1_layout.addWidget(
82
+ QLabel("Select a folder containing "
83
+ "all your images, or specific image files."))
84
+ input_form = QFormLayout()
85
+ input_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
86
+ input_form.setFormAlignment(Qt.AlignLeft)
87
+ input_form.setLabelAlignment(Qt.AlignLeft)
88
+ self.input_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
89
+ self.frames_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
90
+ input_form.addRow("Input:", self.input_widget)
91
+ input_form.addRow("Number of frames: ", self.frames_label)
92
+ step1_layout.addLayout(input_form)
93
+ step1_group.setLayout(step1_layout)
94
+ self.form_layout.addRow(step1_group)
95
+ step2_group = QGroupBox("2️⃣ Basic Options")
96
+ step2_layout = QFormLayout()
97
+ step2_layout.setContentsMargins(15, 0, 15, 15)
98
+ step2_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
99
+ step2_layout.setFormAlignment(Qt.AlignLeft)
100
+ step2_layout.setLabelAlignment(Qt.AlignLeft)
101
+ for widget in [self.noise_detection, self.vignetting_correction, self.align_frames,
102
+ self.balance_frames, self.bunch_stack, self.bunch_frames,
103
+ self.bunch_overlap, self.focus_stack_pyramid,
104
+ self.focus_stack_depth_map, self.multi_layer]:
105
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
99
106
  if self.expert():
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)
107
+ step2_layout.addRow("Automatic noise detection:", self.noise_detection)
108
+ step2_layout.addRow("Vignetting correction:", self.vignetting_correction)
109
+ step2_layout.addRow(
110
+ # f" {constants.ACTION_ICONS[constants.ACTION_ALIGNFRAMES]} "
111
+ "Align layers:", self.align_frames)
112
+ step2_layout.addRow(
113
+ # f" {constants.ACTION_ICONS[constants.ACTION_BALANCEFRAMES]} "
114
+ "Balance layers:", self.balance_frames)
115
+ step2_layout.addRow(
116
+ # f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACKBUNCH]} "
117
+ "Bunch stack:", self.bunch_stack)
118
+ step2_layout.addRow("Bunch frames:", self.bunch_frames)
119
+ step2_layout.addRow("Bunch overlap:", self.bunch_overlap)
120
+ self.bunches_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
121
+ step2_layout.addRow("Number of bunches: ", self.bunches_label)
108
122
  if self.expert():
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)
123
+ step2_layout.addRow(
124
+ f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
125
+ "Focus stack (pyramid):", self.focus_stack_pyramid)
126
+ step2_layout.addRow(
127
+ f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
128
+ "Focus stack (depth map):", self.focus_stack_depth_map)
111
129
  else:
112
- self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
130
+ step2_layout.addRow(
131
+ f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
132
+ "Focus stack:", self.focus_stack_pyramid)
113
133
  if self.expert():
114
- self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
115
- self.add_label("")
116
- self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
117
- self.add_label("")
118
- self.add_label("4️⃣ "
119
- "Select: <b>View</b> > <b>Expert options</b> "
120
- "to unlock advanced configuration.")
134
+ step2_layout.addRow(
135
+ f" {constants.ACTION_ICONS[constants.ACTION_MULTILAYER]} "
136
+ "Save multi layer TIFF:", self.multi_layer)
137
+ step2_group.setLayout(step2_layout)
138
+ self.form_layout.addRow(step2_group)
139
+ step3_group = QGroupBox("3️⃣ Confirmation")
140
+ step3_layout = QVBoxLayout()
141
+ step3_layout.setContentsMargins(15, 0, 15, 15)
142
+ step3_layout.addWidget(
143
+ QLabel("Click 🆗 to confirm and prepare the job."))
144
+ step3_layout.addWidget(
145
+ QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
146
+ step3_group.setLayout(step3_layout)
147
+ self.form_layout.addRow(step3_group)
148
+ step4_group = QGroupBox("4️⃣ Execution")
149
+ step4_layout = QHBoxLayout()
150
+ step4_layout.setContentsMargins(15, 0, 15, 15)
151
+ step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
152
+ icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
153
+ app_icon = QIcon(icon_path)
154
+ icon_pixmap = app_icon.pixmap(80, 80)
155
+ icon_label = QLabel()
156
+ icon_label.setPixmap(icon_pixmap)
157
+ icon_label.setAlignment(Qt.AlignRight)
158
+ step4_layout.addWidget(icon_label)
159
+ step4_group.setLayout(step4_layout)
160
+ self.form_layout.addRow(step4_group)
161
+ group_style = """
162
+ QGroupBox {
163
+ font-weight: bold;
164
+ border: 2px solid #cccccc;
165
+ border-radius: 5px;
166
+ margin-top: 10px;
167
+ padding-top: 15px;
168
+ background-color: #f8f8f8;
169
+ }
170
+ QGroupBox::title {
171
+ subcontrol-origin: margin;
172
+ left: 10px;
173
+ padding: 0 5px 0 5px;
174
+ background-color: #f8f8f8;
175
+ }
176
+ """
177
+ for group in [step1_group, step2_group, step3_group, step4_group]:
178
+ group.setStyleSheet(group_style)
179
+ group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
180
+ self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
181
+ self.form_layout.setFormAlignment(Qt.AlignLeft)
182
+ self.form_layout.setLabelAlignment(Qt.AlignLeft)
121
183
 
122
184
  def update_bunch_options(self, checked):
123
185
  self.bunch_frames.setEnabled(checked)
@@ -125,7 +187,7 @@ class NewProjectDialog(BaseFormDialog):
125
187
  self.update_bunches_label()
126
188
 
127
189
  def update_bunches_label(self):
128
- if not self.input_folder.text():
190
+ if not self.input_widget.get_path():
129
191
  return
130
192
 
131
193
  def count_image_files(path):
@@ -136,8 +198,13 @@ class NewProjectDialog(BaseFormDialog):
136
198
  if extension_tif_jpg(filename):
137
199
  count += 1
138
200
  return count
139
-
140
- self.n_image_files = count_image_files(self.input_folder.text())
201
+ if self.input_widget.get_selection_mode() == 'files' and \
202
+ self.input_widget.get_selected_files():
203
+ self.n_image_files = len(self.input_widget.get_selected_files())
204
+ self.selected_filenames = self.input_widget.get_selected_filenames()
205
+ else:
206
+ self.n_image_files = count_image_files(self.input_widget.get_path())
207
+ self.selected_filenames = []
141
208
  if self.n_image_files == 0:
142
209
  self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
143
210
  self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
@@ -147,36 +214,58 @@ class NewProjectDialog(BaseFormDialog):
147
214
  bunches = get_bunches(list(range(self.n_image_files)),
148
215
  self.bunch_frames.value(),
149
216
  self.bunch_overlap.value())
150
- self.bunches_label.setText(f"{len(bunches)}")
217
+ self.bunches_label.setText(f"{max(1, len(bunches))}")
151
218
  else:
152
219
  self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
153
220
 
221
+ def input_submitted(self):
222
+ self.update_bunches_label()
223
+ self.ok_button.setFocus()
224
+
154
225
  def accept(self):
155
- input_folder = self.input_folder.text()
156
- if not input_folder:
157
- QMessageBox.warning(self, "Input Required", "Please select an input folder")
158
- return
159
- if not os.path.exists(input_folder):
160
- QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
226
+ input_path = self.input_widget.get_path()
227
+ selection_mode = self.input_widget.get_selection_mode()
228
+ selected_files = self.input_widget.get_selected_files()
229
+ if not input_path:
230
+ QMessageBox.warning(self, "Input Required", "Please select an input folder or files")
161
231
  return
162
- if not os.path.isdir(input_folder):
163
- QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
164
- return
165
- if len(input_folder.split('/')) < 2:
232
+ if selection_mode == 'files':
233
+ if not selected_files:
234
+ QMessageBox.warning(self, "Invalid Selection", "No files selected")
235
+ return
236
+ for file_path in selected_files:
237
+ if not os.path.exists(file_path):
238
+ QMessageBox.warning(self, "Invalid Path",
239
+ f"The file {file_path} does not exist")
240
+ return
241
+ else:
242
+ if not os.path.exists(input_path):
243
+ QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
244
+ return
245
+ if not os.path.isdir(input_path):
246
+ QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
247
+ return
248
+ parent_dir = os.path.dirname(input_path)
249
+ if not parent_dir:
250
+ parent_dir = input_path
251
+ if len(parent_dir.split('/')) < 2:
166
252
  QMessageBox.warning(self, "Invalid Path", "The path must have a parent folder")
167
253
  return
168
254
  if self.n_image_files > 0 and not self.bunch_stack.isChecked():
169
- path = self.input_folder.text()
170
- files = os.listdir(path)
171
- file_path = None
172
- for filename in files:
173
- full_path = os.path.join(path, filename)
174
- if extension_tif_jpg(full_path):
175
- file_path = full_path
176
- break
255
+ if selection_mode == 'files' and selected_files:
256
+ file_path = selected_files[0]
257
+ else:
258
+ path = self.input_widget.get_path()
259
+ files = os.listdir(path)
260
+ file_path = None
261
+ for filename in files:
262
+ full_path = os.path.join(path, filename)
263
+ if extension_tif_jpg(full_path):
264
+ file_path = full_path
265
+ break
177
266
  if file_path is None:
178
267
  QMessageBox.warning(
179
- self, "Invalid input", "Could not find images now in the selected path")
268
+ self, "Invalid input", "Could not find images in the selected path")
180
269
  return
181
270
  img = read_img(file_path)
182
271
  height, width = img.shape[:2]
@@ -217,7 +306,16 @@ class NewProjectDialog(BaseFormDialog):
217
306
  super().accept()
218
307
 
219
308
  def get_input_folder(self):
220
- return self.input_folder.text()
309
+ return self.input_widget.get_path()
310
+
311
+ def get_selected_files(self):
312
+ return self.input_widget.get_selected_files()
313
+
314
+ def get_selected_filenames(self):
315
+ return self.input_widget.get_selected_filenames()
316
+
317
+ def get_selection_mode(self):
318
+ return self.input_widget.get_selection_mode()
221
319
 
222
320
  def get_noise_detection(self):
223
321
  return self.noise_detection.isChecked()
@@ -143,7 +143,6 @@ class ProjectController(QObject):
143
143
  return
144
144
  os.chdir(get_app_base_path())
145
145
  self.set_current_file_path('')
146
- self.mark_as_modified(False)
147
146
  self.update_title()
148
147
  self.clear_job_list()
149
148
  self.clear_action_list()
@@ -156,6 +155,7 @@ class ProjectController(QObject):
156
155
  input_folder = dialog.get_input_folder().split('/')
157
156
  working_path = '/'.join(input_folder[:-1])
158
157
  input_path = input_folder[-1]
158
+ selected_filenames = dialog.get_selected_filenames()
159
159
  if dialog.get_noise_detection():
160
160
  job_noise = ActionConfig(
161
161
  constants.ACTION_JOB,
@@ -165,10 +165,14 @@ class ProjectController(QObject):
165
165
  {'name': f'{input_path}-detect-noise'})
166
166
  job_noise.add_sub_action(noise_detection)
167
167
  self.add_job_to_project(job_noise)
168
- job = ActionConfig(constants.ACTION_JOB,
169
- {'name': f'{input_path}-focus-stack',
170
- 'working_path': working_path,
171
- 'input_path': input_path})
168
+ job_params = {
169
+ 'name': f'{input_path}-focus-stack',
170
+ 'working_path': working_path,
171
+ 'input_path': input_path
172
+ }
173
+ if len(selected_filenames) > 0:
174
+ job_params['input_filepaths'] = selected_filenames
175
+ job = ActionConfig(constants.ACTION_JOB, job_params)
172
176
  preprocess_name = ''
173
177
  if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
174
178
  dialog.get_align_frames() or dialog.get_balance_frames():
@@ -227,7 +231,7 @@ class ProjectController(QObject):
227
231
  'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
228
232
  job.add_sub_action(multi_layer)
229
233
  self.add_job_to_project(job)
230
- self.mark_as_modified(True, "New Project")
234
+ self.project_editor.set_modified(True)
231
235
  self.refresh_ui(0, -1)
232
236
 
233
237
  def open_project(self, file_path=False):
@@ -6,7 +6,7 @@ from .. core.exceptions import InvalidOptionError, RunStopException
6
6
  from .. algorithms.stack_framework import StackJob, CombinedActions
7
7
  from .. algorithms.noise_detection import NoiseDetection, MaskNoise
8
8
  from .. algorithms.vignetting import Vignetting
9
- from .. algorithms.align import AlignFrames
9
+ from .. algorithms.align_auto import AlignFramesAuto
10
10
  from .. algorithms.balance import BalanceFrames
11
11
  from .. algorithms.stack import FocusStack, FocusStackBunch
12
12
  from .. algorithms.pyramid_auto import PyramidAutoStack
@@ -93,7 +93,7 @@ class ProjectConverter:
93
93
  return Vignetting(**params)
94
94
  if action_config.type_name == constants.ACTION_ALIGNFRAMES:
95
95
  params = {k: v for k, v in action_config.params.items() if k != 'name'}
96
- return AlignFrames(**params)
96
+ return AlignFramesAuto(**params)
97
97
  if action_config.type_name == constants.ACTION_BALANCEFRAMES:
98
98
  params = {k: v for k, v in action_config.params.items() if k != 'name'}
99
99
  if 'intensity_interval' in params.keys():
@@ -133,7 +133,9 @@ class ProjectConverter:
133
133
  enabled = action_config.params.get('enabled', True)
134
134
  working_path = action_config.params.get('working_path', '')
135
135
  input_path = action_config.params.get('input_path', '')
136
+ input_filepaths = action_config.params.get('input_filepaths', [])
136
137
  stack_job = StackJob(name, working_path, enabled=enabled, input_path=input_path,
138
+ input_filepaths=input_filepaths,
137
139
  logger_name=logger_name, callbacks=callbacks)
138
140
  for sub in action_config.sub_actions:
139
141
  action = self.action(sub)
@@ -1,9 +1,8 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, R1702, R0917, R0913, R0902, E0611, E1131, E1121
1
+ # pylint: disable=C0114, C0115, C0116, R0903, R0904, R1702, R0917, R0913, R0902, E0611, E1131, E1121
2
2
  import os
3
3
  from dataclasses import dataclass
4
- from PySide6.QtWidgets import (QListWidget, QMessageBox,
5
- QDialog, QListWidgetItem, QLabel)
6
- from PySide6.QtCore import Qt, QObject, Signal
4
+ from PySide6.QtWidgets import QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
5
+ from PySide6.QtCore import Qt, QObject, Signal, QEvent
7
6
  from .. config.constants import constants
8
7
  from .colors import ColorPalette
9
8
  from .action_config_dialog import ActionConfigDialog
@@ -101,6 +100,25 @@ class ProjectUndoManager(QObject):
101
100
  self.set_enabled_undo_action_requested.emit(False, '')
102
101
 
103
102
 
103
+ class HandCursorListWidget(QListWidget):
104
+ def __init__(self, parent=None):
105
+ super().__init__(parent)
106
+ self.setMouseTracking(True)
107
+ self.viewport().setMouseTracking(True)
108
+
109
+ def event(self, event):
110
+ if event.type() == QEvent.HoverMove:
111
+ pos = event.position().toPoint()
112
+ item = self.itemAt(pos)
113
+ if item:
114
+ self.viewport().setCursor(Qt.PointingHandCursor)
115
+ else:
116
+ self.viewport().setCursor(Qt.ArrowCursor)
117
+ elif event.type() == QEvent.Leave:
118
+ self.viewport().setCursor(Qt.ArrowCursor)
119
+ return super().event(event)
120
+
121
+
104
122
  class ProjectEditor(QObject):
105
123
  INDENT_SPACE = "&nbsp;&nbsp;&nbsp;↪&nbsp;&nbsp;&nbsp;"
106
124
  CLONE_POSTFIX = " (clone)"
@@ -117,8 +135,8 @@ class ProjectEditor(QObject):
117
135
  self._project = None
118
136
  self._copy_buffer = None
119
137
  self._current_file_path = ''
120
- self._job_list = QListWidget()
121
- self._action_list = QListWidget()
138
+ self._job_list = HandCursorListWidget()
139
+ self._action_list = HandCursorListWidget()
122
140
  self.dialog = None
123
141
 
124
142
  def reset_undo(self):
@@ -246,37 +264,29 @@ class ProjectEditor(QObject):
246
264
  txt = f"{job.params.get('name', '(job)')}"
247
265
  if html:
248
266
  txt = f"<b>{txt}</b>"
249
- in_path = get_action_input_path(job)
250
- return txt + (f" [⚙️ Job: 📁 {in_path[0]} → 📂 ...]" if long_name else "")
267
+ in_path = get_action_input_path(job)[0]
268
+ if os.path.isabs(in_path):
269
+ in_path = ".../" + os.path.basename(in_path)
270
+ ico = constants.ACTION_ICONS[constants.ACTION_JOB]
271
+ return txt + (f" [{ico}Job: 📁 {in_path} → 📂 ...]" if long_name else "")
251
272
 
252
273
  def action_text(self, action, is_sub_action=False, indent=True, long_name=False, html=False):
253
- icon_map = {
254
- constants.ACTION_COMBO: '⚡',
255
- constants.ACTION_NOISEDETECTION: '🌫',
256
- constants.ACTION_FOCUSSTACK: '🎯',
257
- constants.ACTION_FOCUSSTACKBUNCH: '🖇',
258
- constants.ACTION_MULTILAYER: '🎞️',
259
- constants.ACTION_MASKNOISE: '🎭',
260
- constants.ACTION_VIGNETTING: '⭕️',
261
- constants.ACTION_ALIGNFRAMES: '📐',
262
- constants.ACTION_BALANCEFRAMES: '🌈'
263
- }
264
- ico = icon_map.get(action.type_name, '')
274
+ ico = constants.ACTION_ICONS.get(action.type_name, '')
265
275
  if is_sub_action and indent:
266
276
  txt = self.INDENT_SPACE
267
- if ico == '':
268
- ico = '🟣'
269
277
  else:
270
278
  txt = ''
271
- if ico == '':
272
- ico = '🔵'
273
279
  if action.params.get('name', '') != '':
274
280
  txt += f"{action.params['name']}"
275
281
  if html:
276
282
  txt = f"<b>{txt}</b>"
277
- in_path, out_path = get_action_input_path(action), get_action_output_path(action)
278
- return f"{txt} [{ico} {action.type_name}" + \
279
- (f": 📁 <i>{in_path[0]}</i> → 📂 <i>{out_path[0]}</i>]"
283
+ in_path, out_path = get_action_input_path(action)[0], get_action_output_path(action)[0]
284
+ if os.path.isabs(in_path):
285
+ in_path = ".../" + os.path.basename(in_path)
286
+ if os.path.isabs(out_path):
287
+ out_path = ".../" + os.path.basename(out_path)
288
+ return f"{txt} [{ico}{action.type_name}" + \
289
+ (f": 📁 <i>{in_path}</i> → 📂 <i>{out_path}</i>]"
280
290
  if long_name and not is_sub_action else "]")
281
291
 
282
292
  def get_job_at(self, index):
@@ -29,4 +29,4 @@ def create_select_file_paths_widget(value, placeholder, tag):
29
29
  if path:
30
30
  edit.setText(path)
31
31
 
32
- return edit, create_layout_widget_and_connect(button, edit, browse)
32
+ return create_layout_widget_and_connect(button, edit, browse)
@@ -0,0 +1,96 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
2
+ import psutil
3
+ from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QProgressBar, QSizePolicy
4
+ from PySide6.QtCore import QTimer, Qt
5
+
6
+
7
+ class StatusBarSystemMonitor(QWidget):
8
+ def __init__(self, parent=None):
9
+ super().__init__(parent)
10
+ self.setup_ui()
11
+ self.setup_timer()
12
+ self.setFixedHeight(28)
13
+
14
+ def setup_ui(self):
15
+ bar_width = 100
16
+ bar_height = 20
17
+ layout = QHBoxLayout()
18
+ layout.setSpacing(10)
19
+ layout.setContentsMargins(0, 2, 0, 0)
20
+ layout.setAlignment(Qt.AlignLeft)
21
+ layout.setAlignment(Qt.AlignCenter)
22
+ cpu_widget = QWidget()
23
+ cpu_widget.setFixedSize(bar_width, bar_height)
24
+ self.cpu_bar = QProgressBar(cpu_widget)
25
+ self.cpu_bar.setRange(0, 100)
26
+ self.cpu_bar.setTextVisible(False)
27
+ self.cpu_bar.setGeometry(0, 0, bar_width, bar_height)
28
+ self.cpu_bar.setStyleSheet("""
29
+ QProgressBar {
30
+ border: 1px solid #cccccc;
31
+ border-radius: 5px;
32
+ background: #f0f0f0;
33
+ }
34
+ QProgressBar::chunk {
35
+ background-color: #3498db;
36
+ border-radius: 5px;
37
+ }
38
+ """)
39
+ self.cpu_label = QLabel("CPU: --%", cpu_widget)
40
+ self.cpu_label.setAlignment(Qt.AlignCenter)
41
+ self.cpu_label.setGeometry(0, 0, bar_width, bar_height)
42
+ self.cpu_label.setStyleSheet("""
43
+ QLabel {
44
+ color: #2c3e50;
45
+ font-weight: bold;
46
+ background: transparent;
47
+ font-size: 12px;
48
+ }
49
+ """)
50
+ mem_widget = QWidget()
51
+ mem_widget.setFixedSize(bar_width, bar_height)
52
+ self.mem_bar = QProgressBar(mem_widget)
53
+ self.mem_bar.setRange(0, 100)
54
+ self.mem_bar.setTextVisible(False)
55
+ self.mem_bar.setGeometry(0, 0, bar_width, bar_height)
56
+ self.mem_bar.setStyleSheet("""
57
+ QProgressBar {
58
+ border: 1px solid #cccccc;
59
+ border-radius: 5px;
60
+ background: #f0f0f0;
61
+ }
62
+ QProgressBar::chunk {
63
+ background-color: #2ecc71;
64
+ border-radius: 5px;
65
+ }
66
+ """)
67
+ self.mem_label = QLabel("MEM: --%", mem_widget)
68
+ self.mem_label.setAlignment(Qt.AlignCenter)
69
+ self.mem_label.setGeometry(0, 0, bar_width, bar_height)
70
+ self.mem_label.setStyleSheet("""
71
+ QLabel {
72
+ color: #2c3e50;
73
+ font-weight: bold;
74
+ background: transparent;
75
+ font-size: 12px;
76
+ }
77
+ """)
78
+ layout.addWidget(cpu_widget)
79
+ layout.addWidget(mem_widget)
80
+ layout.addStretch()
81
+ self.setLayout(layout)
82
+ self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
83
+
84
+ def setup_timer(self):
85
+ self.timer = QTimer()
86
+ self.timer.timeout.connect(self.update_stats)
87
+ self.timer.start(1000)
88
+
89
+ def update_stats(self):
90
+ cpu_percent = psutil.cpu_percent()
91
+ memory = psutil.virtual_memory()
92
+ mem_percent = memory.percent
93
+ self.cpu_bar.setValue(int(cpu_percent))
94
+ self.cpu_label.setText(f"CPU: {cpu_percent:.1f}%")
95
+ self.mem_bar.setValue(int(mem_percent))
96
+ self.mem_label.setText(f"MEM: {mem_percent:.1f}%")
@@ -39,13 +39,14 @@ class TimerProgressBar(QProgressBar):
39
39
  """)
40
40
 
41
41
  def time_str(self, secs):
42
- x = secs % 1
43
- ss = int(secs // 1)
42
+ xsecs = int(secs * 10)
43
+ x = xsecs % 10
44
+ ss = xsecs // 10
44
45
  s = ss % 60
45
46
  mm = ss // 60
46
47
  m = mm % 60
47
48
  h = mm // 60
48
- t_str = f"{s:02d}" + f"{x:.1f}s".lstrip('0')
49
+ t_str = f"{s:02d}.{x:1d}s"
49
50
  if m > 0:
50
51
  t_str = f"{m:02d}:{t_str}"
51
52
  if h > 0:
@@ -9,7 +9,7 @@ from .. gui.base_form_dialog import BaseFormDialog
9
9
 
10
10
  class ExifData(BaseFormDialog):
11
11
  def __init__(self, exif, parent=None):
12
- super().__init__("EXIF data", parent)
12
+ super().__init__("EXIF data", parent=parent)
13
13
  self.exif = exif
14
14
  self.create_form()
15
15
  button_container = QWidget()
@@ -658,6 +658,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
658
658
  def quit(self):
659
659
  if self.check_unsaved_changes():
660
660
  self.close()
661
+ return True
662
+ return False
661
663
 
662
664
  def undo(self):
663
665
  if self.undo_manager.undo(self.master_layer()):