shinestacker 1.5.4__py3-none-any.whl → 1.6.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.
- 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 +8 -3
- shinestacker/app/project.py +7 -3
- shinestacker/app/retouch.py +8 -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/core/core_utils.py +3 -12
- shinestacker/core/logging.py +3 -2
- 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/base_filter.py +1 -1
- shinestacker/retouch/display_manager.py +48 -7
- shinestacker/retouch/image_editor_ui.py +27 -36
- shinestacker/retouch/image_view_status.py +4 -1
- shinestacker/retouch/image_viewer.py +17 -9
- shinestacker/retouch/io_gui_handler.py +96 -44
- shinestacker/retouch/io_threads.py +78 -0
- shinestacker/retouch/layer_collection.py +12 -0
- shinestacker/retouch/overlaid_view.py +13 -5
- shinestacker/retouch/paint_area_manager.py +30 -0
- shinestacker/retouch/sidebyside_view.py +32 -16
- shinestacker/retouch/transformation_manager.py +1 -3
- shinestacker/retouch/undo_manager.py +15 -13
- shinestacker/retouch/view_strategy.py +79 -26
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/METADATA +1 -1
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/RECORD +44 -39
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.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()
|
|
@@ -157,7 +157,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
157
157
|
except Exception:
|
|
158
158
|
h, w = self.master_layer_copy().shape[:2]
|
|
159
159
|
try:
|
|
160
|
-
self.undo_manager.
|
|
160
|
+
self.undo_manager.set_paint_area(0, 0, w, h)
|
|
161
161
|
self.undo_manager.save_undo_state(
|
|
162
162
|
self.master_layer_copy(),
|
|
163
163
|
self.name
|
|
@@ -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()
|
|
@@ -171,15 +215,13 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
171
215
|
def refresh_master_view(self):
|
|
172
216
|
if self.has_no_master_layer():
|
|
173
217
|
return
|
|
174
|
-
self.image_viewer.
|
|
175
|
-
self.image_viewer.refresh_display()
|
|
218
|
+
self.image_viewer.update_master_display_area()
|
|
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':
|
|
@@ -187,7 +229,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
187
229
|
self.status_message_requested.emit("Temporary view: Individual layer.")
|
|
188
230
|
else:
|
|
189
231
|
self._master_refresh_and_thumb()
|
|
190
|
-
self.image_viewer.strategy.brush_preview.hide()
|
|
191
232
|
self.status_message_requested.emit("Temporary view: Master.")
|
|
192
233
|
|
|
193
234
|
def end_temp_view(self):
|
|
@@ -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
|
|
@@ -13,6 +14,7 @@ from .shortcuts_help import ShortcutsHelp
|
|
|
13
14
|
from .brush import Brush
|
|
14
15
|
from .brush_tool import BrushTool
|
|
15
16
|
from .layer_collection import LayerCollectionHandler
|
|
17
|
+
from .paint_area_manager import PaintAreaManager
|
|
16
18
|
from .undo_manager import UndoManager
|
|
17
19
|
from .layer_collection import LayerCollection
|
|
18
20
|
from .io_gui_handler import IOGuiHandler
|
|
@@ -34,9 +36,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
34
36
|
self.brush = Brush()
|
|
35
37
|
self.brush_tool = BrushTool()
|
|
36
38
|
self.modified = False
|
|
37
|
-
self.mask_layer = None
|
|
38
39
|
self.transformation_manager = TransfromationManager(self)
|
|
39
|
-
self.
|
|
40
|
+
self.paint_area_manager = PaintAreaManager()
|
|
41
|
+
self.undo_manager = UndoManager(self.transformation_manager, self.paint_area_manager)
|
|
40
42
|
self.undo_action = None
|
|
41
43
|
self.redo_action = None
|
|
42
44
|
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
@@ -48,13 +50,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
48
50
|
central_widget = QWidget()
|
|
49
51
|
self.setCentralWidget(central_widget)
|
|
50
52
|
layout = QHBoxLayout(central_widget)
|
|
51
|
-
self.image_viewer = ImageViewer(
|
|
53
|
+
self.image_viewer = ImageViewer(
|
|
54
|
+
self.layer_collection, self.brush_tool, self.paint_area_manager)
|
|
52
55
|
self.image_viewer.connect_signals(
|
|
53
56
|
self.handle_temp_view,
|
|
54
|
-
self.begin_copy_brush_area,
|
|
55
|
-
self.continue_copy_brush_area,
|
|
56
57
|
self.end_copy_brush_area,
|
|
57
|
-
self.handle_brush_size_change
|
|
58
|
+
self.handle_brush_size_change,
|
|
59
|
+
self.handle_needs_update)
|
|
58
60
|
side_panel = QWidget()
|
|
59
61
|
side_layout = QVBoxLayout(side_panel)
|
|
60
62
|
side_layout.setContentsMargins(0, 0, 0, 0)
|
|
@@ -184,6 +186,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
184
186
|
def change_layer_item(item):
|
|
185
187
|
layer_idx = self.thumbnail_list.row(item)
|
|
186
188
|
self.change_layer(layer_idx)
|
|
189
|
+
self.display_manager.highlight_thumbnail(layer_idx)
|
|
187
190
|
|
|
188
191
|
self.thumbnail_list.itemClicked.connect(change_layer_item)
|
|
189
192
|
self.thumbnail_list.setStyleSheet("""
|
|
@@ -236,6 +239,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
236
239
|
self.flow_slider)
|
|
237
240
|
self.image_viewer.set_brush(self.brush_tool.brush)
|
|
238
241
|
self.image_viewer.set_preview_brush(self.brush_tool.brush)
|
|
242
|
+
self.image_viewer.status.set_zoom_factor_requested.connect(self.handle_set_zoom_factor)
|
|
239
243
|
self.brush_tool.update_brush_thumb()
|
|
240
244
|
self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
|
|
241
245
|
menubar = self.menuBar()
|
|
@@ -434,7 +438,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
434
438
|
view_menu.addAction(self.toggle_view_master_individual_action)
|
|
435
439
|
view_menu.addSeparator()
|
|
436
440
|
|
|
437
|
-
self.set_strategy('
|
|
441
|
+
self.set_strategy(AppConfig.get('view_strategy'))
|
|
438
442
|
|
|
439
443
|
sort_asc_action = QAction("Sort Layers A-Z", self)
|
|
440
444
|
sort_asc_action.setProperty("requires_file", True)
|
|
@@ -471,6 +475,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
471
475
|
help_menu.setObjectName("Help")
|
|
472
476
|
shortcuts_help_action = QAction("Shortcuts and Mouse", self)
|
|
473
477
|
|
|
478
|
+
self.zoom_factor_label = QLabel("")
|
|
479
|
+
self.statusBar().addPermanentWidget(self.zoom_factor_label)
|
|
474
480
|
self.statusBar().showMessage("Shine Stacker ready.", 2000)
|
|
475
481
|
|
|
476
482
|
def shortcuts_help():
|
|
@@ -488,6 +494,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
488
494
|
self.set_enabled_file_open_close_actions(False)
|
|
489
495
|
self.installEventFilter(self)
|
|
490
496
|
|
|
497
|
+
def handle_config(self):
|
|
498
|
+
self.set_strategy(AppConfig.get('view_strategy'))
|
|
499
|
+
self.display_manager.update_timer.setInterval(AppConfig.get('display_refresh_time'))
|
|
500
|
+
|
|
491
501
|
def set_enabled_view_toggles(self, enabled):
|
|
492
502
|
self.view_master_action.setEnabled(enabled)
|
|
493
503
|
self.view_individual_action.setEnabled(enabled)
|
|
@@ -620,35 +630,11 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
620
630
|
self.mark_as_modified()
|
|
621
631
|
self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
|
|
622
632
|
|
|
623
|
-
def
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
self.master_layer_copy(),
|
|
629
|
-
self.current_layer(),
|
|
630
|
-
self.master_layer(), self.mask_layer,
|
|
631
|
-
view_pos)
|
|
632
|
-
self.undo_manager.extend_undo_area(*area)
|
|
633
|
-
|
|
634
|
-
def begin_copy_brush_area(self, pos):
|
|
635
|
-
if self.display_manager.view_mode == 'master':
|
|
636
|
-
self.mask_layer = self.io_gui_handler.blank_layer.copy()
|
|
637
|
-
self.copy_master_layer()
|
|
638
|
-
self.undo_manager.reset_undo_area()
|
|
639
|
-
self.copy_brush_area_to_master(pos)
|
|
640
|
-
self.display_manager.needs_update = True
|
|
641
|
-
if not self.display_manager.update_timer.isActive():
|
|
642
|
-
self.display_manager.update_timer.start()
|
|
643
|
-
self.mark_as_modified()
|
|
644
|
-
|
|
645
|
-
def continue_copy_brush_area(self, pos):
|
|
646
|
-
if self.display_manager.view_mode == 'master':
|
|
647
|
-
self.copy_brush_area_to_master(pos)
|
|
648
|
-
self.display_manager.needs_update = True
|
|
649
|
-
if not self.display_manager.update_timer.isActive():
|
|
650
|
-
self.display_manager.update_timer.start()
|
|
651
|
-
self.mark_as_modified()
|
|
633
|
+
def handle_needs_update(self):
|
|
634
|
+
self.display_manager.needs_update = True
|
|
635
|
+
if not self.display_manager.update_timer.isActive():
|
|
636
|
+
self.display_manager.update_timer.start()
|
|
637
|
+
self.mark_as_modified()
|
|
652
638
|
|
|
653
639
|
def end_copy_brush_area(self):
|
|
654
640
|
if self.display_manager.update_timer.isActive():
|
|
@@ -658,6 +644,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
658
644
|
self.mark_as_modified()
|
|
659
645
|
|
|
660
646
|
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
647
|
+
self.image_viewer.update_brush_cursor()
|
|
661
648
|
if self.undo_action:
|
|
662
649
|
if has_undo:
|
|
663
650
|
self.undo_action.setText(f"Undo {undo_desc}")
|
|
@@ -700,6 +687,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
700
687
|
self.io_gui_handler.close_file()
|
|
701
688
|
self.set_master_layer(None)
|
|
702
689
|
self.mark_as_modified(False)
|
|
690
|
+
self.zoom_factor_label.setText("")
|
|
703
691
|
|
|
704
692
|
def set_view_master(self):
|
|
705
693
|
self.display_manager.set_view_master()
|
|
@@ -752,3 +740,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
752
740
|
self.brush_tool.increase_brush_size()
|
|
753
741
|
else:
|
|
754
742
|
self.brush_tool.decrease_brush_size()
|
|
743
|
+
|
|
744
|
+
def handle_set_zoom_factor(self, zoom_factor):
|
|
745
|
+
self.zoom_factor_label.setText(f"zoom: {zoom_factor:.1%}")
|