shinestacker 1.5.3__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 +16 -6
- shinestacker/gui/menu_manager.py +16 -10
- shinestacker/gui/new_project.py +2 -1
- shinestacker/gui/project_controller.py +11 -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 +59 -15
- shinestacker/retouch/image_view_status.py +4 -1
- shinestacker/retouch/io_gui_handler.py +3 -0
- shinestacker/retouch/overlaid_view.py +6 -43
- shinestacker/retouch/sidebyside_view.py +45 -41
- shinestacker/retouch/transformation_manager.py +0 -1
- shinestacker/retouch/undo_manager.py +1 -1
- shinestacker/retouch/view_strategy.py +125 -61
- shinestacker/retouch/white_balance_filter.py +5 -6
- {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/METADATA +1 -1
- {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/RECORD +38 -34
- {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.3.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()
|
|
@@ -154,9 +154,14 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
154
154
|
self.menu_manager.set_enabled_sub_actions_gui)
|
|
155
155
|
self.project_controller.add_recent_file_requested.connect(
|
|
156
156
|
self.menu_manager.add_recent_file)
|
|
157
|
+
self.project_controller.set_enabled_file_open_close_actions_requested.connect(
|
|
158
|
+
self.set_enabled_file_open_close_actions)
|
|
159
|
+
|
|
157
160
|
self.menu_manager.open_file_requested.connect(
|
|
158
161
|
self.project_controller.open_project)
|
|
159
162
|
|
|
163
|
+
self.set_enabled_file_open_close_actions(False)
|
|
164
|
+
|
|
160
165
|
def modified(self):
|
|
161
166
|
return self.project_editor.modified()
|
|
162
167
|
|
|
@@ -436,12 +441,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
436
441
|
return True
|
|
437
442
|
return False
|
|
438
443
|
|
|
439
|
-
def
|
|
440
|
-
self.
|
|
444
|
+
def handle_config(self):
|
|
445
|
+
self.menu_manager.expert_options_action.setChecked(
|
|
446
|
+
AppConfig.get('expert_options'))
|
|
441
447
|
|
|
442
|
-
def
|
|
443
|
-
self.menu_manager.expert_options_action.
|
|
444
|
-
self.expert_options = True
|
|
448
|
+
def toggle_expert_options(self):
|
|
449
|
+
AppConfig.set('expert_options', self.menu_manager.expert_options_action.isChecked())
|
|
445
450
|
|
|
446
451
|
def before_thread_begins(self):
|
|
447
452
|
self.menu_manager.run_job_action.setEnabled(False)
|
|
@@ -571,3 +576,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
571
576
|
self.menu_manager.set_enabled_sub_actions_gui(enable_sub_actions)
|
|
572
577
|
else:
|
|
573
578
|
self.menu_manager.set_enabled_sub_actions_gui(False)
|
|
579
|
+
|
|
580
|
+
def set_enabled_file_open_close_actions(self, enabled):
|
|
581
|
+
for action in self.findChildren(QAction):
|
|
582
|
+
if action.property("requires_file"):
|
|
583
|
+
action.setEnabled(enabled)
|
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
|
|
|
@@ -60,8 +61,10 @@ class MenuManager(QObject):
|
|
|
60
61
|
def get_icon(self, icon):
|
|
61
62
|
return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
|
|
62
63
|
|
|
63
|
-
def action(self, name):
|
|
64
|
+
def action(self, name, requires_file=False):
|
|
64
65
|
action = QAction(name, self.parent)
|
|
66
|
+
if requires_file:
|
|
67
|
+
action.setProperty("requires_file", True)
|
|
65
68
|
shortcut = self.shortcuts.get(name, '')
|
|
66
69
|
if shortcut:
|
|
67
70
|
action.setShortcut(shortcut)
|
|
@@ -110,36 +113,37 @@ class MenuManager(QObject):
|
|
|
110
113
|
self.undo_action.setEnabled(False)
|
|
111
114
|
menu.addAction(self.undo_action)
|
|
112
115
|
for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
|
|
113
|
-
menu.addAction(self.action(name))
|
|
114
|
-
self.delete_element_action = self.action("Delete")
|
|
116
|
+
menu.addAction(self.action(name, requires_file=True))
|
|
117
|
+
self.delete_element_action = self.action("Delete", requires_file=True)
|
|
115
118
|
self.delete_element_action.setEnabled(False)
|
|
116
119
|
menu.addAction(self.delete_element_action)
|
|
117
120
|
menu.addSeparator()
|
|
118
121
|
for name in ["Move &Up", "Move &Down"]:
|
|
119
|
-
menu.addAction(self.action(name))
|
|
122
|
+
menu.addAction(self.action(name, requires_file=True))
|
|
120
123
|
menu.addSeparator()
|
|
121
|
-
self.enable_action = self.action("E&nable")
|
|
124
|
+
self.enable_action = self.action("E&nable", requires_file=True)
|
|
122
125
|
menu.addAction(self.enable_action)
|
|
123
|
-
self.disable_action = self.action("Di&sable")
|
|
126
|
+
self.disable_action = self.action("Di&sable", requires_file=True)
|
|
124
127
|
menu.addAction(self.disable_action)
|
|
125
128
|
for name in ["Enable All", "Disable All"]:
|
|
126
|
-
menu.addAction(self.action(name))
|
|
129
|
+
menu.addAction(self.action(name, requires_file=True))
|
|
127
130
|
|
|
128
131
|
def add_view_menu(self):
|
|
129
132
|
menu = self.menubar.addMenu("&View")
|
|
130
133
|
self.expert_options_action = self.action("Expert Options")
|
|
131
134
|
self.expert_options_action.setCheckable(True)
|
|
135
|
+
self.expert_options_action.setChecked(AppConfig.get('expert_options'))
|
|
132
136
|
menu.addAction(self.expert_options_action)
|
|
133
137
|
|
|
134
138
|
def add_job_menu(self):
|
|
135
139
|
menu = self.menubar.addMenu("&Jobs")
|
|
136
|
-
self.add_job_action = self.action("Add Job")
|
|
140
|
+
self.add_job_action = self.action("Add Job", requires_file=True)
|
|
137
141
|
menu.addAction(self.add_job_action)
|
|
138
142
|
menu.addSeparator()
|
|
139
|
-
self.run_job_action = self.action("Run Job")
|
|
143
|
+
self.run_job_action = self.action("Run Job", requires_file=True)
|
|
140
144
|
self.run_job_action.setEnabled(False)
|
|
141
145
|
menu.addAction(self.run_job_action)
|
|
142
|
-
self.run_all_jobs_action = self.action("Run All Jobs")
|
|
146
|
+
self.run_all_jobs_action = self.action("Run All Jobs", requires_file=True)
|
|
143
147
|
self.set_enabled_run_all_jobs(False)
|
|
144
148
|
menu.addAction(self.run_all_jobs_action)
|
|
145
149
|
|
|
@@ -148,6 +152,7 @@ class MenuManager(QObject):
|
|
|
148
152
|
add_action_menu = QMenu("Add Action", self.parent)
|
|
149
153
|
for action in constants.ACTION_TYPES:
|
|
150
154
|
entry_action = QAction(action, self.parent)
|
|
155
|
+
entry_action.setProperty("requires_file", True)
|
|
151
156
|
entry_action.triggered.connect({
|
|
152
157
|
constants.ACTION_COMBO: self.add_action_combined_actions,
|
|
153
158
|
constants.ACTION_NOISEDETECTION: self.add_action_noise_detection,
|
|
@@ -161,6 +166,7 @@ class MenuManager(QObject):
|
|
|
161
166
|
self.sub_action_menu_entries = []
|
|
162
167
|
for action in constants.SUB_ACTION_TYPES:
|
|
163
168
|
entry_action = QAction(action, self.parent)
|
|
169
|
+
entry_action.setProperty("requires_file", True)
|
|
164
170
|
entry_action.triggered.connect({
|
|
165
171
|
constants.ACTION_MASKNOISE: self.add_sub_action_make_noise,
|
|
166
172
|
constants.ACTION_VIGNETTING: self.add_sub_action_vignetting,
|
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)
|
|
@@ -21,6 +21,7 @@ class ProjectController(QObject):
|
|
|
21
21
|
enable_save_actions_requested = Signal(bool)
|
|
22
22
|
enable_sub_actions_requested = Signal(bool)
|
|
23
23
|
add_recent_file_requested = Signal(str)
|
|
24
|
+
set_enabled_file_open_close_actions_requested = Signal(bool)
|
|
24
25
|
|
|
25
26
|
def __init__(self, parent):
|
|
26
27
|
super().__init__(parent)
|
|
@@ -138,6 +139,7 @@ class ProjectController(QObject):
|
|
|
138
139
|
self.clear_action_list()
|
|
139
140
|
self.mark_as_modified(False)
|
|
140
141
|
self.project_editor.reset_undo()
|
|
142
|
+
self.set_enabled_file_open_close_actions_requested.emit(False)
|
|
141
143
|
|
|
142
144
|
def new_project(self):
|
|
143
145
|
if not self.check_unsaved_changes():
|
|
@@ -153,9 +155,9 @@ class ProjectController(QObject):
|
|
|
153
155
|
if dialog.exec() == QDialog.Accepted:
|
|
154
156
|
self.save_actions_set_enabled(True)
|
|
155
157
|
self.project_editor.reset_undo()
|
|
156
|
-
input_folder = dialog.get_input_folder()
|
|
157
|
-
working_path =
|
|
158
|
-
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)
|
|
159
161
|
selected_filenames = dialog.get_selected_filenames()
|
|
160
162
|
if dialog.get_noise_detection():
|
|
161
163
|
job_noise = ActionConfig(
|
|
@@ -208,13 +210,15 @@ class ProjectController(QObject):
|
|
|
208
210
|
focus_pyramid_name = f'{input_path}-focus-stack-pyramid'
|
|
209
211
|
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
210
212
|
{'name': focus_pyramid_name,
|
|
211
|
-
'stacker': constants.STACK_ALGO_PYRAMID
|
|
213
|
+
'stacker': constants.STACK_ALGO_PYRAMID,
|
|
214
|
+
'exif_path': input_path})
|
|
212
215
|
job.add_sub_action(focus_pyramid)
|
|
213
216
|
if dialog.get_focus_stack_depth_map():
|
|
214
217
|
focus_depth_map_name = f'{input_path}-focus-stack-depth-map'
|
|
215
218
|
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
216
219
|
{'name': focus_depth_map_name,
|
|
217
|
-
'stacker': constants.STACK_ALGO_DEPTH_MAP
|
|
220
|
+
'stacker': constants.STACK_ALGO_DEPTH_MAP,
|
|
221
|
+
'exif_path': input_path})
|
|
218
222
|
job.add_sub_action(focus_depth_map)
|
|
219
223
|
if dialog.get_multi_layer():
|
|
220
224
|
multi_input_path = []
|
|
@@ -229,7 +233,7 @@ class ProjectController(QObject):
|
|
|
229
233
|
multi_layer = ActionConfig(
|
|
230
234
|
constants.ACTION_MULTILAYER,
|
|
231
235
|
{'name': f'{input_path}-multi-layer',
|
|
232
|
-
|
|
236
|
+
'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
|
|
233
237
|
job.add_sub_action(multi_layer)
|
|
234
238
|
self.add_job_to_project(job)
|
|
235
239
|
self.project_editor.set_modified(True)
|
|
@@ -250,6 +254,7 @@ class ProjectController(QObject):
|
|
|
250
254
|
project = Project.from_dict(json_obj['project'])
|
|
251
255
|
if project is None:
|
|
252
256
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
257
|
+
self.set_enabled_file_open_close_actions_requested.emit(True)
|
|
253
258
|
self.set_project(project)
|
|
254
259
|
self.mark_as_modified(False)
|
|
255
260
|
self.add_recent_file_requested.emit(abs_file_path)
|
|
@@ -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':
|