shinestacker 1.3.0__py3-none-any.whl → 1.4.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 (50) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +229 -41
  3. shinestacker/algorithms/align_auto.py +15 -3
  4. shinestacker/algorithms/align_parallel.py +81 -25
  5. shinestacker/algorithms/balance.py +23 -13
  6. shinestacker/algorithms/base_stack_algo.py +14 -20
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/noise_detection.py +3 -1
  9. shinestacker/algorithms/pyramid.py +8 -22
  10. shinestacker/algorithms/pyramid_auto.py +5 -14
  11. shinestacker/algorithms/pyramid_tiles.py +18 -20
  12. shinestacker/algorithms/stack_framework.py +1 -1
  13. shinestacker/algorithms/utils.py +37 -10
  14. shinestacker/algorithms/vignetting.py +2 -0
  15. shinestacker/app/gui_utils.py +10 -0
  16. shinestacker/app/main.py +3 -1
  17. shinestacker/app/project.py +3 -1
  18. shinestacker/app/retouch.py +3 -1
  19. shinestacker/config/gui_constants.py +2 -2
  20. shinestacker/core/core_utils.py +10 -1
  21. shinestacker/gui/action_config.py +172 -7
  22. shinestacker/gui/action_config_dialog.py +443 -452
  23. shinestacker/gui/colors.py +1 -0
  24. shinestacker/gui/folder_file_selection.py +5 -0
  25. shinestacker/gui/gui_run.py +2 -2
  26. shinestacker/gui/main_window.py +18 -9
  27. shinestacker/gui/menu_manager.py +26 -2
  28. shinestacker/gui/new_project.py +5 -5
  29. shinestacker/gui/project_controller.py +4 -0
  30. shinestacker/gui/project_editor.py +6 -4
  31. shinestacker/gui/recent_file_manager.py +93 -0
  32. shinestacker/gui/sys_mon.py +24 -23
  33. shinestacker/retouch/base_filter.py +5 -5
  34. shinestacker/retouch/brush_preview.py +3 -0
  35. shinestacker/retouch/brush_tool.py +11 -11
  36. shinestacker/retouch/display_manager.py +21 -37
  37. shinestacker/retouch/image_editor_ui.py +129 -71
  38. shinestacker/retouch/image_view_status.py +61 -0
  39. shinestacker/retouch/image_viewer.py +89 -431
  40. shinestacker/retouch/io_gui_handler.py +12 -2
  41. shinestacker/retouch/overlaid_view.py +212 -0
  42. shinestacker/retouch/shortcuts_help.py +13 -3
  43. shinestacker/retouch/sidebyside_view.py +479 -0
  44. shinestacker/retouch/view_strategy.py +466 -0
  45. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  46. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/RECORD +50 -45
  47. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  48. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  49. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  50. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ class ColorPalette:
16
16
  BLACK = ColorEntry(0, 0, 0)
17
17
  WHITE = ColorEntry(255, 255, 255)
18
18
  LIGHT_BLUE = ColorEntry(210, 210, 240)
19
+ LIGHT_GREEN = ColorEntry(210, 240, 210)
19
20
  LIGHT_RED = ColorEntry(240, 210, 210)
20
21
  DARK_BLUE = ColorEntry(0, 0, 160)
21
22
  DARK_RED = ColorEntry(160, 0, 0)
@@ -58,6 +58,7 @@ class FolderFileSelectionWidget(QWidget):
58
58
  # self.path_edit.setPlaceholderText("input files")
59
59
 
60
60
  def handle_browse(self):
61
+ self.path_edit.setText('')
61
62
  if self.selection_mode == 'folder':
62
63
  self.browse_folder()
63
64
  else:
@@ -80,6 +81,7 @@ class FolderFileSelectionWidget(QWidget):
80
81
  self.selected_files = files
81
82
  self.path_edit.setText(parent_dir)
82
83
  else:
84
+ self.selected_files = []
83
85
  QMessageBox.warning(
84
86
  self, "Invalid Selection",
85
87
  "All files must be in the same directory."
@@ -91,6 +93,9 @@ class FolderFileSelectionWidget(QWidget):
91
93
  def get_selected_files(self):
92
94
  return self.selected_files
93
95
 
96
+ def num_selected_files(self):
97
+ return len(self.selected_files)
98
+
94
99
  def get_selected_filenames(self):
95
100
  return [os.path.basename(file_path) for file_path in self.selected_files]
96
101
 
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
9
9
  from .. config.constants import constants
10
10
  from .. config.gui_constants import gui_constants
11
11
  from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
12
- from .. algorithms.utils import extension_tif_jpg, extension_pdf
12
+ from .. algorithms.utils import extension_jpg_tif_png, extension_pdf
13
13
  from .gui_logging import LogWorker, QTextEditLogger
14
14
  from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
15
15
  from .colors import (
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
209
209
  try:
210
210
  if extension_pdf(path):
211
211
  image_view = GuiPdfView(path, self)
212
- elif extension_tif_jpg(path):
212
+ elif extension_jpg_tif_png(path):
213
213
  image_view = GuiImageView(path, self)
214
214
  else:
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
@@ -134,19 +134,28 @@ class MainWindow(QMainWindow, LogManager):
134
134
  self.update_title()
135
135
 
136
136
  self.project_editor.modified_signal.connect(handle_modified)
137
- self.project_editor.select_signal.connect(self.update_delete_action_state)
138
- self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
137
+ self.project_editor.select_signal.connect(
138
+ self.update_delete_action_state)
139
+ self.project_editor.refresh_ui_signal.connect(
140
+ self.refresh_ui)
139
141
  self.project_editor.enable_delete_action_signal.connect(
140
142
  self.menu_manager.delete_element_action.setEnabled)
141
143
  self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
142
144
  self.menu_manager.set_enabled_undo_action)
143
- self.project_controller.update_title_requested.connect(self.update_title)
144
- self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
145
- self.project_controller.activate_window_requested.connect(self.activateWindow)
145
+ self.project_controller.update_title_requested.connect(
146
+ self.update_title)
147
+ self.project_controller.refresh_ui_requested.connect(
148
+ self.refresh_ui)
149
+ self.project_controller.activate_window_requested.connect(
150
+ self.activateWindow)
146
151
  self.project_controller.enable_save_actions_requested.connect(
147
152
  self.menu_manager.save_actions_set_enabled)
148
153
  self.project_controller.enable_sub_actions_requested.connect(
149
154
  self.menu_manager.set_enabled_sub_actions_gui)
155
+ self.project_controller.add_recent_file_requested.connect(
156
+ self.menu_manager.add_recent_file)
157
+ self.menu_manager.open_file_requested.connect(
158
+ self.project_controller.open_project)
150
159
 
151
160
  def modified(self):
152
161
  return self.project_editor.modified()
@@ -301,6 +310,9 @@ class MainWindow(QMainWindow, LogManager):
301
310
  edit_config_action.triggered.connect(self.edit_current_action)
302
311
  menu.addAction(edit_config_action)
303
312
  menu.addSeparator()
313
+ menu.addAction(self.menu_manager.run_job_action)
314
+ menu.addAction(self.menu_manager.run_all_jobs_action)
315
+ menu.addSeparator()
304
316
  self.current_action_working_path, name = get_action_working_path(current_action)
305
317
  if self.current_action_working_path != '' and \
306
318
  os.path.exists(self.current_action_working_path):
@@ -339,9 +351,6 @@ class MainWindow(QMainWindow, LogManager):
339
351
  self.browse_output_path_action = QAction(action_name)
340
352
  self.browse_output_path_action.triggered.connect(self.browse_output_path)
341
353
  menu.addAction(self.browse_output_path_action)
342
- menu.addSeparator()
343
- menu.addAction(self.menu_manager.run_job_action)
344
- menu.addAction(self.menu_manager.run_all_jobs_action)
345
354
  if current_action.type_name == constants.ACTION_JOB:
346
355
  retouch_path = self.get_retouch_path(current_action)
347
356
  if len(retouch_path) > 0:
@@ -533,7 +542,7 @@ class MainWindow(QMainWindow, LogManager):
533
542
  r = self.get_retouch_path(job)
534
543
  if len(r) > 0:
535
544
  retouch_paths.append((job.params["name"], r))
536
- new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
545
+ new_window, id_str = self.create_new_window(f"{project_name} [Project]",
537
546
  labels, retouch_paths)
538
547
  worker = ProjectLogWorker(self.project(), id_str)
539
548
  self.connect_worker_signals(worker, new_window)
@@ -1,13 +1,20 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
2
2
  import os
3
+ from functools import partial
4
+ from PySide6.QtCore import Signal, QObject
3
5
  from PySide6.QtGui import QAction, QIcon
4
6
  from PySide6.QtWidgets import QMenu, QComboBox
5
7
  from .. config.constants import constants
8
+ from .recent_file_manager import RecentFileManager
6
9
 
7
10
 
8
- class MenuManager:
11
+ class MenuManager(QObject):
12
+ open_file_requested = Signal(str)
13
+
9
14
  def __init__(self, menubar, actions, project_editor, parent):
15
+ super().__init__(parent)
10
16
  self.script_dir = os.path.dirname(__file__)
17
+ self._recent_file_manager = RecentFileManager("shinestacker-recent-project-files.txt")
11
18
  self.project_editor = project_editor
12
19
  self.parent = parent
13
20
  self.menubar = menubar
@@ -69,10 +76,27 @@ class MenuManager:
69
76
  action.triggered.connect(action_fun)
70
77
  return action
71
78
 
79
+ def update_recent_files(self):
80
+ self.recent_files_menu.clear()
81
+ recent_files = self._recent_file_manager.get_files_with_display_names()
82
+ for file_path, display_name in recent_files.items():
83
+ action = self.recent_files_menu.addAction(display_name)
84
+ action.setData(file_path)
85
+ action.triggered.connect(partial(self.open_file_requested.emit, file_path))
86
+ self.recent_files_menu.setEnabled(len(recent_files) > 0)
87
+
88
+ def add_recent_file(self, file_path):
89
+ self._recent_file_manager.add_file(file_path)
90
+ self.update_recent_files()
91
+
72
92
  def add_file_menu(self):
73
93
  menu = self.menubar.addMenu("&File")
74
- for name in ["&New...", "&Open...", "&Close"]:
94
+ for name in ["&New...", "&Open..."]:
75
95
  menu.addAction(self.action(name))
96
+ self.recent_files_menu = QMenu("Open &Recent", menu)
97
+ menu.addMenu(self.recent_files_menu)
98
+ self.update_recent_files()
99
+ menu.addAction(self.action("&Close"))
76
100
  menu.addSeparator()
77
101
  self.save_action = self.action("&Save")
78
102
  menu.addAction(self.save_action)
@@ -75,7 +75,7 @@ class NewProjectDialog(BaseFormDialog):
75
75
  self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
76
76
  self.multi_layer = QCheckBox()
77
77
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
78
- step1_group = QGroupBox("1️⃣ Select Input")
78
+ step1_group = QGroupBox("1) Select Input")
79
79
  step1_layout = QVBoxLayout()
80
80
  step1_layout.setContentsMargins(15, 0, 15, 15)
81
81
  step1_layout.addWidget(
@@ -92,7 +92,7 @@ class NewProjectDialog(BaseFormDialog):
92
92
  step1_layout.addLayout(input_form)
93
93
  step1_group.setLayout(step1_layout)
94
94
  self.form_layout.addRow(step1_group)
95
- step2_group = QGroupBox("2️⃣ Basic Options")
95
+ step2_group = QGroupBox("2) Basic Options")
96
96
  step2_layout = QFormLayout()
97
97
  step2_layout.setContentsMargins(15, 0, 15, 15)
98
98
  step2_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -136,7 +136,7 @@ class NewProjectDialog(BaseFormDialog):
136
136
  "Save multi layer TIFF:", self.multi_layer)
137
137
  step2_group.setLayout(step2_layout)
138
138
  self.form_layout.addRow(step2_group)
139
- step3_group = QGroupBox("3️⃣ Confirmation")
139
+ step3_group = QGroupBox("3) Confirm")
140
140
  step3_layout = QVBoxLayout()
141
141
  step3_layout.setContentsMargins(15, 0, 15, 15)
142
142
  step3_layout.addWidget(
@@ -145,7 +145,7 @@ class NewProjectDialog(BaseFormDialog):
145
145
  QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
146
146
  step3_group.setLayout(step3_layout)
147
147
  self.form_layout.addRow(step3_group)
148
- step4_group = QGroupBox("4️⃣ Execution")
148
+ step4_group = QGroupBox("4) Execute")
149
149
  step4_layout = QHBoxLayout()
150
150
  step4_layout.setContentsMargins(15, 0, 15, 15)
151
151
  step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
@@ -200,7 +200,7 @@ class NewProjectDialog(BaseFormDialog):
200
200
  return count
201
201
  if self.input_widget.get_selection_mode() == 'files' and \
202
202
  self.input_widget.get_selected_files():
203
- self.n_image_files = len(self.input_widget.get_selected_files())
203
+ self.n_image_files = self.input_widget.num_selected_files()
204
204
  self.selected_filenames = self.input_widget.get_selected_filenames()
205
205
  else:
206
206
  self.n_image_files = count_image_files(self.input_widget.get_path())
@@ -20,6 +20,7 @@ class ProjectController(QObject):
20
20
  activate_window_requested = Signal()
21
21
  enable_save_actions_requested = Signal(bool)
22
22
  enable_sub_actions_requested = Signal(bool)
23
+ add_recent_file_requested = Signal(str)
23
24
 
24
25
  def __init__(self, parent):
25
26
  super().__init__(parent)
@@ -242,6 +243,7 @@ class ProjectController(QObject):
242
243
  self.parent, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
243
244
  if file_path:
244
245
  try:
246
+ abs_file_path = os.path.abspath(file_path)
245
247
  self.set_current_file_path(file_path)
246
248
  with open(self.current_file_path(), 'r', encoding="utf-8") as file:
247
249
  json_obj = json.load(file)
@@ -250,6 +252,7 @@ class ProjectController(QObject):
250
252
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
251
253
  self.set_project(project)
252
254
  self.mark_as_modified(False)
255
+ self.add_recent_file_requested.emit(abs_file_path)
253
256
  self.project_editor.reset_undo()
254
257
  self.refresh_ui(0, -1)
255
258
  if self.job_list_count() > 0:
@@ -311,6 +314,7 @@ class ProjectController(QObject):
311
314
  f.write(json_obj)
312
315
  self.mark_as_modified(False)
313
316
  self.update_title_requested.emit()
317
+ self.add_recent_file_requested.emit(file_path)
314
318
  except Exception as e:
315
319
  QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
316
320
 
@@ -268,7 +268,7 @@ class ProjectEditor(QObject):
268
268
  if os.path.isabs(in_path):
269
269
  in_path = ".../" + os.path.basename(in_path)
270
270
  ico = constants.ACTION_ICONS[constants.ACTION_JOB]
271
- return txt + (f" [{ico}Job: 📁 {in_path} → 📂 ...]" if long_name else "")
271
+ return txt + (f" [{ico}Job] - 📁 {in_path} → 📂 ..." if long_name else "")
272
272
 
273
273
  def action_text(self, action, is_sub_action=False, indent=True, long_name=False, html=False):
274
274
  ico = constants.ACTION_ICONS.get(action.type_name, '')
@@ -285,9 +285,9 @@ class ProjectEditor(QObject):
285
285
  in_path = ".../" + os.path.basename(in_path)
286
286
  if os.path.isabs(out_path):
287
287
  out_path = ".../" + os.path.basename(out_path)
288
- return f"{txt} [{ico}{action.type_name}" + \
289
- (f": 📁 <i>{in_path}</i> → 📂 <i>{out_path}</i>]"
290
- if long_name and not is_sub_action else "]")
288
+ return f"{txt} [{ico}{action.type_name}]" + \
289
+ (f" - 📁 <i>{in_path}</i> → 📂 <i>{out_path}</i>"
290
+ if long_name and not is_sub_action else "")
291
291
 
292
292
  def get_job_at(self, index):
293
293
  return None if index < 0 else self.project_job(index)
@@ -495,6 +495,8 @@ class ProjectEditor(QObject):
495
495
  text = self.action_text(action, long_name=True, html=True, is_sub_action=is_sub_action)
496
496
  item = QListWidgetItem()
497
497
  item.setText('')
498
+ item.setToolTip("<b>Double-click:</b> configure parameters<br>"
499
+ "<b>Right-click:</b> show menu")
498
500
  item.setData(Qt.ItemDataRole.UserRole, True)
499
501
  widget_list.addItem(item)
500
502
  html_text = f"✅ <span style='color:#{ColorPalette.DARK_BLUE.hex()};'>{text}</span>" \
@@ -0,0 +1,93 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
2
+ import os
3
+ from PySide6.QtCore import QStandardPaths
4
+
5
+
6
+ class RecentFileManager:
7
+ def __init__(self, filename, max_entries=10):
8
+ self.filename = filename
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
+
29
+ def get_files(self):
30
+ file_path = self.get_file_path()
31
+ try:
32
+ with open(file_path, 'r', encoding='utf-8') as f:
33
+ files = [line.strip() for line in f.readlines()]
34
+ return [f for f in files if f and os.path.exists(f)]
35
+ except (FileNotFoundError, IOError):
36
+ return []
37
+
38
+ def get_files_with_display_names(self):
39
+ files = self.get_files()
40
+ basename_count = {}
41
+ for file_path in files:
42
+ basename = os.path.basename(file_path)
43
+ basename_count[basename] = basename_count.get(basename, 0) + 1
44
+ result = {}
45
+ for file_path in files:
46
+ basename = os.path.basename(file_path)
47
+ if basename_count[basename] == 1:
48
+ result[file_path] = basename
49
+ else:
50
+ parent_dir = os.path.basename(os.path.dirname(file_path))
51
+ result[file_path] = f"{basename} ({parent_dir})"
52
+ if list(result.values()).count(result[file_path]) > 1:
53
+ path_components = file_path.split(os.sep)
54
+ for i in range(2, min(5, len(path_components))):
55
+ display_name = os.sep.join(path_components[-i:])
56
+ if sum(1 for f in files
57
+ if os.sep.join(os.path.normpath(f).split(os.sep)[-i:]) ==
58
+ display_name) == 1:
59
+ result[file_path] = display_name
60
+ break
61
+ else:
62
+ if len(file_path) > 50:
63
+ result[file_path] = "..." + file_path[-47:]
64
+ else:
65
+ result[file_path] = file_path
66
+ return result
67
+
68
+ def add_file(self, file_path):
69
+ file_path = os.path.abspath(file_path)
70
+ recent_files = self.get_files()
71
+ if file_path in recent_files:
72
+ recent_files.remove(file_path)
73
+ recent_files.insert(0, file_path)
74
+ recent_files = recent_files[:self.max_entries]
75
+ self._save_files(recent_files)
76
+
77
+ def remove_file(self, file_path):
78
+ file_path = os.path.normpath(file_path)
79
+ recent_files = self.get_files()
80
+ if file_path in recent_files:
81
+ recent_files.remove(file_path)
82
+ self._save_files(recent_files)
83
+
84
+ def _save_files(self, files):
85
+ file_path = self.get_file_path()
86
+ try:
87
+ with open(file_path, 'w', encoding='utf-8') as f:
88
+ f.write('\n'.join(files))
89
+ except IOError as e:
90
+ raise e
91
+
92
+ def clear(self):
93
+ self._save_files([])
@@ -2,6 +2,7 @@
2
2
  import psutil
3
3
  from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QProgressBar, QSizePolicy
4
4
  from PySide6.QtCore import QTimer, Qt
5
+ from .colors import ColorPalette
5
6
 
6
7
 
7
8
  class StatusBarSystemMonitor(QWidget):
@@ -13,7 +14,7 @@ class StatusBarSystemMonitor(QWidget):
13
14
 
14
15
  def setup_ui(self):
15
16
  bar_width = 100
16
- bar_height = 20
17
+ bar_height = 22
17
18
  layout = QHBoxLayout()
18
19
  layout.setSpacing(10)
19
20
  layout.setContentsMargins(0, 2, 0, 0)
@@ -25,27 +26,27 @@ class StatusBarSystemMonitor(QWidget):
25
26
  self.cpu_bar.setRange(0, 100)
26
27
  self.cpu_bar.setTextVisible(False)
27
28
  self.cpu_bar.setGeometry(0, 0, bar_width, bar_height)
28
- self.cpu_bar.setStyleSheet("""
29
- QProgressBar {
29
+ self.cpu_bar.setStyleSheet(f"""
30
+ QProgressBar {{
30
31
  border: 1px solid #cccccc;
31
32
  border-radius: 5px;
32
- background: #f0f0f0;
33
- }
34
- QProgressBar::chunk {
35
- background-color: #3498db;
33
+ background: #F0F0F0;
34
+ }}
35
+ QProgressBar::chunk {{
36
+ background-color: #{ColorPalette.LIGHT_BLUE.hex()};
36
37
  border-radius: 5px;
37
- }
38
+ }}
38
39
  """)
39
40
  self.cpu_label = QLabel("CPU: --%", cpu_widget)
40
41
  self.cpu_label.setAlignment(Qt.AlignCenter)
41
42
  self.cpu_label.setGeometry(0, 0, bar_width, bar_height)
42
- self.cpu_label.setStyleSheet("""
43
- QLabel {
44
- color: #2c3e50;
43
+ self.cpu_label.setStyleSheet(f"""
44
+ QLabel {{
45
+ color: #{ColorPalette.DARK_BLUE.hex()};
45
46
  font-weight: bold;
46
47
  background: transparent;
47
48
  font-size: 12px;
48
- }
49
+ }}
49
50
  """)
50
51
  mem_widget = QWidget()
51
52
  mem_widget.setFixedSize(bar_width, bar_height)
@@ -53,27 +54,27 @@ class StatusBarSystemMonitor(QWidget):
53
54
  self.mem_bar.setRange(0, 100)
54
55
  self.mem_bar.setTextVisible(False)
55
56
  self.mem_bar.setGeometry(0, 0, bar_width, bar_height)
56
- self.mem_bar.setStyleSheet("""
57
- QProgressBar {
58
- border: 1px solid #cccccc;
57
+ self.mem_bar.setStyleSheet(f"""
58
+ QProgressBar {{
59
+ border: 1px solid #CCCCCC;
59
60
  border-radius: 5px;
60
61
  background: #f0f0f0;
61
- }
62
- QProgressBar::chunk {
63
- background-color: #2ecc71;
62
+ }}
63
+ QProgressBar::chunk {{
64
+ background-color: #{ColorPalette.LIGHT_GREEN.hex()};
64
65
  border-radius: 5px;
65
- }
66
+ }}
66
67
  """)
67
68
  self.mem_label = QLabel("MEM: --%", mem_widget)
68
69
  self.mem_label.setAlignment(Qt.AlignCenter)
69
70
  self.mem_label.setGeometry(0, 0, bar_width, bar_height)
70
- self.mem_label.setStyleSheet("""
71
- QLabel {
72
- color: #2c3e50;
71
+ self.mem_label.setStyleSheet(f"""
72
+ QLabel {{
73
+ color: #{ColorPalette.DARK_BLUE.hex()};
73
74
  font-weight: bold;
74
75
  background: transparent;
75
76
  font-size: 12px;
76
- }
77
+ }}
77
78
  """)
78
79
  layout.addWidget(cpu_widget)
79
80
  layout.addWidget(mem_widget)
@@ -48,7 +48,7 @@ class BaseFilter(ABC):
48
48
  nonlocal active_worker, dialog_closed # noqa
49
49
  dialog_closed = True
50
50
  self.editor.restore_master_layer()
51
- self.editor.display_manager.display_master_layer()
51
+ self.editor.image_viewer.update_master_display()
52
52
  if active_worker and active_worker.isRunning():
53
53
  active_worker.wait()
54
54
  initial_timer.stop()
@@ -62,10 +62,10 @@ class BaseFilter(ABC):
62
62
  current_region = self.editor.image_viewer.get_visible_image_portion()[1]
63
63
  if current_region == region:
64
64
  self.editor.set_master_layer(img)
65
- self.editor.display_manager.display_master_layer()
65
+ self.editor.image_viewer.update_master_display()
66
66
  else:
67
67
  self.editor.set_master_layer(img)
68
- self.editor.display_manager.display_master_layer()
68
+ self.editor.image_viewer.update_master_display()
69
69
  try:
70
70
  dlg.activateWindow()
71
71
  except Exception:
@@ -125,7 +125,7 @@ class BaseFilter(ABC):
125
125
 
126
126
  def restore_original():
127
127
  self.editor.restore_master_layer()
128
- self.editor.display_manager.display_master_layer()
128
+ self.editor.image_viewer.update_master_display()
129
129
  try:
130
130
  dlg.activateWindow()
131
131
  except Exception:
@@ -155,7 +155,7 @@ class BaseFilter(ABC):
155
155
  final_img = self.apply(self.editor.master_layer_copy(), *params)
156
156
  self.editor.set_master_layer(final_img)
157
157
  self.editor.copy_master_layer()
158
- self.editor.display_manager.display_master_layer()
158
+ self.editor.image_viewer.update_master_display()
159
159
  self.editor.display_manager.update_master_thumbnail()
160
160
  self.editor.mark_as_modified()
161
161
  else:
@@ -41,6 +41,7 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
41
41
  self.setVisible(False)
42
42
  self.setZValue(500)
43
43
  self.setTransformationMode(Qt.SmoothTransformation)
44
+ self.brush = None
44
45
 
45
46
  def get_layer_area(self, layer, x, y, w, h):
46
47
  if not isinstance(layer, np.ndarray):
@@ -64,6 +65,8 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
64
65
  raise RuntimeError("Bitmas is neither 8 bit nor 16, but of type " + area.dtype)
65
66
 
66
67
  def update(self, scene_pos, size):
68
+ if self.brush is None:
69
+ return
67
70
  try:
68
71
  if self.layer_collection is None or self.number_of_layers() == 0 or size <= 0:
69
72
  self.hide()
@@ -12,7 +12,7 @@ from .brush_preview import create_brush_mask
12
12
  class BrushTool:
13
13
  def __init__(self):
14
14
  self.brush = None
15
- self.brush_preview = None
15
+ self.brush_preview_widget = None
16
16
  self.image_viewer = None
17
17
  self.size_slider = None
18
18
  self.hardness_slider = None
@@ -21,11 +21,11 @@ class BrushTool:
21
21
  self._brush_mask_cache = {}
22
22
  self.brush_text = None
23
23
 
24
- def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
24
+ def setup_ui(self, brush, brush_preview_widget, image_viewer, size_slider, hardness_slider,
25
25
  opacity_slider, flow_slider):
26
26
  self.brush = brush
27
- self.brush_preview = brush_preview
28
- self.brush_text = QLabel(brush_preview.parent())
27
+ self.brush_preview_widget = brush_preview_widget
28
+ self.brush_text = QLabel(brush_preview_widget.parent())
29
29
  self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
30
  self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
31
  self.brush_text.raise_()
@@ -96,13 +96,13 @@ class BrushTool:
96
96
  preview_size = min(self.brush.size, width + 30, height + 30)
97
97
  center_x, center_y = width // 2, height // 2
98
98
  radius = preview_size // 2
99
- if self.image_viewer.cursor_style == 'preview':
99
+ if self.image_viewer.strategy.cursor_style == 'preview':
100
100
  gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
101
101
  painter.setBrush(QBrush(gradient))
102
102
  painter.setPen(
103
103
  QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
104
104
  gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
105
- elif self.image_viewer.cursor_style == 'outline':
105
+ elif self.image_viewer.strategy.cursor_style == 'outline':
106
106
  painter.setBrush(Qt.NoBrush)
107
107
  painter.setPen(
108
108
  QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
@@ -113,7 +113,7 @@ class BrushTool:
113
113
  QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
114
114
  gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
115
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
116
- if self.image_viewer.cursor_style == 'preview':
116
+ if self.image_viewer.strategy.cursor_style == 'preview':
117
117
  painter.setPen(QPen(QColor(0, 0, 160)))
118
118
  font = QApplication.font()
119
119
  painter.setFont(font)
@@ -126,13 +126,13 @@ class BrushTool:
126
126
  f"Flow: {self.brush.flow}%"
127
127
  )
128
128
  self.brush_text.adjustSize()
129
- self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
129
+ self.brush_text.move(10, self.brush_preview_widget.height() // 2 + 125)
130
130
  self.brush_text.show()
131
131
  else:
132
132
  self.brush_text.hide()
133
133
  painter.end()
134
- self.brush_preview.setPixmap(pixmap)
135
- self.image_viewer.update_brush_cursor()
134
+ self.brush_preview_widget.setPixmap(pixmap)
135
+ self.image_viewer.strategy.update_brush_cursor()
136
136
 
137
137
  def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
138
138
  view_pos):
@@ -140,7 +140,7 @@ class BrushTool:
140
140
  return False
141
141
  if dest_layer is None:
142
142
  dest_layer = master_layer
143
- scene_pos = self.image_viewer.mapToScene(view_pos)
143
+ scene_pos = self.image_viewer.strategy.map_to_scene(view_pos)
144
144
  x_center = int(round(scene_pos.x()))
145
145
  y_center = int(round(scene_pos.y()))
146
146
  radius = int(round(self.brush.size // 2))