shinestacker 0.3.6__py3-none-any.whl → 0.5.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 (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +37 -20
  3. shinestacker/algorithms/balance.py +2 -1
  4. shinestacker/algorithms/base_stack_algo.py +2 -1
  5. shinestacker/algorithms/multilayer.py +11 -8
  6. shinestacker/algorithms/noise_detection.py +13 -7
  7. shinestacker/algorithms/stack.py +5 -4
  8. shinestacker/algorithms/stack_framework.py +12 -10
  9. shinestacker/app/about_dialog.py +69 -1
  10. shinestacker/app/main.py +1 -1
  11. shinestacker/config/config.py +1 -0
  12. shinestacker/config/constants.py +8 -1
  13. shinestacker/config/gui_constants.py +7 -5
  14. shinestacker/core/framework.py +15 -10
  15. shinestacker/gui/action_config.py +11 -7
  16. shinestacker/gui/actions_window.py +8 -0
  17. shinestacker/gui/gui_logging.py +8 -7
  18. shinestacker/gui/gui_run.py +8 -8
  19. shinestacker/gui/main_window.py +17 -12
  20. shinestacker/gui/new_project.py +31 -17
  21. shinestacker/gui/project_converter.py +0 -1
  22. shinestacker/gui/select_path_widget.py +3 -1
  23. shinestacker/retouch/brush_tool.py +23 -6
  24. shinestacker/retouch/display_manager.py +57 -20
  25. shinestacker/retouch/image_editor.py +5 -9
  26. shinestacker/retouch/image_editor_ui.py +55 -16
  27. shinestacker/retouch/image_viewer.py +104 -20
  28. shinestacker/retouch/io_gui_handler.py +74 -24
  29. shinestacker/retouch/io_manager.py +23 -8
  30. shinestacker/retouch/layer_collection.py +2 -1
  31. {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/METADATA +5 -4
  32. {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/RECORD +36 -36
  33. shinestacker-0.5.0.dist-info/licenses/LICENSE +165 -0
  34. shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
  35. {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/WHEEL +0 -0
  36. {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-0.3.6.dist-info → shinestacker-0.5.0.dist-info}/top_level.txt +0 -0
@@ -30,6 +30,7 @@ class ActionsWindow(ProjectEditor):
30
30
  def mark_as_modified(self):
31
31
  self._modified_project = True
32
32
  self.project_buffer.append(self.project.clone())
33
+ self.save_actions_set_enabled(True)
33
34
  self.update_title()
34
35
 
35
36
  def close_project(self):
@@ -40,6 +41,7 @@ class ActionsWindow(ProjectEditor):
40
41
  self.job_list.clear()
41
42
  self.action_list.clear()
42
43
  self._modified_project = False
44
+ self.save_actions_set_enabled(False)
43
45
 
44
46
  def new_project(self):
45
47
  if not self._check_unsaved_changes():
@@ -51,8 +53,10 @@ class ActionsWindow(ProjectEditor):
51
53
  self.job_list.clear()
52
54
  self.action_list.clear()
53
55
  self.set_project(Project())
56
+ self.save_actions_set_enabled(False)
54
57
  dialog = NewProjectDialog(self)
55
58
  if dialog.exec() == QDialog.Accepted:
59
+ self.save_actions_set_enabled(True)
56
60
  input_folder = dialog.get_input_folder().split('/')
57
61
  working_path = '/'.join(input_folder[:-1])
58
62
  input_path = input_folder[-1]
@@ -142,6 +146,7 @@ class ActionsWindow(ProjectEditor):
142
146
  if len(self.project.jobs) > 0:
143
147
  self.job_list.setCurrentRow(0)
144
148
  self.activateWindow()
149
+ self.save_actions_set_enabled(True)
145
150
  for job in self.project.jobs:
146
151
  if 'working_path' in job.params.keys():
147
152
  working_path = job.params['working_path']
@@ -256,3 +261,6 @@ class ActionsWindow(ProjectEditor):
256
261
  if dialog.exec() == QDialog.Accepted:
257
262
  self.on_job_selected(self.job_list.currentRow())
258
263
  self.mark_as_modified()
264
+
265
+ def save_actions_set_enabled(self, enabled):
266
+ pass
@@ -17,6 +17,7 @@ class SimpleHtmlFormatter(logging.Formatter):
17
17
  FF = '80'
18
18
  OO = '00'
19
19
  MM = '40'
20
+ GG = 'FF'
20
21
  ANSI_COLORS = {
21
22
  # Reset
22
23
  '\x1b[0m': '</span>',
@@ -32,13 +33,13 @@ class SimpleHtmlFormatter(logging.Formatter):
32
33
  '\x1b[37m': f'<span style="color:#{FF}{FF}{FF}">', # white
33
34
  # Brilliant colors (90-97)
34
35
  '\x1b[90m': f'<span style="color:#{MM}{MM}{MM}">',
35
- '\x1b[91m': f'<span style="color:#{FF}{MM}{MM}">',
36
- '\x1b[92m': f'<span style="color:#{MM}{FF}{MM}">',
37
- '\x1b[93m': f'<span style="color:#{FF}{FF}{MM}">',
38
- '\x1b[94m': f'<span style="color:#{MM}{MM}{FF}">',
39
- '\x1b[95m': f'<span style="color:#{FF}{MM}{FF}">',
40
- '\x1b[96m': f'<span style="color:#{MM}{FF}{FF}">',
41
- '\x1b[97m': f'<span style="color:#{FF}{FF}{FF}">',
36
+ '\x1b[91m': f'<span style="color:#{GG}{MM}{MM}">',
37
+ '\x1b[92m': f'<span style="color:#{MM}{GG}{MM}">',
38
+ '\x1b[93m': f'<span style="color:#{GG}{GG}{MM}">',
39
+ '\x1b[94m': f'<span style="color:#{MM}{MM}{GG}">',
40
+ '\x1b[95m': f'<span style="color:#{GG}{MM}{GG}">',
41
+ '\x1b[96m': f'<span style="color:#{MM}{GG}{GG}">',
42
+ '\x1b[97m': f'<span style="color:#{GG}{GG}{GG}">',
42
43
  # Background (40-47)
43
44
  '\x1b[40m': f'<span style="background-color:#{OO}{OO}{OO}">',
44
45
  '\x1b[41m': f'<span style="background-color:#{FF}{OO}{OO}">',
@@ -13,6 +13,10 @@ from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
13
13
  from .colors import ColorPalette
14
14
 
15
15
 
16
+ ACTION_RUNNING_COLOR = ColorPalette.MEDIUM_BLUE
17
+ ACTION_DONE_COLOR = ColorPalette.MEDIUM_GREEN
18
+
19
+
16
20
  class ColorButton(QPushButton):
17
21
  def __init__(self, text, enabled, parent=None):
18
22
  super().__init__(text.replace(gui_constants.DISABLED_TAG, ''), parent)
@@ -36,10 +40,6 @@ class ColorButton(QPushButton):
36
40
  """)
37
41
 
38
42
 
39
- action_running_color = ColorPalette.MEDIUM_BLUE
40
- action_done_color = ColorPalette.MEDIUM_GREEN
41
-
42
-
43
43
  class TimerProgressBar(QProgressBar):
44
44
  light_background_color = ColorPalette.LIGHT_BLUE
45
45
  border_color = ColorPalette.DARK_BLUE
@@ -123,10 +123,10 @@ class TimerProgressBar(QProgressBar):
123
123
  # pylint: enable=C0103
124
124
 
125
125
  def set_running_style(self):
126
- self.set_style(action_running_color)
126
+ self.set_style(ACTION_RUNNING_COLOR)
127
127
 
128
128
  def set_done_style(self):
129
- self.set_style(action_done_color)
129
+ self.set_style(ACTION_DONE_COLOR)
130
130
 
131
131
 
132
132
  class RunWindow(QTextEditLogger):
@@ -246,7 +246,7 @@ class RunWindow(QTextEditLogger):
246
246
  @Slot(int, str)
247
247
  def handle_before_action(self, run_id, _name):
248
248
  if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
249
- self.color_widgets[self.row_widget_id][run_id].set_color(*action_running_color.tuple())
249
+ self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_RUNNING_COLOR.tuple())
250
250
  self.progress_bar.start(1)
251
251
  if run_id == -1:
252
252
  self.progress_bar.set_running_style()
@@ -254,7 +254,7 @@ class RunWindow(QTextEditLogger):
254
254
  @Slot(int, str)
255
255
  def handle_after_action(self, run_id, _name):
256
256
  if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
257
- self.color_widgets[self.row_widget_id][run_id].set_color(*action_done_color.tuple())
257
+ self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_DONE_COLOR.tuple())
258
258
  self.progress_bar.stop()
259
259
  if run_id == -1:
260
260
  self.row_widget_id += 1
@@ -163,11 +163,11 @@ class MainWindow(ActionsWindow, LogManager):
163
163
  self.action_list.itemDoubleClicked.connect(self.on_action_edit)
164
164
  vbox_left = QVBoxLayout()
165
165
  vbox_left.setSpacing(4)
166
- vbox_left.addWidget(QLabel("Jobs"))
166
+ vbox_left.addWidget(QLabel("Job"))
167
167
  vbox_left.addWidget(self.job_list)
168
168
  vbox_right = QVBoxLayout()
169
169
  vbox_right.setSpacing(4)
170
- vbox_right.addWidget(QLabel("Actions"))
170
+ vbox_right.addWidget(QLabel("Action"))
171
171
  vbox_right.addWidget(self.action_list)
172
172
  self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
173
173
  self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
@@ -189,19 +189,24 @@ class MainWindow(ActionsWindow, LogManager):
189
189
  open_action.setShortcut("Ctrl+O")
190
190
  open_action.triggered.connect(self.open_project)
191
191
  menu.addAction(open_action)
192
- save_action = QAction("&Save", self)
193
- save_action.setShortcut("Ctrl+S")
194
- save_action.triggered.connect(self.save_project)
195
- menu.addAction(save_action)
196
- save_as_action = QAction("Save &As...", self)
197
- save_as_action.setShortcut("Ctrl+Shift+S")
198
- save_as_action.triggered.connect(self.save_project_as)
199
- menu.addAction(save_as_action)
192
+ self.save_action = QAction("&Save", self)
193
+ self.save_action.setShortcut("Ctrl+S")
194
+ self.save_action.triggered.connect(self.save_project)
195
+ menu.addAction(self.save_action)
196
+ self.save_as_action = QAction("Save &As...", self)
197
+ self.save_as_action.setShortcut("Ctrl+Shift+S")
198
+ self.save_as_action.triggered.connect(self.save_project_as)
199
+ menu.addAction(self.save_as_action)
200
+ self.save_actions_set_enabled(False)
200
201
  close_action = QAction("&Close", self)
201
202
  close_action.setShortcut("Ctrl+W")
202
203
  close_action.triggered.connect(self.close_project)
203
204
  menu.addAction(close_action)
204
205
 
206
+ def save_actions_set_enabled(self, enabled):
207
+ self.save_action.setEnabled(enabled)
208
+ self.save_as_action.setEnabled(enabled)
209
+
205
210
  def add_edit_menu(self, menubar):
206
211
  menu = menubar.addMenu("&Edit")
207
212
  undo_action = QAction("&Undo", self)
@@ -615,7 +620,7 @@ class MainWindow(ActionsWindow, LogManager):
615
620
  labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
616
621
  r = self.get_retouch_path(job)
617
622
  retouch_paths = [] if len(r) == 0 else [(job_name, r)]
618
- new_window, id_str = self.create_new_window("Job: " + job_name,
623
+ new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
619
624
  labels, retouch_paths)
620
625
  worker = JobLogWorker(job, id_str)
621
626
  self.connect_signals(worker, new_window)
@@ -638,7 +643,7 @@ class MainWindow(ActionsWindow, LogManager):
638
643
  r = self.get_retouch_path(job)
639
644
  if len(r) > 0:
640
645
  retouch_paths.append((job.params["name"], r))
641
- new_window, id_str = self.create_new_window("Project: " + project_name,
646
+ new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
642
647
  labels, retouch_paths)
643
648
  worker = ProjectLogWorker(self.project, id_str)
644
649
  self.connect_signals(worker, new_window)
@@ -9,6 +9,8 @@ from .. config.constants import constants
9
9
  from .. algorithms.stack import get_bunches
10
10
  from .select_path_widget import create_select_file_paths_widget
11
11
 
12
+ DEFAULT_NO_COUNT_LABEL = " - "
13
+
12
14
 
13
15
  class NewProjectDialog(QDialog):
14
16
  def __init__(self, parent=None):
@@ -51,8 +53,9 @@ class NewProjectDialog(QDialog):
51
53
  spacer.setFixedHeight(10)
52
54
  self.layout.addRow(spacer)
53
55
 
54
- container = create_select_file_paths_widget('', 'input files folder', 'input files folder')
55
-
56
+ self.input_folder, container = create_select_file_paths_widget(
57
+ '', 'input files folder', 'input files folder')
58
+ self.input_folder.textChanged.connect(self.update_bunches_label)
56
59
  self.noise_detection = QCheckBox()
57
60
  self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
58
61
  self.vignetting_correction = QCheckBox()
@@ -72,7 +75,8 @@ class NewProjectDialog(QDialog):
72
75
  bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
73
76
  self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
74
77
  self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
75
- self.bunches_label = QLabel("")
78
+ self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
79
+ self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
76
80
 
77
81
  self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
78
82
  self.bunch_stack.toggled.connect(self.update_bunch_options)
@@ -88,6 +92,7 @@ class NewProjectDialog(QDialog):
88
92
 
89
93
  self.add_bold_label("Select input:")
90
94
  self.layout.addRow("Input folder:", container)
95
+ self.layout.addRow("Number of frames: ", self.frames_label)
91
96
  self.add_bold_label("Select actions:")
92
97
  if self.expert():
93
98
  self.layout.addRow("Automatic noise detection:", self.noise_detection)
@@ -111,25 +116,34 @@ class NewProjectDialog(QDialog):
111
116
  self.update_bunches_label()
112
117
 
113
118
  def update_bunches_label(self):
119
+ if not self.input_folder.text():
120
+ return
121
+
122
+ def count_image_files(path):
123
+ if path == '' or not os.path.isdir(path):
124
+ return 0
125
+ extensions = ['jpg', 'jpeg', 'tif', 'tiff']
126
+ count = 0
127
+ for filename in os.listdir(path):
128
+ if '.' in filename:
129
+ ext = filename.lower().split('.')[-1]
130
+ if ext in extensions:
131
+ count += 1
132
+ return count
133
+
134
+ n_image_files = count_image_files(self.input_folder.text())
135
+ if n_image_files == 0:
136
+ self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
137
+ self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
138
+ return
139
+ self.frames_label.setText(f"{n_image_files}")
114
140
  if self.bunch_stack.isChecked():
115
- def count_image_files(path):
116
- if path == '' or not os.path.isdir(path):
117
- return 0
118
- extensions = ['jpg', 'jpeg', 'tif', 'tiff']
119
- count = 0
120
- for filename in os.listdir(path):
121
- if '.' in filename:
122
- ext = filename.lower().split('.')[-1]
123
- if ext in extensions:
124
- count += 1
125
- return count
126
-
127
- bunches = get_bunches(list(range(count_image_files(self.input_folder.text()))),
141
+ bunches = get_bunches(list(range(n_image_files)),
128
142
  self.bunch_frames.value(),
129
143
  self.bunch_overlap.value())
130
144
  self.bunches_label.setText(f"{len(bunches)}")
131
145
  else:
132
- self.bunches_label.setText(" - ")
146
+ self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
133
147
 
134
148
  def accept(self):
135
149
  input_folder = self.input_folder.text()
@@ -112,7 +112,6 @@ class ProjectConverter:
112
112
  else:
113
113
  raise InvalidOptionError('stacker', stacker, f"valid options are: "
114
114
  f"{constants.STACK_ALGO_PYRAMID}, "
115
- f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
116
115
  f"{constants.STACK_ALGO_DEPTH_MAP}")
117
116
  if action_config.type_name == constants.ACTION_FOCUSSTACK:
118
117
  return FocusStack(**module_dict, stack_algo=stack_algo)
@@ -9,6 +9,7 @@ def create_layout_widget_no_margins(layout):
9
9
  container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
10
10
  return container
11
11
 
12
+
12
13
  def create_layout_widget_and_connect(button, edit, browse):
13
14
  button.clicked.connect(browse)
14
15
  button.setAutoDefault(False)
@@ -17,6 +18,7 @@ def create_layout_widget_and_connect(button, edit, browse):
17
18
  layout.addWidget(button)
18
19
  return create_layout_widget_no_margins(layout)
19
20
 
21
+
20
22
  def create_select_file_paths_widget(value, placeholder, tag):
21
23
  edit = QLineEdit(value)
22
24
  edit.setPlaceholderText(placeholder)
@@ -27,4 +29,4 @@ def create_select_file_paths_widget(value, placeholder, tag):
27
29
  if path:
28
30
  edit.setText(path)
29
31
 
30
- return create_layout_widget_and_connect(button, edit, browse)
32
+ return edit, create_layout_widget_and_connect(button, edit, browse)
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
2
2
  import numpy as np
3
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
+ from PySide6.QtWidgets import QApplication, QLabel
4
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
4
5
  from PySide6.QtCore import Qt, QPoint
5
6
  from .brush_gradient import create_default_brush_gradient
6
7
  from .. config.gui_constants import gui_constants
@@ -18,11 +19,16 @@ class BrushTool:
18
19
  self.opacity_slider = None
19
20
  self.flow_slider = None
20
21
  self._brush_mask_cache = {}
22
+ self.brush_text = None
21
23
 
22
24
  def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
23
25
  opacity_slider, flow_slider):
24
26
  self.brush = brush
25
27
  self.brush_preview = brush_preview
28
+ self.brush_text = QLabel(brush_preview.parent())
29
+ self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
+ self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
+ self.brush_text.raise_()
26
32
  self.image_viewer = image_viewer
27
33
  self.size_slider = size_slider
28
34
  self.hardness_slider = hardness_slider
@@ -86,7 +92,7 @@ class BrushTool:
86
92
  pixmap = QPixmap(width, height)
87
93
  pixmap.fill(Qt.transparent)
88
94
  painter = QPainter(pixmap)
89
- painter.setRenderHint(QPainter.Antialiasing)
95
+ painter.setRenderHint(QPainter.TextAntialiasing, True)
90
96
  preview_size = min(self.brush.size, width + 30, height + 30)
91
97
  center_x, center_y = width // 2, height // 2
92
98
  radius = preview_size // 2
@@ -109,10 +115,21 @@ class BrushTool:
109
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
110
116
  if self.image_viewer.cursor_style == 'preview':
111
117
  painter.setPen(QPen(QColor(0, 0, 160)))
112
- painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
113
- painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
114
- painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
115
- painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
118
+ font = QApplication.font()
119
+ painter.setFont(font)
120
+ font.setHintingPreference(QFont.PreferFullHinting)
121
+ painter.setFont(font)
122
+ self.brush_text.setText(
123
+ f"Size: {int(self.brush.size)}px\n"
124
+ f"Hardness: {self.brush.hardness}%\n"
125
+ f"Opacity: {self.brush.opacity}%\n"
126
+ f"Flow: {self.brush.flow}%"
127
+ )
128
+ self.brush_text.adjustSize()
129
+ self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
130
+ self.brush_text.show()
131
+ else:
132
+ self.brush_text.hide()
116
133
  painter.end()
117
134
  self.brush_preview.setPixmap(pixmap)
118
135
  self.image_viewer.update_brush_cursor()
@@ -1,6 +1,7 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
2
2
  import numpy as np
3
- from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
3
+ from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
4
+ QAbstractItemView)
4
5
  from PySide6.QtGui import QPixmap, QImage
5
6
  from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
6
7
  from .. config.gui_constants import gui_constants
@@ -39,6 +40,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
39
40
  self.update_timer = QTimer()
40
41
  self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
41
42
  self.update_timer.timeout.connect(self.process_pending_updates)
43
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
42
44
 
43
45
  def process_pending_updates(self):
44
46
  if self.needs_update:
@@ -64,15 +66,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
64
66
  self.display_master_layer()
65
67
 
66
68
  def create_thumbnail(self, layer):
67
- if layer.dtype == np.uint16:
68
- layer = (layer // 256).astype(np.uint8)
69
- height, width = layer.shape[:2]
70
- if layer.ndim == 3 and layer.shape[-1] == 3:
71
- qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
69
+ source_layer = (layer // 256).astype(np.uint8) if layer.dtype == np.uint16 else layer
70
+ height, width = source_layer.shape[:2]
71
+ if layer.ndim == 3 and source_layer.shape[-1] == 3:
72
+ qimg = QImage(source_layer.data, width, height, 3 * width, QImage.Format_RGB888)
72
73
  else:
73
- qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
74
+ qimg = QImage(source_layer.data, width, height, width, QImage.Format_Grayscale8)
74
75
  return QPixmap.fromImage(
75
- qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
76
+ qimg.scaledToWidth(
77
+ gui_constants.UI_SIZES['thumbnail_width'], Qt.SmoothTransformation))
76
78
 
77
79
  def update_thumbnails(self):
78
80
  self.update_master_thumbnail()
@@ -103,17 +105,22 @@ class DisplayManager(QObject, LayerCollectionHandler):
103
105
  self.master_thumbnail_label.setPixmap(pixmap)
104
106
 
105
107
  def add_thumbnail_item(self, thumbnail, label, i, is_current):
106
- item_widget = QWidget()
107
- layout = QVBoxLayout(item_widget)
108
- layout.setContentsMargins(0, 0, 0, 0)
109
- layout.setSpacing(0)
110
-
108
+ container = QWidget()
109
+ container.setFixedWidth(gui_constants.UI_SIZES['thumbnail_width'] + 4)
110
+ container.setObjectName("thumbnailContainer")
111
+ container_layout = QVBoxLayout(container)
112
+ container_layout.setContentsMargins(2, 2, 2, 2)
113
+ container_layout.setSpacing(0)
114
+ content_widget = QWidget()
115
+ content_layout = QVBoxLayout(content_widget)
116
+ content_layout.setContentsMargins(0, 0, 0, 0)
117
+ content_layout.setSpacing(0)
111
118
  thumbnail_label = QLabel()
112
119
  thumbnail_label.setPixmap(thumbnail)
113
120
  thumbnail_label.setAlignment(Qt.AlignCenter)
114
- layout.addWidget(thumbnail_label)
115
-
121
+ content_layout.addWidget(thumbnail_label)
116
122
  label_widget = ClickableLabel(label)
123
+ label_widget.setFixedHeight(gui_constants.UI_SIZES['label_height'])
117
124
  label_widget.setAlignment(Qt.AlignCenter)
118
125
 
119
126
  def rename_label(label_widget, old_label, i):
@@ -124,21 +131,45 @@ class DisplayManager(QObject, LayerCollectionHandler):
124
131
  self.set_layer_labels(i, new_label)
125
132
 
126
133
  label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
127
- layout.addWidget(label_widget)
134
+ content_layout.addWidget(label_widget)
135
+ container_layout.addWidget(content_widget)
136
+ if is_current:
137
+ container.setStyleSheet(
138
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
139
+ else:
140
+ container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
128
141
  item = QListWidgetItem()
129
- item.setSizeHint(QSize(gui_constants.IMG_WIDTH, gui_constants.IMG_HEIGHT))
142
+ item.setSizeHint(QSize(gui_constants.UI_SIZES['thumbnail_width'] + 4,
143
+ thumbnail.height() + label_widget.height() + 4))
130
144
  self.thumbnail_list.addItem(item)
131
- self.thumbnail_list.setItemWidget(item, item_widget)
132
-
145
+ self.thumbnail_list.setItemWidget(item, container)
133
146
  if is_current:
134
147
  self.thumbnail_list.setCurrentItem(item)
135
148
 
149
+ def highlight_thumbnail(self, index):
150
+ for i in range(self.thumbnail_list.count()):
151
+ item = self.thumbnail_list.item(i)
152
+ widget = self.thumbnail_list.itemWidget(item)
153
+ if widget:
154
+ widget.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
155
+ current_item = self.thumbnail_list.item(index)
156
+ if current_item:
157
+ widget = self.thumbnail_list.itemWidget(current_item)
158
+ if widget:
159
+ widget.setStyleSheet(
160
+ f"#thumbnailContainer{{ border: 2px solid {self.thumbnail_highlight}; }}")
161
+ self.thumbnail_list.setCurrentRow(index)
162
+ self.thumbnail_list.scrollToItem(
163
+ self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
164
+
136
165
  def set_view_master(self):
137
166
  if self.has_no_master_layer():
138
167
  return
139
168
  self.view_mode = 'master'
140
169
  self.temp_view_individual = False
141
170
  self.display_master_layer()
171
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
172
+ self.highlight_thumbnail(self.current_layer_idx())
142
173
  self.status_message_requested.emit("View mode: Master")
143
174
  self.cursor_preview_state_changed.emit(True) # True = allow preview
144
175
 
@@ -148,6 +179,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
148
179
  self.view_mode = 'individual'
149
180
  self.temp_view_individual = False
150
181
  self.display_current_layer()
182
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
183
+ self.highlight_thumbnail(self.current_layer_idx())
151
184
  self.status_message_requested.emit("View mode: Individual layers")
152
185
  self.cursor_preview_state_changed.emit(False) # False = no preview
153
186
 
@@ -155,6 +188,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
155
188
  if not self.temp_view_individual and self.view_mode == 'master':
156
189
  self.temp_view_individual = True
157
190
  self.image_viewer.update_brush_cursor()
191
+ self.thumbnail_highlight = gui_constants.THUMB_HI_COLOR
192
+ self.highlight_thumbnail(self.current_layer_idx())
158
193
  self.display_current_layer()
159
194
  self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
160
195
 
@@ -162,6 +197,8 @@ class DisplayManager(QObject, LayerCollectionHandler):
162
197
  if self.temp_view_individual:
163
198
  self.temp_view_individual = False
164
199
  self.image_viewer.update_brush_cursor()
200
+ self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
201
+ self.highlight_thumbnail(self.current_layer_idx())
165
202
  self.display_master_layer()
166
203
  self.status_message_requested.emit("View mode: Master")
167
204
  self.cursor_preview_state_changed.emit(True) # Restore preview
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902
2
- from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
2
+ from PySide6.QtWidgets import QMainWindow, QMessageBox
3
3
  from .. config.constants import constants
4
4
  from .undo_manager import UndoManager
5
5
  from .layer_collection import LayerCollection
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
87
87
  def update_title(self):
88
88
  title = constants.APP_TITLE
89
89
  if self.io_gui_handler is not None:
90
- path = self.io_gui_handler.io_manager.current_file_path
90
+ path = self.io_gui_handler.current_file_path()
91
91
  if path != '':
92
92
  title += f" - {path.split('/')[-1]}"
93
93
  if self.modified:
@@ -96,6 +96,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
96
96
 
97
97
  def mark_as_modified(self):
98
98
  self.modified = True
99
+ self.save_actions_set_enabled(True)
99
100
  self.update_title()
100
101
 
101
102
  def change_layer(self, layer_idx):
@@ -114,19 +115,14 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
114
115
  new_idx = max(0, self.current_layer_idx() - 1)
115
116
  if new_idx != self.current_layer_idx():
116
117
  self.change_layer(new_idx)
117
- self.highlight_thumbnail(new_idx)
118
+ self.display_manager.highlight_thumbnail(new_idx)
118
119
 
119
120
  def next_layer(self):
120
121
  if self.layer_stack() is not None:
121
122
  new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
122
123
  if new_idx != self.current_layer_idx():
123
124
  self.change_layer(new_idx)
124
- self.highlight_thumbnail(new_idx)
125
-
126
- def highlight_thumbnail(self, index):
127
- self.thumbnail_list.setCurrentRow(index)
128
- self.thumbnail_list.scrollToItem(
129
- self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
125
+ self.display_manager.highlight_thumbnail(new_idx)
130
126
 
131
127
  def copy_layer_to_master(self):
132
128
  if self.layer_stack() is None or self.master_layer() is None: