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.

Files changed (38) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/multilayer.py +1 -1
  3. shinestacker/algorithms/stack.py +17 -9
  4. shinestacker/app/args_parser_opts.py +4 -0
  5. shinestacker/app/gui_utils.py +10 -2
  6. shinestacker/app/main.py +5 -2
  7. shinestacker/app/project.py +4 -2
  8. shinestacker/app/retouch.py +3 -1
  9. shinestacker/app/settings_dialog.py +171 -0
  10. shinestacker/config/app_config.py +30 -0
  11. shinestacker/config/constants.py +3 -0
  12. shinestacker/config/gui_constants.py +4 -2
  13. shinestacker/config/settings.py +110 -0
  14. shinestacker/gui/action_config.py +6 -5
  15. shinestacker/gui/action_config_dialog.py +17 -74
  16. shinestacker/gui/config_dialog.py +78 -0
  17. shinestacker/gui/main_window.py +16 -6
  18. shinestacker/gui/menu_manager.py +16 -10
  19. shinestacker/gui/new_project.py +2 -1
  20. shinestacker/gui/project_controller.py +11 -6
  21. shinestacker/gui/project_model.py +16 -1
  22. shinestacker/gui/recent_file_manager.py +3 -21
  23. shinestacker/retouch/display_manager.py +47 -5
  24. shinestacker/retouch/image_editor_ui.py +59 -15
  25. shinestacker/retouch/image_view_status.py +4 -1
  26. shinestacker/retouch/io_gui_handler.py +3 -0
  27. shinestacker/retouch/overlaid_view.py +6 -43
  28. shinestacker/retouch/sidebyside_view.py +45 -41
  29. shinestacker/retouch/transformation_manager.py +0 -1
  30. shinestacker/retouch/undo_manager.py +1 -1
  31. shinestacker/retouch/view_strategy.py +125 -61
  32. shinestacker/retouch/white_balance_filter.py +5 -6
  33. {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/METADATA +1 -1
  34. {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/RECORD +38 -34
  35. {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.5.3.dist-info → shinestacker-1.6.0.dist-info}/licenses/LICENSE +0 -0
  38. {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 Qt, QTimer
6
- from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QMessageBox,
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(QDialog):
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
- scroll_area = QScrollArea()
27
- scroll_area.setWidgetResizable(True)
28
- container_widget = QWidget()
29
- self.container_layout = QFormLayout(container_widget)
30
- self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
31
- self.container_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
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().mark_as_modified(True, "Modify Configuration")
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 self.parent().expert_options
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=constants.DEFAULT_PY_MAX_THREADS,
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=constants.DEFAULT_MAX_FWK_THREADS,
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=constants.DEFAULT_ALIGN_MAX_THREADS,
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
@@ -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 toggle_expert_options(self):
440
- self.expert_options = self.menu_manager.expert_options_action.isChecked()
444
+ def handle_config(self):
445
+ self.menu_manager.expert_options_action.setChecked(
446
+ AppConfig.get('expert_options'))
441
447
 
442
- def set_expert_options(self):
443
- self.menu_manager.expert_options_action.setChecked(True)
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)
@@ -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,
@@ -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 self.parent().expert_options
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().split('/')
157
- working_path = '/'.join(input_folder[:-1])
158
- input_path = input_folder[-1]
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
- 'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
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
- self.params = params or {}
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 PySide6.QtCore import QStandardPaths
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
- self.filename = filename
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(gui_constants.PAINT_REFRESH_TIMER)
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._set_master_thumbnail(self.create_thumbnail(self.master_layer()))
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':