shinestacker 1.5.4__py3-none-any.whl → 1.6.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/multilayer.py +1 -1
- shinestacker/algorithms/stack.py +17 -9
- shinestacker/app/args_parser_opts.py +4 -0
- shinestacker/app/gui_utils.py +10 -2
- shinestacker/app/main.py +5 -2
- shinestacker/app/project.py +4 -2
- shinestacker/app/retouch.py +3 -1
- shinestacker/app/settings_dialog.py +171 -0
- shinestacker/config/app_config.py +30 -0
- shinestacker/config/constants.py +3 -0
- shinestacker/config/gui_constants.py +4 -2
- shinestacker/config/settings.py +110 -0
- shinestacker/gui/action_config.py +6 -5
- shinestacker/gui/action_config_dialog.py +17 -74
- shinestacker/gui/config_dialog.py +78 -0
- shinestacker/gui/main_window.py +6 -6
- shinestacker/gui/menu_manager.py +2 -0
- shinestacker/gui/new_project.py +2 -1
- shinestacker/gui/project_controller.py +8 -6
- shinestacker/gui/project_model.py +16 -1
- shinestacker/gui/recent_file_manager.py +3 -21
- shinestacker/retouch/display_manager.py +47 -5
- shinestacker/retouch/image_editor_ui.py +14 -1
- shinestacker/retouch/image_view_status.py +4 -1
- shinestacker/retouch/sidebyside_view.py +29 -13
- shinestacker/retouch/transformation_manager.py +0 -1
- shinestacker/retouch/undo_manager.py +1 -1
- shinestacker/retouch/view_strategy.py +14 -4
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/METADATA +1 -1
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/RECORD +35 -31
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/top_level.txt +0 -0
|
@@ -2,87 +2,29 @@
|
|
|
2
2
|
# pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
|
|
3
3
|
import os
|
|
4
4
|
import traceback
|
|
5
|
-
from PySide6.QtCore import
|
|
6
|
-
from PySide6.QtWidgets import
|
|
7
|
-
QStackedWidget, QFormLayout, QDialog)
|
|
5
|
+
from PySide6.QtCore import QTimer
|
|
6
|
+
from PySide6.QtWidgets import QWidget, QLabel, QMessageBox, QStackedWidget
|
|
8
7
|
from .. config.constants import constants
|
|
8
|
+
from .. config.app_config import AppConfig
|
|
9
9
|
from .. algorithms.align import validate_align_config
|
|
10
|
-
from .base_form_dialog import create_form_layout
|
|
11
10
|
from . action_config import (
|
|
12
11
|
DefaultActionConfigurator,
|
|
13
12
|
FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
14
13
|
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
|
|
15
14
|
)
|
|
16
15
|
from .folder_file_selection import FolderFileSelectionWidget
|
|
16
|
+
from .config_dialog import ConfigDialog
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class ActionConfigDialog(
|
|
19
|
+
class ActionConfigDialog(ConfigDialog):
|
|
20
20
|
def __init__(self, action, current_wd, parent=None):
|
|
21
|
-
super().__init__(parent)
|
|
22
|
-
self.setWindowTitle(f"Configure {action.type_name}")
|
|
23
|
-
self.form_layout = create_form_layout(self)
|
|
24
|
-
self.current_wd = current_wd
|
|
25
21
|
self.action = action
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
33
|
-
self.container_layout.setLabelAlignment(Qt.AlignLeft)
|
|
34
|
-
self.configurator = self.get_configurator(action.type_name)
|
|
35
|
-
self.configurator.create_form(self.container_layout, action)
|
|
36
|
-
scroll_area.setWidget(container_widget)
|
|
37
|
-
button_box = QHBoxLayout()
|
|
38
|
-
ok_button = QPushButton("OK")
|
|
39
|
-
ok_button.setFocus()
|
|
40
|
-
cancel_button = QPushButton("Cancel")
|
|
41
|
-
reset_button = QPushButton("Reset")
|
|
42
|
-
button_box.addWidget(ok_button)
|
|
43
|
-
button_box.addWidget(cancel_button)
|
|
44
|
-
button_box.addWidget(reset_button)
|
|
45
|
-
reset_button.clicked.connect(self.reset_to_defaults)
|
|
46
|
-
self.form_layout.addRow(scroll_area)
|
|
47
|
-
self.form_layout.addRow(button_box)
|
|
48
|
-
QTimer.singleShot(0, self.adjust_dialog_size)
|
|
49
|
-
ok_button.clicked.connect(self.accept)
|
|
50
|
-
cancel_button.clicked.connect(self.reject)
|
|
51
|
-
|
|
52
|
-
def adjust_dialog_size(self):
|
|
53
|
-
screen_geometry = self.screen().availableGeometry()
|
|
54
|
-
screen_height = screen_geometry.height()
|
|
55
|
-
screen_width = screen_geometry.width()
|
|
56
|
-
scroll_area = self.findChild(QScrollArea)
|
|
57
|
-
container_widget = scroll_area.widget()
|
|
58
|
-
container_size = container_widget.sizeHint()
|
|
59
|
-
container_height = container_size.height()
|
|
60
|
-
container_width = container_size.width()
|
|
61
|
-
button_row_height = 50 # Approx height of button row
|
|
62
|
-
margins_height = 40 # Approx. height of margins
|
|
63
|
-
total_height_needed = container_height + button_row_height + margins_height
|
|
64
|
-
if total_height_needed < screen_height * 0.8:
|
|
65
|
-
width = max(container_width + 40, 600)
|
|
66
|
-
height = total_height_needed
|
|
67
|
-
self.resize(width, height)
|
|
68
|
-
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
69
|
-
else:
|
|
70
|
-
max_height = int(screen_height * 0.9)
|
|
71
|
-
width = max(container_width + 40, 600)
|
|
72
|
-
width = min(width, int(screen_width * 0.9))
|
|
73
|
-
self.resize(width, max_height)
|
|
74
|
-
self.setMaximumHeight(max_height)
|
|
75
|
-
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
76
|
-
self.setMinimumHeight(min(max_height, 500))
|
|
77
|
-
self.setMinimumWidth(width)
|
|
78
|
-
self.center_on_screen()
|
|
79
|
-
|
|
80
|
-
def center_on_screen(self):
|
|
81
|
-
screen_geometry = self.screen().availableGeometry()
|
|
82
|
-
center_point = screen_geometry.center()
|
|
83
|
-
frame_geometry = self.frameGeometry()
|
|
84
|
-
frame_geometry.moveCenter(center_point)
|
|
85
|
-
self.move(frame_geometry.topLeft())
|
|
22
|
+
self.current_wd = current_wd
|
|
23
|
+
super().__init__(f"Configure {action.type_name}", parent)
|
|
24
|
+
|
|
25
|
+
def create_form_content(self):
|
|
26
|
+
self.configurator = self.get_configurator(self.action.type_name)
|
|
27
|
+
self.configurator.create_form(self.container_layout, self.action)
|
|
86
28
|
|
|
87
29
|
def get_configurator(self, action_type):
|
|
88
30
|
configurators = {
|
|
@@ -102,7 +44,8 @@ class ActionConfigDialog(QDialog):
|
|
|
102
44
|
|
|
103
45
|
def accept(self):
|
|
104
46
|
if self.configurator.update_params(self.action.params):
|
|
105
|
-
self.parent()
|
|
47
|
+
if hasattr(self.parent(), 'mark_as_modified'):
|
|
48
|
+
self.parent().mark_as_modified(True, "Modify Configuration")
|
|
106
49
|
super().accept()
|
|
107
50
|
|
|
108
51
|
def reset_to_defaults(self):
|
|
@@ -111,7 +54,7 @@ class ActionConfigDialog(QDialog):
|
|
|
111
54
|
builder.reset_to_defaults()
|
|
112
55
|
|
|
113
56
|
def expert(self):
|
|
114
|
-
return
|
|
57
|
+
return AppConfig.get('expert_options')
|
|
115
58
|
|
|
116
59
|
|
|
117
60
|
class JobConfigurator(DefaultActionConfigurator):
|
|
@@ -329,7 +272,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
329
272
|
max_threads = self.add_field_to_layout(
|
|
330
273
|
q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
|
|
331
274
|
expert=True,
|
|
332
|
-
required=False, default=
|
|
275
|
+
required=False, default=AppConfig.get('focus_stack_params')['max_threads'],
|
|
333
276
|
min_val=1, max_val=64)
|
|
334
277
|
tile_size = self.add_field_to_layout(
|
|
335
278
|
q_pyramid.layout(), 'pyramid_tile_size', FIELD_INT, 'Tile size (px)',
|
|
@@ -490,7 +433,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
490
433
|
default=True)
|
|
491
434
|
self.add_field(
|
|
492
435
|
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
493
|
-
required=False, default=
|
|
436
|
+
required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
|
|
494
437
|
expert=True,
|
|
495
438
|
min_val=1, max_val=64)
|
|
496
439
|
self.add_field(
|
|
@@ -757,7 +700,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
|
|
|
757
700
|
min_val=1.0, max_val=64.0)
|
|
758
701
|
max_threads = self.add_field_to_layout(
|
|
759
702
|
layout, 'max_threads', FIELD_INT, 'Max num. of cores',
|
|
760
|
-
required=False, default=
|
|
703
|
+
required=False, default=AppConfig.get('align_frames_params')['max_threads'],
|
|
761
704
|
min_val=1, max_val=64)
|
|
762
705
|
chunk_submit = self.add_field_to_layout(
|
|
763
706
|
layout, 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from PySide6.QtCore import Qt, QTimer
|
|
4
|
+
from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QScrollArea, QFormLayout, QDialog
|
|
5
|
+
from .base_form_dialog import create_form_layout
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigDialog(QDialog):
|
|
9
|
+
def __init__(self, title, parent=None):
|
|
10
|
+
super().__init__(parent)
|
|
11
|
+
self.setWindowTitle(title)
|
|
12
|
+
self.form_layout = create_form_layout(self)
|
|
13
|
+
scroll_area = QScrollArea()
|
|
14
|
+
scroll_area.setWidgetResizable(True)
|
|
15
|
+
container_widget = QWidget()
|
|
16
|
+
self.container_layout = QFormLayout(container_widget)
|
|
17
|
+
self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
18
|
+
self.container_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
19
|
+
self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
20
|
+
self.container_layout.setLabelAlignment(Qt.AlignLeft)
|
|
21
|
+
scroll_area.setWidget(container_widget)
|
|
22
|
+
button_box = QHBoxLayout()
|
|
23
|
+
self.ok_button = QPushButton("OK")
|
|
24
|
+
self.ok_button.setFocus()
|
|
25
|
+
self.cancel_button = QPushButton("Cancel")
|
|
26
|
+
self.reset_button = QPushButton("Reset")
|
|
27
|
+
button_box.addWidget(self.ok_button)
|
|
28
|
+
button_box.addWidget(self.cancel_button)
|
|
29
|
+
button_box.addWidget(self.reset_button)
|
|
30
|
+
self.reset_button.clicked.connect(self.reset_to_defaults)
|
|
31
|
+
self.ok_button.clicked.connect(self.accept)
|
|
32
|
+
self.cancel_button.clicked.connect(self.reject)
|
|
33
|
+
self.form_layout.addRow(scroll_area)
|
|
34
|
+
self.form_layout.addRow(button_box)
|
|
35
|
+
QTimer.singleShot(0, self.adjust_dialog_size)
|
|
36
|
+
self.create_form_content()
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def create_form_content(self):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def adjust_dialog_size(self):
|
|
43
|
+
screen_geometry = self.screen().availableGeometry()
|
|
44
|
+
screen_height = screen_geometry.height()
|
|
45
|
+
screen_width = screen_geometry.width()
|
|
46
|
+
scroll_area = self.findChild(QScrollArea)
|
|
47
|
+
container_widget = scroll_area.widget()
|
|
48
|
+
container_size = container_widget.sizeHint()
|
|
49
|
+
container_height = container_size.height()
|
|
50
|
+
container_width = container_size.width()
|
|
51
|
+
button_row_height = 50 # Approx height of button row
|
|
52
|
+
margins_height = 40 # Approx. height of margins
|
|
53
|
+
total_height_needed = container_height + button_row_height + margins_height
|
|
54
|
+
if total_height_needed < screen_height * 0.8:
|
|
55
|
+
width = max(container_width + 40, 600)
|
|
56
|
+
height = total_height_needed
|
|
57
|
+
self.resize(width, height)
|
|
58
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
59
|
+
else:
|
|
60
|
+
max_height = int(screen_height * 0.9)
|
|
61
|
+
width = max(container_width + 40, 600)
|
|
62
|
+
width = min(width, int(screen_width * 0.9))
|
|
63
|
+
self.resize(width, max_height)
|
|
64
|
+
self.setMaximumHeight(max_height)
|
|
65
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
66
|
+
self.setMinimumHeight(min(max_height, 500))
|
|
67
|
+
self.setMinimumWidth(width)
|
|
68
|
+
self.center_on_screen()
|
|
69
|
+
|
|
70
|
+
def center_on_screen(self):
|
|
71
|
+
screen_geometry = self.screen().availableGeometry()
|
|
72
|
+
center_point = screen_geometry.center()
|
|
73
|
+
frame_geometry = self.frameGeometry()
|
|
74
|
+
frame_geometry.moveCenter(center_point)
|
|
75
|
+
self.move(frame_geometry.topLeft())
|
|
76
|
+
|
|
77
|
+
def reset_to_defaults(self):
|
|
78
|
+
pass
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -7,6 +7,7 @@ from PySide6.QtGui import QGuiApplication, QAction, QIcon
|
|
|
7
7
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox,
|
|
8
8
|
QSplitter, QToolBar, QMenu, QMainWindow)
|
|
9
9
|
from .. config.constants import constants
|
|
10
|
+
from .. config.app_config import AppConfig
|
|
10
11
|
from .. core.core_utils import running_under_windows, running_under_macos
|
|
11
12
|
from .colors import ColorPalette
|
|
12
13
|
from .project_model import Project
|
|
@@ -86,7 +87,6 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
86
87
|
self._windows = []
|
|
87
88
|
self._workers = []
|
|
88
89
|
self.retouch_callback = None
|
|
89
|
-
self.expert_options = False
|
|
90
90
|
self.job_list().setStyleSheet(LIST_STYLE_SHEET)
|
|
91
91
|
self.action_list().setStyleSheet(LIST_STYLE_SHEET)
|
|
92
92
|
self.menu_manager.add_menus()
|
|
@@ -441,12 +441,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
441
441
|
return True
|
|
442
442
|
return False
|
|
443
443
|
|
|
444
|
-
def
|
|
445
|
-
self.
|
|
444
|
+
def handle_config(self):
|
|
445
|
+
self.menu_manager.expert_options_action.setChecked(
|
|
446
|
+
AppConfig.get('expert_options'))
|
|
446
447
|
|
|
447
|
-
def
|
|
448
|
-
self.menu_manager.expert_options_action.
|
|
449
|
-
self.expert_options = True
|
|
448
|
+
def toggle_expert_options(self):
|
|
449
|
+
AppConfig.set('expert_options', self.menu_manager.expert_options_action.isChecked())
|
|
450
450
|
|
|
451
451
|
def before_thread_begins(self):
|
|
452
452
|
self.menu_manager.run_job_action.setEnabled(False)
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -5,6 +5,7 @@ from PySide6.QtCore import Signal, QObject
|
|
|
5
5
|
from PySide6.QtGui import QAction, QIcon
|
|
6
6
|
from PySide6.QtWidgets import QMenu, QComboBox
|
|
7
7
|
from .. config.constants import constants
|
|
8
|
+
from .. config.app_config import AppConfig
|
|
8
9
|
from .recent_file_manager import RecentFileManager
|
|
9
10
|
|
|
10
11
|
|
|
@@ -131,6 +132,7 @@ class MenuManager(QObject):
|
|
|
131
132
|
menu = self.menubar.addMenu("&View")
|
|
132
133
|
self.expert_options_action = self.action("Expert Options")
|
|
133
134
|
self.expert_options_action.setCheckable(True)
|
|
135
|
+
self.expert_options_action.setChecked(AppConfig.get('expert_options'))
|
|
134
136
|
menu.addAction(self.expert_options_action)
|
|
135
137
|
|
|
136
138
|
def add_job_menu(self):
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -7,6 +7,7 @@ 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
|
+
from .. config.app_config import AppConfig
|
|
10
11
|
from .. algorithms.utils import read_img, extension_tif_jpg
|
|
11
12
|
from .. algorithms.stack import get_bunches
|
|
12
13
|
from .folder_file_selection import FolderFileSelectionWidget
|
|
@@ -31,7 +32,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
31
32
|
self.selected_filenames = []
|
|
32
33
|
|
|
33
34
|
def expert(self):
|
|
34
|
-
return
|
|
35
|
+
return AppConfig.get('expert_options')
|
|
35
36
|
|
|
36
37
|
def add_bold_label(self, label):
|
|
37
38
|
label = QLabel(label)
|
|
@@ -155,9 +155,9 @@ class ProjectController(QObject):
|
|
|
155
155
|
if dialog.exec() == QDialog.Accepted:
|
|
156
156
|
self.save_actions_set_enabled(True)
|
|
157
157
|
self.project_editor.reset_undo()
|
|
158
|
-
input_folder = dialog.get_input_folder()
|
|
159
|
-
working_path =
|
|
160
|
-
input_path = input_folder
|
|
158
|
+
input_folder = dialog.get_input_folder()
|
|
159
|
+
working_path = os.path.dirname(input_folder)
|
|
160
|
+
input_path = os.path.basename(input_folder)
|
|
161
161
|
selected_filenames = dialog.get_selected_filenames()
|
|
162
162
|
if dialog.get_noise_detection():
|
|
163
163
|
job_noise = ActionConfig(
|
|
@@ -210,13 +210,15 @@ class ProjectController(QObject):
|
|
|
210
210
|
focus_pyramid_name = f'{input_path}-focus-stack-pyramid'
|
|
211
211
|
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
212
212
|
{'name': focus_pyramid_name,
|
|
213
|
-
'stacker': constants.STACK_ALGO_PYRAMID
|
|
213
|
+
'stacker': constants.STACK_ALGO_PYRAMID,
|
|
214
|
+
'exif_path': input_path})
|
|
214
215
|
job.add_sub_action(focus_pyramid)
|
|
215
216
|
if dialog.get_focus_stack_depth_map():
|
|
216
217
|
focus_depth_map_name = f'{input_path}-focus-stack-depth-map'
|
|
217
218
|
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
218
219
|
{'name': focus_depth_map_name,
|
|
219
|
-
'stacker': constants.STACK_ALGO_DEPTH_MAP
|
|
220
|
+
'stacker': constants.STACK_ALGO_DEPTH_MAP,
|
|
221
|
+
'exif_path': input_path})
|
|
220
222
|
job.add_sub_action(focus_depth_map)
|
|
221
223
|
if dialog.get_multi_layer():
|
|
222
224
|
multi_input_path = []
|
|
@@ -231,7 +233,7 @@ class ProjectController(QObject):
|
|
|
231
233
|
multi_layer = ActionConfig(
|
|
232
234
|
constants.ACTION_MULTILAYER,
|
|
233
235
|
{'name': f'{input_path}-multi-layer',
|
|
234
|
-
|
|
236
|
+
'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
|
|
235
237
|
job.add_sub_action(multi_layer)
|
|
236
238
|
self.add_job_to_project(job)
|
|
237
239
|
self.project_editor.set_modified(True)
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, R0911
|
|
2
2
|
from copy import deepcopy
|
|
3
3
|
from .. config.constants import constants
|
|
4
|
+
from .. config.app_config import AppConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TYPE_NAME_APP_CONFIG_MAP = {
|
|
8
|
+
constants.ACTION_COMBO: 'combined_actions_params',
|
|
9
|
+
constants.ACTION_ALIGNFRAMES: 'align_frames_params',
|
|
10
|
+
constants.ACTION_FOCUSSTACK: 'focus_stack_params',
|
|
11
|
+
constants.ACTION_FOCUSSTACKBUNCH: 'focus_stack_bunch_params'
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
|
|
6
15
|
class ActionConfig:
|
|
7
16
|
def __init__(self, type_name: str, params=None, parent=None):
|
|
8
17
|
self.type_name = type_name
|
|
9
|
-
|
|
18
|
+
app_config_params_key = TYPE_NAME_APP_CONFIG_MAP.get(type_name, '')
|
|
19
|
+
if app_config_params_key != '':
|
|
20
|
+
self.params = AppConfig.get(app_config_params_key, {})
|
|
21
|
+
else:
|
|
22
|
+
self.params = {}
|
|
23
|
+
if params:
|
|
24
|
+
self.params = {**self.params, **params}
|
|
10
25
|
self.parent = parent
|
|
11
26
|
self.sub_actions: list[ActionConfig] = []
|
|
12
27
|
|
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from .. config.settings import StdPathFile
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class RecentFileManager:
|
|
6
|
+
class RecentFileManager(StdPathFile):
|
|
7
7
|
def __init__(self, filename, max_entries=10):
|
|
8
|
-
|
|
8
|
+
super().__init__(filename)
|
|
9
9
|
self.max_entries = max_entries
|
|
10
|
-
self._config_dir = None
|
|
11
|
-
|
|
12
|
-
def get_config_dir(self):
|
|
13
|
-
if self._config_dir is None:
|
|
14
|
-
config_dir = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)
|
|
15
|
-
if not config_dir:
|
|
16
|
-
if os.name == 'nt': # Windows
|
|
17
|
-
config_dir = os.path.join(os.environ.get('APPDATA', ''), 'ShineStacker')
|
|
18
|
-
elif os.name == 'posix': # macOS and Linux
|
|
19
|
-
config_dir = os.path.expanduser('~/.config/shinestacker')
|
|
20
|
-
else:
|
|
21
|
-
config_dir = os.path.join(os.path.expanduser('~'), '.shinestacker')
|
|
22
|
-
os.makedirs(config_dir, exist_ok=True)
|
|
23
|
-
self._config_dir = config_dir
|
|
24
|
-
return self._config_dir
|
|
25
|
-
|
|
26
|
-
def get_file_path(self):
|
|
27
|
-
return os.path.join(self.get_config_dir(), self.filename)
|
|
28
10
|
|
|
29
11
|
def get_files(self):
|
|
30
12
|
file_path = self.get_file_path()
|
|
@@ -3,8 +3,9 @@ import numpy as np
|
|
|
3
3
|
from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
|
|
4
4
|
QAbstractItemView)
|
|
5
5
|
from PySide6.QtGui import QPixmap, QImage
|
|
6
|
-
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
|
|
6
|
+
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal, QThread
|
|
7
7
|
from .. config.gui_constants import gui_constants
|
|
8
|
+
from .. config.app_config import AppConfig
|
|
8
9
|
from .layer_collection import LayerCollectionHandler
|
|
9
10
|
|
|
10
11
|
|
|
@@ -23,6 +24,32 @@ class ClickableLabel(QLabel):
|
|
|
23
24
|
# pylint: enable=C0103
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
class ThumbnailWorker(QThread):
|
|
28
|
+
thumbnail_ready = Signal(object)
|
|
29
|
+
|
|
30
|
+
def __init__(self, layer_data):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.layer_data = layer_data
|
|
33
|
+
|
|
34
|
+
def run(self):
|
|
35
|
+
if self.layer_data is None:
|
|
36
|
+
self.thumbnail_ready.emit(None)
|
|
37
|
+
return
|
|
38
|
+
source_layer = (self.layer_data // 256).astype(np.uint8) \
|
|
39
|
+
if self.layer_data.dtype == np.uint16 else self.layer_data
|
|
40
|
+
if not source_layer.flags.c_contiguous:
|
|
41
|
+
source_layer = np.ascontiguousarray(source_layer)
|
|
42
|
+
height, width = source_layer.shape[:2]
|
|
43
|
+
if self.layer_data.ndim == 3 and source_layer.shape[-1] == 3:
|
|
44
|
+
qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
45
|
+
else:
|
|
46
|
+
qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
47
|
+
thumbnail = QPixmap.fromImage(
|
|
48
|
+
qimg.scaledToWidth(
|
|
49
|
+
gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
|
|
50
|
+
self.thumbnail_ready.emit(thumbnail)
|
|
51
|
+
|
|
52
|
+
|
|
26
53
|
class DisplayManager(QObject, LayerCollectionHandler):
|
|
27
54
|
status_message_requested = Signal(str)
|
|
28
55
|
|
|
@@ -36,8 +63,10 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
36
63
|
self.view_mode = 'master'
|
|
37
64
|
self.needs_update = False
|
|
38
65
|
self.update_timer = QTimer()
|
|
39
|
-
self.update_timer.setInterval(
|
|
66
|
+
self.update_timer.setInterval(AppConfig.get('display_refresh_time'))
|
|
40
67
|
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
68
|
+
self.thumbnail_worker = None
|
|
69
|
+
self.thumbnail_update_pending = False
|
|
41
70
|
|
|
42
71
|
def process_pending_updates(self):
|
|
43
72
|
if self.needs_update:
|
|
@@ -77,7 +106,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
77
106
|
if self.has_no_master_layer():
|
|
78
107
|
self._clear_master_thumbnail()
|
|
79
108
|
else:
|
|
80
|
-
self.
|
|
109
|
+
self._start_async_thumbnail_generation()
|
|
110
|
+
|
|
111
|
+
def _start_async_thumbnail_generation(self):
|
|
112
|
+
if self.thumbnail_worker and self.thumbnail_worker.isRunning():
|
|
113
|
+
self.thumbnail_update_pending = True
|
|
114
|
+
return
|
|
115
|
+
self.thumbnail_worker = ThumbnailWorker(self.master_layer())
|
|
116
|
+
self.thumbnail_worker.thumbnail_ready.connect(self._on_thumbnail_ready)
|
|
117
|
+
self.thumbnail_worker.start()
|
|
118
|
+
|
|
119
|
+
def _on_thumbnail_ready(self, thumbnail):
|
|
120
|
+
if thumbnail is not None:
|
|
121
|
+
self._set_master_thumbnail(thumbnail)
|
|
122
|
+
if self.thumbnail_update_pending:
|
|
123
|
+
self.thumbnail_update_pending = False
|
|
124
|
+
self._start_async_thumbnail_generation()
|
|
81
125
|
|
|
82
126
|
def _clear_master_thumbnail(self):
|
|
83
127
|
self.master_thumbnail_label.clear()
|
|
@@ -172,14 +216,12 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
172
216
|
if self.has_no_master_layer():
|
|
173
217
|
return
|
|
174
218
|
self.image_viewer.update_master_display()
|
|
175
|
-
self.image_viewer.refresh_display()
|
|
176
219
|
self.update_master_thumbnail()
|
|
177
220
|
|
|
178
221
|
def refresh_current_view(self):
|
|
179
222
|
if self.number_of_layers() == 0:
|
|
180
223
|
return
|
|
181
224
|
self.image_viewer.update_current_display()
|
|
182
|
-
self.image_viewer.refresh_display()
|
|
183
225
|
|
|
184
226
|
def start_temp_view(self):
|
|
185
227
|
if self.view_mode == 'master':
|
|
@@ -6,6 +6,7 @@ from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
|
|
|
6
6
|
from PySide6.QtCore import Qt
|
|
7
7
|
from PySide6.QtGui import QGuiApplication
|
|
8
8
|
from .. config.constants import constants
|
|
9
|
+
from .. config.app_config import AppConfig
|
|
9
10
|
from .. config.gui_constants import gui_constants
|
|
10
11
|
from .. gui.recent_file_manager import RecentFileManager
|
|
11
12
|
from .image_viewer import ImageViewer
|
|
@@ -184,6 +185,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
184
185
|
def change_layer_item(item):
|
|
185
186
|
layer_idx = self.thumbnail_list.row(item)
|
|
186
187
|
self.change_layer(layer_idx)
|
|
188
|
+
self.display_manager.highlight_thumbnail(layer_idx)
|
|
187
189
|
|
|
188
190
|
self.thumbnail_list.itemClicked.connect(change_layer_item)
|
|
189
191
|
self.thumbnail_list.setStyleSheet("""
|
|
@@ -236,6 +238,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
236
238
|
self.flow_slider)
|
|
237
239
|
self.image_viewer.set_brush(self.brush_tool.brush)
|
|
238
240
|
self.image_viewer.set_preview_brush(self.brush_tool.brush)
|
|
241
|
+
self.image_viewer.status.set_zoom_factor_requested.connect(self.handle_set_zoom_factor)
|
|
239
242
|
self.brush_tool.update_brush_thumb()
|
|
240
243
|
self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
|
|
241
244
|
menubar = self.menuBar()
|
|
@@ -434,7 +437,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
434
437
|
view_menu.addAction(self.toggle_view_master_individual_action)
|
|
435
438
|
view_menu.addSeparator()
|
|
436
439
|
|
|
437
|
-
self.set_strategy('
|
|
440
|
+
self.set_strategy(AppConfig.get('view_strategy'))
|
|
438
441
|
|
|
439
442
|
sort_asc_action = QAction("Sort Layers A-Z", self)
|
|
440
443
|
sort_asc_action.setProperty("requires_file", True)
|
|
@@ -471,6 +474,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
471
474
|
help_menu.setObjectName("Help")
|
|
472
475
|
shortcuts_help_action = QAction("Shortcuts and Mouse", self)
|
|
473
476
|
|
|
477
|
+
self.zoom_factor_label = QLabel("")
|
|
478
|
+
self.statusBar().addPermanentWidget(self.zoom_factor_label)
|
|
474
479
|
self.statusBar().showMessage("Shine Stacker ready.", 2000)
|
|
475
480
|
|
|
476
481
|
def shortcuts_help():
|
|
@@ -488,6 +493,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
488
493
|
self.set_enabled_file_open_close_actions(False)
|
|
489
494
|
self.installEventFilter(self)
|
|
490
495
|
|
|
496
|
+
def handle_config(self):
|
|
497
|
+
self.set_strategy(AppConfig.get('view_strategy'))
|
|
498
|
+
self.display_manager.update_timer.setInterval(AppConfig.get('display_refresh_time'))
|
|
499
|
+
|
|
491
500
|
def set_enabled_view_toggles(self, enabled):
|
|
492
501
|
self.view_master_action.setEnabled(enabled)
|
|
493
502
|
self.view_individual_action.setEnabled(enabled)
|
|
@@ -700,6 +709,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
700
709
|
self.io_gui_handler.close_file()
|
|
701
710
|
self.set_master_layer(None)
|
|
702
711
|
self.mark_as_modified(False)
|
|
712
|
+
self.zoom_factor_label.setText("")
|
|
703
713
|
|
|
704
714
|
def set_view_master(self):
|
|
705
715
|
self.display_manager.set_view_master()
|
|
@@ -752,3 +762,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
752
762
|
self.brush_tool.increase_brush_size()
|
|
753
763
|
else:
|
|
754
764
|
self.brush_tool.decrease_brush_size()
|
|
765
|
+
|
|
766
|
+
def handle_set_zoom_factor(self, zoom_factor):
|
|
767
|
+
self.zoom_factor_label.setText(f"zoom: {zoom_factor:.1%}")
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
2
|
-
from PySide6.QtCore import QObject, QRectF
|
|
2
|
+
from PySide6.QtCore import QObject, QRectF, Signal
|
|
3
3
|
from PySide6.QtGui import QPixmap
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ImageViewStatus(QObject):
|
|
7
|
+
set_zoom_factor_requested = Signal(float)
|
|
8
|
+
|
|
7
9
|
def __init__(self, parent=None):
|
|
8
10
|
super().__init__(parent)
|
|
9
11
|
self.pixmap_master = QPixmap()
|
|
@@ -53,6 +55,7 @@ class ImageViewStatus(QObject):
|
|
|
53
55
|
|
|
54
56
|
def set_zoom_factor(self, zoom_factor):
|
|
55
57
|
self.zoom_factor = zoom_factor
|
|
58
|
+
self.set_zoom_factor_requested.emit(zoom_factor)
|
|
56
59
|
|
|
57
60
|
def set_min_scale(self, min_scale):
|
|
58
61
|
self.min_scale = min_scale
|
|
@@ -61,6 +61,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
61
61
|
self.master_view.setFocusPolicy(Qt.NoFocus)
|
|
62
62
|
self.current_brush_cursor = None
|
|
63
63
|
self.last_color_update_time_current = 0
|
|
64
|
+
self._updating_scrollbars = False
|
|
64
65
|
|
|
65
66
|
def setup_layout(self):
|
|
66
67
|
raise NotImplementedError("Subclasses must implement setup_layout")
|
|
@@ -78,14 +79,25 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
78
79
|
self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
|
|
79
80
|
self.master_view.mouse_released.connect(self.handle_master_mouse_release)
|
|
80
81
|
self.master_view.gesture_event.connect(self.handle_gesture_event)
|
|
82
|
+
|
|
83
|
+
def sync_scrollbars(source, target):
|
|
84
|
+
if not self._updating_scrollbars:
|
|
85
|
+
self._updating_scrollbars = True
|
|
86
|
+
target.setValue(source.value())
|
|
87
|
+
self._updating_scrollbars = False
|
|
88
|
+
|
|
81
89
|
self.current_view.horizontalScrollBar().valueChanged.connect(
|
|
82
|
-
self.
|
|
90
|
+
lambda value: sync_scrollbars(self.current_view.horizontalScrollBar(),
|
|
91
|
+
self.master_view.horizontalScrollBar()))
|
|
83
92
|
self.current_view.verticalScrollBar().valueChanged.connect(
|
|
84
|
-
self.
|
|
93
|
+
lambda value: sync_scrollbars(self.current_view.verticalScrollBar(),
|
|
94
|
+
self.master_view.verticalScrollBar()))
|
|
85
95
|
self.master_view.horizontalScrollBar().valueChanged.connect(
|
|
86
|
-
self.
|
|
96
|
+
lambda value: sync_scrollbars(self.master_view.horizontalScrollBar(),
|
|
97
|
+
self.current_view.horizontalScrollBar()))
|
|
87
98
|
self.master_view.verticalScrollBar().valueChanged.connect(
|
|
88
|
-
self.
|
|
99
|
+
lambda value: sync_scrollbars(self.master_view.verticalScrollBar(),
|
|
100
|
+
self.current_view.verticalScrollBar()))
|
|
89
101
|
self.current_view.wheel_event.connect(self.handle_wheel_event)
|
|
90
102
|
self.master_view.wheel_event.connect(self.handle_wheel_event)
|
|
91
103
|
# pylint: disable=C0103, W0201
|
|
@@ -377,28 +389,32 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
377
389
|
def set_master_image(self, qimage):
|
|
378
390
|
self.status.set_master_image(qimage)
|
|
379
391
|
pixmap = self.status.pixmap_master
|
|
380
|
-
|
|
392
|
+
pixmap_rect = QRectF(pixmap.rect())
|
|
393
|
+
self.pixmap_item_master.setPos(0, 0)
|
|
394
|
+
self.master_view.setSceneRect(pixmap_rect)
|
|
395
|
+
self.master_scene.setSceneRect(pixmap_rect)
|
|
381
396
|
self.pixmap_item_master.setPixmap(pixmap)
|
|
382
|
-
|
|
397
|
+
_img_width, _img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
|
|
383
398
|
self.master_view.resetTransform()
|
|
384
399
|
self.master_view.scale(scale_factor, scale_factor)
|
|
385
400
|
self.master_view.centerOn(self.pixmap_item_master)
|
|
386
|
-
center = self.master_scene.sceneRect().center()
|
|
387
|
-
self.brush_preview.setPos(max(0, min(center.x(), img_width)),
|
|
388
|
-
max(0, min(center.y(), img_height)))
|
|
389
|
-
self.master_scene.setSceneRect(QRectF(self.pixmap_item_master.boundingRect()))
|
|
390
401
|
self.center_image(self.master_view)
|
|
391
402
|
self.update_cursor_pen_width()
|
|
392
403
|
|
|
393
404
|
def set_current_image(self, qimage):
|
|
394
405
|
self.status.set_current_image(qimage)
|
|
395
406
|
pixmap = self.status.pixmap_current
|
|
396
|
-
|
|
407
|
+
pixmap_rect = QRectF(pixmap.rect())
|
|
408
|
+
self.pixmap_item_current.setPos(0, 0)
|
|
409
|
+
self.current_view.setSceneRect(pixmap_rect)
|
|
410
|
+
self.current_scene.setSceneRect(pixmap_rect)
|
|
397
411
|
self.pixmap_item_current.setPixmap(pixmap)
|
|
412
|
+
_img_width, _img_height, scale_factor = self.setup_view_image(self.current_view, pixmap)
|
|
398
413
|
self.current_view.resetTransform()
|
|
399
|
-
self.
|
|
400
|
-
self.
|
|
414
|
+
self.current_view.scale(scale_factor, scale_factor)
|
|
415
|
+
self.current_view.centerOn(self.pixmap_item_current)
|
|
401
416
|
self.center_image(self.current_view)
|
|
417
|
+
self.update_cursor_pen_width()
|
|
402
418
|
|
|
403
419
|
def arrange_images(self):
|
|
404
420
|
if self.status.empty():
|
|
@@ -26,7 +26,6 @@ class TransfromationManager(LayerCollectionHandler):
|
|
|
26
26
|
self.copy_master_layer()
|
|
27
27
|
self.editor.image_viewer.update_master_display()
|
|
28
28
|
self.editor.image_viewer.update_current_display()
|
|
29
|
-
self.editor.image_viewer.refresh_display()
|
|
30
29
|
self.editor.display_manager.update_thumbnails()
|
|
31
30
|
self.editor.mark_as_modified()
|
|
32
31
|
|