shinestacker 1.2.0__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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +148 -115
- shinestacker/algorithms/align_auto.py +64 -0
- shinestacker/algorithms/align_parallel.py +296 -0
- shinestacker/algorithms/balance.py +14 -13
- shinestacker/algorithms/base_stack_algo.py +11 -2
- shinestacker/algorithms/multilayer.py +14 -15
- shinestacker/algorithms/noise_detection.py +13 -14
- shinestacker/algorithms/pyramid.py +4 -4
- shinestacker/algorithms/pyramid_auto.py +16 -10
- shinestacker/algorithms/pyramid_tiles.py +19 -11
- shinestacker/algorithms/stack.py +30 -26
- shinestacker/algorithms/stack_framework.py +200 -178
- shinestacker/algorithms/vignetting.py +16 -13
- shinestacker/app/main.py +7 -3
- shinestacker/config/constants.py +63 -26
- shinestacker/config/gui_constants.py +1 -1
- shinestacker/core/core_utils.py +4 -0
- shinestacker/core/framework.py +114 -33
- shinestacker/gui/action_config.py +57 -5
- shinestacker/gui/action_config_dialog.py +156 -17
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/folder_file_selection.py +101 -0
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +13 -11
- shinestacker/gui/main_window.py +10 -5
- shinestacker/gui/menu_manager.py +4 -0
- shinestacker/gui/new_project.py +171 -74
- shinestacker/gui/project_controller.py +13 -9
- shinestacker/gui/project_converter.py +4 -2
- shinestacker/gui/project_editor.py +72 -53
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +96 -0
- shinestacker/gui/tab_widget.py +3 -3
- shinestacker/gui/time_progress_bar.py +4 -3
- shinestacker/retouch/exif_data.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
|
|
2
|
-
# pylint: disable=E0606, W0718, R1702, W0102, W0221
|
|
2
|
+
# pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914
|
|
3
|
+
import os
|
|
3
4
|
import traceback
|
|
4
|
-
from
|
|
5
|
-
from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea,
|
|
5
|
+
from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QSizePolicy,
|
|
6
6
|
QMessageBox, QStackedWidget, QFormLayout, QDialog)
|
|
7
7
|
from PySide6.QtCore import Qt, QTimer
|
|
8
8
|
from .. config.constants import constants
|
|
@@ -12,8 +12,9 @@ from .base_form_dialog import create_form_layout
|
|
|
12
12
|
from . action_config import (
|
|
13
13
|
FieldBuilder, ActionConfigurator,
|
|
14
14
|
FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
15
|
-
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO
|
|
15
|
+
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
|
|
16
16
|
)
|
|
17
|
+
from .folder_file_selection import FolderFileSelectionWidget
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class ActionConfigDialog(QDialog):
|
|
@@ -101,12 +102,9 @@ class ActionConfigDialog(QDialog):
|
|
|
101
102
|
action_type, DefaultActionConfigurator)(self.expert(), self.current_wd)
|
|
102
103
|
|
|
103
104
|
def accept(self):
|
|
104
|
-
self.parent().project_editor.add_undo(self.parent().project().clone())
|
|
105
105
|
if self.configurator.update_params(self.action.params):
|
|
106
|
-
self.parent().mark_as_modified()
|
|
106
|
+
self.parent().mark_as_modified(True, "Modify Configuration")
|
|
107
107
|
super().accept()
|
|
108
|
-
else:
|
|
109
|
-
self.parent().project_editor.pop_undo()
|
|
110
108
|
|
|
111
109
|
def reset_to_defaults(self):
|
|
112
110
|
builder = self.configurator.get_builder()
|
|
@@ -125,7 +123,7 @@ class NoNameActionConfigurator(ActionConfigurator):
|
|
|
125
123
|
def get_builder(self):
|
|
126
124
|
return self.builder
|
|
127
125
|
|
|
128
|
-
def update_params(self, params
|
|
126
|
+
def update_params(self, params):
|
|
129
127
|
return self.builder.update_params(params)
|
|
130
128
|
|
|
131
129
|
def add_bold_label(self, label):
|
|
@@ -140,6 +138,24 @@ class NoNameActionConfigurator(ActionConfigurator):
|
|
|
140
138
|
required=False, add_to_layout=None, **kwargs):
|
|
141
139
|
return self.builder.add_field(tag, field_type, label, required, add_to_layout, **kwargs)
|
|
142
140
|
|
|
141
|
+
def labelled_widget(self, label, widget):
|
|
142
|
+
row = QWidget()
|
|
143
|
+
layout = QHBoxLayout()
|
|
144
|
+
layout.setContentsMargins(2, 2, 2, 2)
|
|
145
|
+
layout.setSpacing(8)
|
|
146
|
+
label_widget = QLabel(label)
|
|
147
|
+
label_widget.setFixedWidth(120)
|
|
148
|
+
label_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
|
|
149
|
+
layout.addWidget(label_widget)
|
|
150
|
+
layout.addWidget(widget)
|
|
151
|
+
layout.setStretch(0, 1)
|
|
152
|
+
layout.setStretch(1, 3)
|
|
153
|
+
row.setLayout(layout)
|
|
154
|
+
return row
|
|
155
|
+
|
|
156
|
+
def add_labelled_row(self, label, widget):
|
|
157
|
+
self.add_row(self.labelled_widget(label, widget))
|
|
158
|
+
|
|
143
159
|
|
|
144
160
|
class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
145
161
|
def create_form(self, layout, action, tag='Action'):
|
|
@@ -149,13 +165,98 @@ class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
|
149
165
|
|
|
150
166
|
|
|
151
167
|
class JobConfigurator(DefaultActionConfigurator):
|
|
168
|
+
def __init__(self, expert, current_wd):
|
|
169
|
+
super().__init__(expert, current_wd)
|
|
170
|
+
self.working_path_label = None
|
|
171
|
+
self.input_path_label = None
|
|
172
|
+
self.frames_label = None
|
|
173
|
+
self.input_widget = None
|
|
174
|
+
|
|
152
175
|
def create_form(self, layout, action):
|
|
153
176
|
super().create_form(layout, action, "Job")
|
|
154
|
-
self.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
self.input_widget = FolderFileSelectionWidget()
|
|
178
|
+
self.frames_label = QLabel("0")
|
|
179
|
+
working_path = action.params.get('working_path', '')
|
|
180
|
+
input_path = action.params.get('input_path', '')
|
|
181
|
+
input_filepaths = action.params.get('input_filepaths', [])
|
|
182
|
+
if isinstance(input_filepaths, str) and input_filepaths:
|
|
183
|
+
input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
|
|
184
|
+
self.working_path_label = QLabel(working_path or "Not set")
|
|
185
|
+
self.input_path_label = QLabel(input_path or "Not set")
|
|
186
|
+
has_existing_data = working_path or input_path or input_filepaths
|
|
187
|
+
if input_filepaths:
|
|
188
|
+
self.input_widget.files_mode_radio.setChecked(True)
|
|
189
|
+
self.input_widget.selected_files = input_filepaths
|
|
190
|
+
if input_filepaths:
|
|
191
|
+
parent_dir = os.path.dirname(input_filepaths[0])
|
|
192
|
+
self.input_widget.path_edit.setText(parent_dir)
|
|
193
|
+
elif input_path and working_path:
|
|
194
|
+
self.input_widget.folder_mode_radio.setChecked(True)
|
|
195
|
+
input_fullpath = os.path.join(working_path, input_path)
|
|
196
|
+
self.input_widget.path_edit.setText(input_fullpath)
|
|
197
|
+
elif input_path:
|
|
198
|
+
self.input_widget.folder_mode_radio.setChecked(True)
|
|
199
|
+
self.input_widget.path_edit.setText(input_path)
|
|
200
|
+
self.input_widget.text_changed_connect(self.update_paths_and_frames)
|
|
201
|
+
self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
|
|
202
|
+
self.input_widget.files_mode_radio.toggled.connect(self.update_paths_and_frames)
|
|
203
|
+
self.add_bold_label("Input Selection:")
|
|
204
|
+
self.add_row(self.input_widget)
|
|
205
|
+
self.add_labelled_row("Number of frames: ", self.frames_label)
|
|
206
|
+
self.add_bold_label("Derived Paths:")
|
|
207
|
+
self.add_labelled_row("Working path: ", self.working_path_label)
|
|
208
|
+
self.add_labelled_row("Input path:", self.input_path_label)
|
|
209
|
+
if not has_existing_data:
|
|
210
|
+
self.update_paths_and_frames()
|
|
211
|
+
else:
|
|
212
|
+
self.update_frames_count()
|
|
213
|
+
|
|
214
|
+
def update_frames_count(self):
|
|
215
|
+
if self.input_widget.get_selection_mode() == 'files':
|
|
216
|
+
count = len(self.input_widget.get_selected_files())
|
|
217
|
+
else:
|
|
218
|
+
count = self.count_image_files(self.input_widget.get_path())
|
|
219
|
+
self.frames_label.setText(str(count))
|
|
220
|
+
|
|
221
|
+
def update_paths_and_frames(self):
|
|
222
|
+
self.update_frames_count()
|
|
223
|
+
selection_mode = self.input_widget.get_selection_mode()
|
|
224
|
+
selected_files = self.input_widget.get_selected_files()
|
|
225
|
+
input_path = self.input_widget.get_path()
|
|
226
|
+
if selection_mode == 'files' and selected_files:
|
|
227
|
+
input_path = os.path.dirname(selected_files[0])
|
|
228
|
+
input_path_value = os.path.basename(os.path.normpath(input_path)) if input_path else ""
|
|
229
|
+
working_path_value = os.path.dirname(input_path) if input_path else ""
|
|
230
|
+
self.input_path_label.setText(input_path_value or "Not set")
|
|
231
|
+
self.working_path_label.setText(working_path_value or "Not set")
|
|
232
|
+
|
|
233
|
+
def count_image_files(self, path):
|
|
234
|
+
if not path or not os.path.isdir(path):
|
|
235
|
+
return 0
|
|
236
|
+
count = 0
|
|
237
|
+
for filename in os.listdir(path):
|
|
238
|
+
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
|
|
239
|
+
count += 1
|
|
240
|
+
return count
|
|
241
|
+
|
|
242
|
+
def update_params(self, params):
|
|
243
|
+
if not super().update_params(params):
|
|
244
|
+
return False
|
|
245
|
+
selection_mode = self.input_widget.get_selection_mode()
|
|
246
|
+
selected_files = self.input_widget.get_selected_files()
|
|
247
|
+
input_path = self.input_widget.get_path()
|
|
248
|
+
if selection_mode == 'files' and selected_files:
|
|
249
|
+
params['input_filepaths'] = self.input_widget.get_selected_filenames()
|
|
250
|
+
params['input_path'] = os.path.dirname(selected_files[0])
|
|
251
|
+
else:
|
|
252
|
+
params['input_filepaths'] = []
|
|
253
|
+
params['input_path'] = input_path
|
|
254
|
+
if 'working_path' not in params or not params['working_path']:
|
|
255
|
+
if params['input_path']:
|
|
256
|
+
params['working_path'] = os.path.dirname(params['input_path'])
|
|
257
|
+
else:
|
|
258
|
+
params['working_path'] = ''
|
|
259
|
+
return True
|
|
159
260
|
|
|
160
261
|
|
|
161
262
|
class NoiseDetectionConfigurator(DefaultActionConfigurator):
|
|
@@ -431,11 +532,18 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
431
532
|
'resample', FIELD_INT, 'Resample frame stack', required=False,
|
|
432
533
|
default=1, min_val=1, max_val=100)
|
|
433
534
|
self.add_field(
|
|
434
|
-
'
|
|
435
|
-
default
|
|
535
|
+
'reference_index', FIELD_REF_IDX, 'Reference frame', required=False,
|
|
536
|
+
default=0)
|
|
436
537
|
self.add_field(
|
|
437
538
|
'step_process', FIELD_BOOL, 'Step process', required=False,
|
|
438
539
|
default=True)
|
|
540
|
+
self.add_field(
|
|
541
|
+
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
542
|
+
required=False, default=constants.DEFAULT_MAX_FWK_THREADS,
|
|
543
|
+
min_val=1, max_val=64)
|
|
544
|
+
self.add_field(
|
|
545
|
+
'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
546
|
+
required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
|
|
439
547
|
|
|
440
548
|
|
|
441
549
|
class MaskNoiseConfigurator(DefaultActionConfigurator):
|
|
@@ -485,6 +593,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
485
593
|
TRANSFORM_OPTIONS = ['Rigid', 'Homography']
|
|
486
594
|
METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
|
|
487
595
|
MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
|
|
596
|
+
MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
|
|
488
597
|
|
|
489
598
|
def __init__(self, expert, current_wd):
|
|
490
599
|
super().__init__(expert, current_wd)
|
|
@@ -651,6 +760,36 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
651
760
|
default=constants.DEFAULT_BORDER_BLUR,
|
|
652
761
|
min_val=0, max_val=1000, step=1)
|
|
653
762
|
self.add_bold_label("Miscellanea:")
|
|
763
|
+
if self.expert:
|
|
764
|
+
mode = self.add_field(
|
|
765
|
+
'mode', FIELD_COMBO, 'Mode',
|
|
766
|
+
required=False, options=self.MODE_OPTIONS, values=constants.ALIGN_VALID_MODES,
|
|
767
|
+
default=dict(zip(constants.ALIGN_VALID_MODES,
|
|
768
|
+
self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
|
|
769
|
+
memory_limit = self.add_field(
|
|
770
|
+
'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
|
|
771
|
+
required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
|
|
772
|
+
min_val=1.0, max_val=64.0)
|
|
773
|
+
max_threads = self.add_field(
|
|
774
|
+
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
775
|
+
required=False, default=constants.DEFAULT_ALIGN_MAX_THREADS,
|
|
776
|
+
min_val=1, max_val=64)
|
|
777
|
+
chunk_submit = self.add_field(
|
|
778
|
+
'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
779
|
+
required=False, default=constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
|
|
780
|
+
bw_matching = self.add_field(
|
|
781
|
+
'bw_matching', FIELD_BOOL, 'Match using black & white',
|
|
782
|
+
required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
|
|
783
|
+
|
|
784
|
+
def change_mode():
|
|
785
|
+
text = mode.currentText()
|
|
786
|
+
enabled = text != self.MODE_OPTIONS[1]
|
|
787
|
+
memory_limit.setEnabled(enabled)
|
|
788
|
+
max_threads.setEnabled(enabled)
|
|
789
|
+
chunk_submit.setEnabled(enabled)
|
|
790
|
+
bw_matching.setEnabled(enabled)
|
|
791
|
+
|
|
792
|
+
mode.currentIndexChanged.connect(change_mode)
|
|
654
793
|
self.add_field(
|
|
655
794
|
'plot_summary', FIELD_BOOL, 'Plot summary',
|
|
656
795
|
required=False, default=False)
|
|
@@ -658,7 +797,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
658
797
|
'plot_matches', FIELD_BOOL, 'Plot matches',
|
|
659
798
|
required=False, default=False)
|
|
660
799
|
|
|
661
|
-
def update_params(self, params
|
|
800
|
+
def update_params(self, params):
|
|
662
801
|
if self.detector_field and self.descriptor_field and self.matching_method_field:
|
|
663
802
|
try:
|
|
664
803
|
detector = self.detector_field.currentText()
|
|
@@ -13,10 +13,10 @@ def create_form_layout(parent):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class BaseFormDialog(QDialog):
|
|
16
|
-
def __init__(self, title, parent=None):
|
|
16
|
+
def __init__(self, title, width=500, parent=None):
|
|
17
17
|
super().__init__(parent)
|
|
18
18
|
self.setWindowTitle(title)
|
|
19
|
-
self.resize(
|
|
19
|
+
self.resize(width, self.height())
|
|
20
20
|
self.form_layout = create_form_layout(self)
|
|
21
21
|
|
|
22
22
|
def add_row_to_layout(self, item):
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import os
|
|
3
|
+
from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
|
|
4
|
+
QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FolderFileSelectionWidget(QWidget):
|
|
9
|
+
def __init__(self, parent=None):
|
|
10
|
+
super().__init__(parent)
|
|
11
|
+
self.selection_mode = 'folder' # 'folder' or 'files'
|
|
12
|
+
self.selected_files = []
|
|
13
|
+
self.setup_ui()
|
|
14
|
+
|
|
15
|
+
def setup_ui(self):
|
|
16
|
+
self.mode_group = QButtonGroup(self)
|
|
17
|
+
self.folder_mode_radio = QRadioButton("Select Folder")
|
|
18
|
+
self.folder_mode_radio.setMaximumWidth(100)
|
|
19
|
+
self.files_mode_radio = QRadioButton("Select Files")
|
|
20
|
+
self.files_mode_radio.setMaximumWidth(100)
|
|
21
|
+
self.folder_mode_radio.setChecked(True)
|
|
22
|
+
self.mode_group.addButton(self.folder_mode_radio)
|
|
23
|
+
self.mode_group.addButton(self.files_mode_radio)
|
|
24
|
+
self.path_edit = QLineEdit()
|
|
25
|
+
self.path_edit.setPlaceholderText("input files folder")
|
|
26
|
+
self.browse_button = QPushButton("Browse Folder...")
|
|
27
|
+
self.browse_button.setFixedWidth(120)
|
|
28
|
+
main_layout = QVBoxLayout()
|
|
29
|
+
main_layout.setSpacing(10)
|
|
30
|
+
main_layout.setAlignment(Qt.AlignLeft)
|
|
31
|
+
mode_layout = QHBoxLayout()
|
|
32
|
+
mode_layout.setContentsMargins(2, 2, 2, 2)
|
|
33
|
+
mode_layout.setSpacing(20)
|
|
34
|
+
mode_layout.addWidget(self.folder_mode_radio)
|
|
35
|
+
mode_layout.addWidget(self.files_mode_radio)
|
|
36
|
+
mode_layout.addStretch()
|
|
37
|
+
main_layout.addLayout(mode_layout)
|
|
38
|
+
input_layout = QHBoxLayout()
|
|
39
|
+
input_layout.setContentsMargins(2, 2, 2, 2)
|
|
40
|
+
input_layout.setSpacing(8)
|
|
41
|
+
input_layout.setAlignment(Qt.AlignLeft)
|
|
42
|
+
input_layout.addWidget(self.path_edit)
|
|
43
|
+
input_layout.addWidget(self.browse_button)
|
|
44
|
+
main_layout.addLayout(input_layout)
|
|
45
|
+
self.setLayout(main_layout)
|
|
46
|
+
self.folder_mode_radio.toggled.connect(self.update_selection_mode)
|
|
47
|
+
self.files_mode_radio.toggled.connect(self.update_selection_mode)
|
|
48
|
+
self.browse_button.clicked.connect(self.handle_browse)
|
|
49
|
+
|
|
50
|
+
def update_selection_mode(self):
|
|
51
|
+
if self.folder_mode_radio.isChecked():
|
|
52
|
+
self.selection_mode = 'folder'
|
|
53
|
+
self.browse_button.setText("Browse Folder...")
|
|
54
|
+
# self.path_edit.setPlaceholderText("input files folder")
|
|
55
|
+
else:
|
|
56
|
+
self.selection_mode = 'files'
|
|
57
|
+
self.browse_button.setText("Browse Files...")
|
|
58
|
+
# self.path_edit.setPlaceholderText("input files")
|
|
59
|
+
|
|
60
|
+
def handle_browse(self):
|
|
61
|
+
if self.selection_mode == 'folder':
|
|
62
|
+
self.browse_folder()
|
|
63
|
+
else:
|
|
64
|
+
self.browse_files()
|
|
65
|
+
|
|
66
|
+
def browse_folder(self):
|
|
67
|
+
path = QFileDialog.getExistingDirectory(self, "Select Input Folder")
|
|
68
|
+
if path:
|
|
69
|
+
self.selected_files = []
|
|
70
|
+
self.path_edit.setText(path)
|
|
71
|
+
|
|
72
|
+
def browse_files(self):
|
|
73
|
+
files, _ = QFileDialog.getOpenFileNames(
|
|
74
|
+
self, "Select Input Files", "",
|
|
75
|
+
"Image files (*.png *.jpg *.jpeg *.tif *.tiff)"
|
|
76
|
+
)
|
|
77
|
+
if files:
|
|
78
|
+
parent_dir = os.path.dirname(files[0])
|
|
79
|
+
if all(os.path.dirname(f) == parent_dir for f in files):
|
|
80
|
+
self.selected_files = files
|
|
81
|
+
self.path_edit.setText(parent_dir)
|
|
82
|
+
else:
|
|
83
|
+
QMessageBox.warning(
|
|
84
|
+
self, "Invalid Selection",
|
|
85
|
+
"All files must be in the same directory."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_selection_mode(self):
|
|
89
|
+
return self.selection_mode
|
|
90
|
+
|
|
91
|
+
def get_selected_files(self):
|
|
92
|
+
return self.selected_files
|
|
93
|
+
|
|
94
|
+
def get_selected_filenames(self):
|
|
95
|
+
return [os.path.basename(file_path) for file_path in self.selected_files]
|
|
96
|
+
|
|
97
|
+
def get_path(self):
|
|
98
|
+
return self.path_edit.text()
|
|
99
|
+
|
|
100
|
+
def text_changed_connect(self, callback):
|
|
101
|
+
self.path_edit.textChanged.connect(callback)
|
shinestacker/gui/gui_images.py
CHANGED
|
@@ -67,13 +67,13 @@ class GuiImageView(QWidget):
|
|
|
67
67
|
super().__init__(parent)
|
|
68
68
|
self.file_path = file_path
|
|
69
69
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
self.
|
|
70
|
+
self.main_layout = QVBoxLayout()
|
|
71
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
72
|
+
self.main_layout.setSpacing(0)
|
|
73
73
|
self.image_label = QLabel()
|
|
74
74
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
75
|
-
self.
|
|
76
|
-
self.setLayout(self.
|
|
75
|
+
self.main_layout.addWidget(self.image_label)
|
|
76
|
+
self.setLayout(self.main_layout)
|
|
77
77
|
pixmap = QPixmap(file_path)
|
|
78
78
|
if pixmap:
|
|
79
79
|
scaled_pixmap = pixmap.scaledToWidth(
|
|
@@ -105,13 +105,13 @@ class GuiOpenApp(QWidget):
|
|
|
105
105
|
self.file_path = file_path
|
|
106
106
|
self.app = app
|
|
107
107
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
108
|
+
self.main_layout = QVBoxLayout()
|
|
109
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
110
|
+
self.main_layout.setSpacing(0)
|
|
111
111
|
self.image_label = QLabel()
|
|
112
112
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
113
|
-
self.
|
|
114
|
-
self.setLayout(self.
|
|
113
|
+
self.main_layout.addWidget(self.image_label)
|
|
114
|
+
self.setLayout(self.main_layout)
|
|
115
115
|
pixmap = QPixmap(file_path)
|
|
116
116
|
if pixmap:
|
|
117
117
|
scaled_pixmap = pixmap.scaledToWidth(
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -18,6 +18,7 @@ from .colors import (
|
|
|
18
18
|
ACTION_STOPPED_COLOR, ACTION_FAILED_COLOR)
|
|
19
19
|
from .time_progress_bar import TimerProgressBar
|
|
20
20
|
from .flow_layout import FlowLayout
|
|
21
|
+
from .sys_mon import StatusBarSystemMonitor
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ColorButton(QPushButton):
|
|
@@ -96,7 +97,8 @@ class RunWindow(QTextEditLogger):
|
|
|
96
97
|
self.right_area.setMaximumWidth(0)
|
|
97
98
|
self.image_area_widget.setFixedWidth(0)
|
|
98
99
|
layout.addLayout(output_layout)
|
|
99
|
-
|
|
100
|
+
self.system_monitor = StatusBarSystemMonitor(self)
|
|
101
|
+
self.status_bar.addPermanentWidget(self.system_monitor)
|
|
100
102
|
n_paths = len(self.retouch_paths) if self.retouch_paths else 0
|
|
101
103
|
if n_paths == 1:
|
|
102
104
|
self.retouch_widget = QPushButton(f"Retouch {self.retouch_paths[0][0]}")
|
|
@@ -284,15 +286,15 @@ class RunWorker(LogWorker):
|
|
|
284
286
|
self.id_str = id_str
|
|
285
287
|
self.status = constants.STATUS_RUNNING
|
|
286
288
|
self.callbacks = {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
constants.CALLBACK_BEFORE_ACTION: self.before_action,
|
|
290
|
+
constants.CALLBACK_AFTER_ACTION: self.after_action,
|
|
291
|
+
constants.CALLBACK_STEP_COUNTS: self.step_counts,
|
|
292
|
+
constants.CALLBACK_BEGIN_STEPS: self.begin_steps,
|
|
293
|
+
constants.CALLBACK_END_STEPS: self.end_steps,
|
|
294
|
+
constants.CALLBACK_AFTER_STEP: self.after_step,
|
|
295
|
+
constants.CALLBACK_CHECK_RUNNING: self.check_running,
|
|
296
|
+
constants.CALLBACK_SAVE_PLOT: self.save_plot,
|
|
297
|
+
constants.CALLBACK_OPEN_APP: self.open_app
|
|
296
298
|
}
|
|
297
299
|
self.tag = ""
|
|
298
300
|
|
|
@@ -328,7 +330,7 @@ class RunWorker(LogWorker):
|
|
|
328
330
|
self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
|
|
329
331
|
self.html_signal.emit(f'''
|
|
330
332
|
<div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
|
|
331
|
-
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-
|
|
333
|
+
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weight: bold;">{self.tag} begins</span>
|
|
332
334
|
</div>
|
|
333
335
|
''') # noqa
|
|
334
336
|
status, error_message = self.do_run()
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -43,7 +43,10 @@ class ProjectLogWorker(RunWorker):
|
|
|
43
43
|
|
|
44
44
|
LIST_STYLE_SHEET = f"""
|
|
45
45
|
QListWidget::item:selected {{
|
|
46
|
-
background-color: #{ColorPalette.LIGHT_BLUE.hex()}
|
|
46
|
+
background-color: #{ColorPalette.LIGHT_BLUE.hex()};
|
|
47
|
+
}}
|
|
48
|
+
QListWidget::item:hover {{
|
|
49
|
+
background-color: #F0F0F0;
|
|
47
50
|
}}
|
|
48
51
|
"""
|
|
49
52
|
|
|
@@ -136,7 +139,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
136
139
|
self.project_editor.enable_delete_action_signal.connect(
|
|
137
140
|
self.menu_manager.delete_element_action.setEnabled)
|
|
138
141
|
self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
|
|
139
|
-
self.menu_manager.
|
|
142
|
+
self.menu_manager.set_enabled_undo_action)
|
|
140
143
|
self.project_controller.update_title_requested.connect(self.update_title)
|
|
141
144
|
self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
|
|
142
145
|
self.project_controller.activate_window_requested.connect(self.activateWindow)
|
|
@@ -148,8 +151,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
148
151
|
def modified(self):
|
|
149
152
|
return self.project_editor.modified()
|
|
150
153
|
|
|
151
|
-
def mark_as_modified(self, modified=True):
|
|
152
|
-
self.project_editor.mark_as_modified(modified)
|
|
154
|
+
def mark_as_modified(self, modified=True, description=''):
|
|
155
|
+
self.project_editor.mark_as_modified(modified, description)
|
|
153
156
|
|
|
154
157
|
def set_project(self, project):
|
|
155
158
|
self.project_editor.set_project(project)
|
|
@@ -421,12 +424,14 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
421
424
|
for worker in self._workers:
|
|
422
425
|
worker.stop()
|
|
423
426
|
self.close()
|
|
427
|
+
return True
|
|
428
|
+
return False
|
|
424
429
|
|
|
425
430
|
def toggle_expert_options(self):
|
|
426
431
|
self.expert_options = self.menu_manager.expert_options_action.isChecked()
|
|
427
432
|
|
|
428
433
|
def set_expert_options(self):
|
|
429
|
-
self.expert_options_action.setChecked(True)
|
|
434
|
+
self.menu_manager.expert_options_action.setChecked(True)
|
|
430
435
|
self.expert_options = True
|
|
431
436
|
|
|
432
437
|
def before_thread_begins(self):
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -244,3 +244,7 @@ class MenuManager:
|
|
|
244
244
|
if not enabled:
|
|
245
245
|
tooltip += " (requires more tha one job)"
|
|
246
246
|
self.run_all_jobs_action.setToolTip(tooltip)
|
|
247
|
+
|
|
248
|
+
def set_enabled_undo_action(self, enabled, description):
|
|
249
|
+
self.undo_action.setEnabled(enabled)
|
|
250
|
+
self.undo_action.setText(f"&Undo {description}")
|