shinestacker 1.5.4__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (35) 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 +6 -6
  18. shinestacker/gui/menu_manager.py +2 -0
  19. shinestacker/gui/new_project.py +2 -1
  20. shinestacker/gui/project_controller.py +8 -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 +14 -1
  25. shinestacker/retouch/image_view_status.py +4 -1
  26. shinestacker/retouch/sidebyside_view.py +29 -13
  27. shinestacker/retouch/transformation_manager.py +0 -1
  28. shinestacker/retouch/undo_manager.py +1 -1
  29. shinestacker/retouch/view_strategy.py +14 -4
  30. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/METADATA +1 -1
  31. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/RECORD +35 -31
  32. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/WHEEL +0 -0
  33. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/entry_points.txt +0 -0
  34. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/licenses/LICENSE +0 -0
  35. {shinestacker-1.5.4.dist-info → shinestacker-1.6.0.dist-info}/top_level.txt +0 -0
@@ -2,87 +2,29 @@
2
2
  # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
3
3
  import os
4
4
  import traceback
5
- from PySide6.QtCore import 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()
@@ -441,12 +441,12 @@ class MainWindow(QMainWindow, LogManager):
441
441
  return True
442
442
  return False
443
443
 
444
- def toggle_expert_options(self):
445
- 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'))
446
447
 
447
- def set_expert_options(self):
448
- self.menu_manager.expert_options_action.setChecked(True)
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)
@@ -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):
@@ -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)
@@ -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().split('/')
159
- working_path = '/'.join(input_folder[:-1])
160
- 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)
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
- 'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
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
- 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':
@@ -6,6 +6,7 @@ from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
6
6
  from PySide6.QtCore import Qt
7
7
  from PySide6.QtGui import QGuiApplication
8
8
  from .. config.constants import constants
9
+ from .. config.app_config import AppConfig
9
10
  from .. config.gui_constants import gui_constants
10
11
  from .. gui.recent_file_manager import RecentFileManager
11
12
  from .image_viewer import ImageViewer
@@ -184,6 +185,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
184
185
  def change_layer_item(item):
185
186
  layer_idx = self.thumbnail_list.row(item)
186
187
  self.change_layer(layer_idx)
188
+ self.display_manager.highlight_thumbnail(layer_idx)
187
189
 
188
190
  self.thumbnail_list.itemClicked.connect(change_layer_item)
189
191
  self.thumbnail_list.setStyleSheet("""
@@ -236,6 +238,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
236
238
  self.flow_slider)
237
239
  self.image_viewer.set_brush(self.brush_tool.brush)
238
240
  self.image_viewer.set_preview_brush(self.brush_tool.brush)
241
+ self.image_viewer.status.set_zoom_factor_requested.connect(self.handle_set_zoom_factor)
239
242
  self.brush_tool.update_brush_thumb()
240
243
  self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
241
244
  menubar = self.menuBar()
@@ -434,7 +437,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
434
437
  view_menu.addAction(self.toggle_view_master_individual_action)
435
438
  view_menu.addSeparator()
436
439
 
437
- self.set_strategy('overlaid')
440
+ self.set_strategy(AppConfig.get('view_strategy'))
438
441
 
439
442
  sort_asc_action = QAction("Sort Layers A-Z", self)
440
443
  sort_asc_action.setProperty("requires_file", True)
@@ -471,6 +474,8 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
471
474
  help_menu.setObjectName("Help")
472
475
  shortcuts_help_action = QAction("Shortcuts and Mouse", self)
473
476
 
477
+ self.zoom_factor_label = QLabel("")
478
+ self.statusBar().addPermanentWidget(self.zoom_factor_label)
474
479
  self.statusBar().showMessage("Shine Stacker ready.", 2000)
475
480
 
476
481
  def shortcuts_help():
@@ -488,6 +493,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
488
493
  self.set_enabled_file_open_close_actions(False)
489
494
  self.installEventFilter(self)
490
495
 
496
+ def handle_config(self):
497
+ self.set_strategy(AppConfig.get('view_strategy'))
498
+ self.display_manager.update_timer.setInterval(AppConfig.get('display_refresh_time'))
499
+
491
500
  def set_enabled_view_toggles(self, enabled):
492
501
  self.view_master_action.setEnabled(enabled)
493
502
  self.view_individual_action.setEnabled(enabled)
@@ -700,6 +709,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
700
709
  self.io_gui_handler.close_file()
701
710
  self.set_master_layer(None)
702
711
  self.mark_as_modified(False)
712
+ self.zoom_factor_label.setText("")
703
713
 
704
714
  def set_view_master(self):
705
715
  self.display_manager.set_view_master()
@@ -752,3 +762,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
752
762
  self.brush_tool.increase_brush_size()
753
763
  else:
754
764
  self.brush_tool.decrease_brush_size()
765
+
766
+ def handle_set_zoom_factor(self, zoom_factor):
767
+ self.zoom_factor_label.setText(f"zoom: {zoom_factor:.1%}")
@@ -1,9 +1,11 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
- from PySide6.QtCore import QObject, QRectF
2
+ from PySide6.QtCore import QObject, QRectF, Signal
3
3
  from PySide6.QtGui import QPixmap
4
4
 
5
5
 
6
6
  class ImageViewStatus(QObject):
7
+ set_zoom_factor_requested = Signal(float)
8
+
7
9
  def __init__(self, parent=None):
8
10
  super().__init__(parent)
9
11
  self.pixmap_master = QPixmap()
@@ -53,6 +55,7 @@ class ImageViewStatus(QObject):
53
55
 
54
56
  def set_zoom_factor(self, zoom_factor):
55
57
  self.zoom_factor = zoom_factor
58
+ self.set_zoom_factor_requested.emit(zoom_factor)
56
59
 
57
60
  def set_min_scale(self, min_scale):
58
61
  self.min_scale = min_scale
@@ -61,6 +61,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
61
61
  self.master_view.setFocusPolicy(Qt.NoFocus)
62
62
  self.current_brush_cursor = None
63
63
  self.last_color_update_time_current = 0
64
+ self._updating_scrollbars = False
64
65
 
65
66
  def setup_layout(self):
66
67
  raise NotImplementedError("Subclasses must implement setup_layout")
@@ -78,14 +79,25 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
78
79
  self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
79
80
  self.master_view.mouse_released.connect(self.handle_master_mouse_release)
80
81
  self.master_view.gesture_event.connect(self.handle_gesture_event)
82
+
83
+ def sync_scrollbars(source, target):
84
+ if not self._updating_scrollbars:
85
+ self._updating_scrollbars = True
86
+ target.setValue(source.value())
87
+ self._updating_scrollbars = False
88
+
81
89
  self.current_view.horizontalScrollBar().valueChanged.connect(
82
- self.master_view.horizontalScrollBar().setValue)
90
+ lambda value: sync_scrollbars(self.current_view.horizontalScrollBar(),
91
+ self.master_view.horizontalScrollBar()))
83
92
  self.current_view.verticalScrollBar().valueChanged.connect(
84
- self.master_view.verticalScrollBar().setValue)
93
+ lambda value: sync_scrollbars(self.current_view.verticalScrollBar(),
94
+ self.master_view.verticalScrollBar()))
85
95
  self.master_view.horizontalScrollBar().valueChanged.connect(
86
- self.current_view.horizontalScrollBar().setValue)
96
+ lambda value: sync_scrollbars(self.master_view.horizontalScrollBar(),
97
+ self.current_view.horizontalScrollBar()))
87
98
  self.master_view.verticalScrollBar().valueChanged.connect(
88
- self.current_view.verticalScrollBar().setValue)
99
+ lambda value: sync_scrollbars(self.master_view.verticalScrollBar(),
100
+ self.current_view.verticalScrollBar()))
89
101
  self.current_view.wheel_event.connect(self.handle_wheel_event)
90
102
  self.master_view.wheel_event.connect(self.handle_wheel_event)
91
103
  # pylint: disable=C0103, W0201
@@ -377,28 +389,32 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
377
389
  def set_master_image(self, qimage):
378
390
  self.status.set_master_image(qimage)
379
391
  pixmap = self.status.pixmap_master
380
- self.master_view.setSceneRect(QRectF(pixmap.rect()))
392
+ pixmap_rect = QRectF(pixmap.rect())
393
+ self.pixmap_item_master.setPos(0, 0)
394
+ self.master_view.setSceneRect(pixmap_rect)
395
+ self.master_scene.setSceneRect(pixmap_rect)
381
396
  self.pixmap_item_master.setPixmap(pixmap)
382
- img_width, img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
397
+ _img_width, _img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
383
398
  self.master_view.resetTransform()
384
399
  self.master_view.scale(scale_factor, scale_factor)
385
400
  self.master_view.centerOn(self.pixmap_item_master)
386
- center = self.master_scene.sceneRect().center()
387
- self.brush_preview.setPos(max(0, min(center.x(), img_width)),
388
- max(0, min(center.y(), img_height)))
389
- self.master_scene.setSceneRect(QRectF(self.pixmap_item_master.boundingRect()))
390
401
  self.center_image(self.master_view)
391
402
  self.update_cursor_pen_width()
392
403
 
393
404
  def set_current_image(self, qimage):
394
405
  self.status.set_current_image(qimage)
395
406
  pixmap = self.status.pixmap_current
396
- self.current_scene.setSceneRect(QRectF(pixmap.rect()))
407
+ pixmap_rect = QRectF(pixmap.rect())
408
+ self.pixmap_item_current.setPos(0, 0)
409
+ self.current_view.setSceneRect(pixmap_rect)
410
+ self.current_scene.setSceneRect(pixmap_rect)
397
411
  self.pixmap_item_current.setPixmap(pixmap)
412
+ _img_width, _img_height, scale_factor = self.setup_view_image(self.current_view, pixmap)
398
413
  self.current_view.resetTransform()
399
- self.master_view.scale(self.zoom_factor(), self.zoom_factor())
400
- self.current_scene.setSceneRect(QRectF(self.pixmap_item_current.boundingRect()))
414
+ self.current_view.scale(scale_factor, scale_factor)
415
+ self.current_view.centerOn(self.pixmap_item_current)
401
416
  self.center_image(self.current_view)
417
+ self.update_cursor_pen_width()
402
418
 
403
419
  def arrange_images(self):
404
420
  if self.status.empty():
@@ -26,7 +26,6 @@ class TransfromationManager(LayerCollectionHandler):
26
26
  self.copy_master_layer()
27
27
  self.editor.image_viewer.update_master_display()
28
28
  self.editor.image_viewer.update_current_display()
29
- self.editor.image_viewer.refresh_display()
30
29
  self.editor.display_manager.update_thumbnails()
31
30
  self.editor.mark_as_modified()
32
31