shinestacker 0.3.0__py3-none-any.whl → 0.3.1__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.

@@ -1,61 +1,28 @@
1
1
  import traceback
2
2
  import numpy as np
3
- import cv2
4
3
  from PySide6.QtWidgets import (QMainWindow, QFileDialog, QMessageBox, QAbstractItemView,
5
4
  QVBoxLayout, QLabel, QDialog, QApplication)
6
- from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush, QRadialGradient, QGuiApplication, QCursor
5
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush, QGuiApplication, QCursor
7
6
  from PySide6.QtCore import Qt, QTimer, QEvent, QPoint
8
7
  from .. config.constants import constants
9
8
  from .. config.gui_constants import gui_constants
10
- from .. core.exceptions import ShapeError, BitDepthError
11
- from .. algorithms.exif import get_exif, write_image_with_exif_data
12
- from .. algorithms.multilayer import write_multilayer_tiff_from_images
13
- from .. algorithms.utils import read_img, validate_image, get_img_metadata
14
9
  from .brush import Brush
15
10
  from .brush_controller import BrushController
16
11
  from .undo_manager import UndoManager
17
12
  from .file_loader import FileLoader
18
13
  from .exif_data import ExifData
19
-
20
-
21
- def slider_to_brush_size(slider_val):
22
- normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
23
- size = gui_constants.BRUSH_SIZES['min'] + gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
24
- return max(gui_constants.BRUSH_SIZES['min'], min(gui_constants.BRUSH_SIZES['max'], size))
25
-
26
-
27
- def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
28
- gradient = QRadialGradient(center_x, center_y, float(radius))
29
- inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
30
- outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
31
- inner_with_opacity = QColor(inner)
32
- inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
33
- if hardness < 100:
34
- hardness_normalized = float(hardness) / 100.0
35
- gradient.setColorAt(0.0, inner_with_opacity)
36
- gradient.setColorAt(hardness_normalized, inner_with_opacity)
37
- gradient.setColorAt(1.0, outer)
38
- else:
39
- gradient.setColorAt(0.0, inner_with_opacity)
40
- gradient.setColorAt(1.0, inner_with_opacity)
41
- return gradient
14
+ from .layer_collection import LayerCollection
15
+ from .io_manager import IOManager
16
+ from .brush_gradient import create_brush_gradient
42
17
 
43
18
 
44
19
  class ImageEditor(QMainWindow):
45
20
  def __init__(self):
46
21
  super().__init__()
47
- self.current_stack = None
48
- self.master_layer = None
49
- self.current_labels = None
50
- self.current_layer = 0
51
- self.shape = None
52
- self.dtype = None
53
- self._brush_mask_cache = {}
22
+ self.layer_collection = LayerCollection()
23
+ self.io_manager = IOManager(self.layer_collection)
54
24
  self.view_mode = 'master'
55
25
  self.temp_view_individual = False
56
- self.current_file_path = ''
57
- self.exif_path = ''
58
- self.exif_data = None
59
26
  self.modified = False
60
27
  self.installEventFilter(self)
61
28
  self.update_timer = QTimer(self)
@@ -119,43 +86,14 @@ class ImageEditor(QMainWindow):
119
86
  return True
120
87
 
121
88
  def sort_layers(self, order):
122
- if not hasattr(self, 'current_stack') or not hasattr(self, 'current_labels'):
123
- return
124
- master_index = -1
125
- master_label = None
126
- master_layer = None
127
- for i, label in enumerate(self.current_labels):
128
- if label.lower() == "master":
129
- master_index = i
130
- master_label = self.current_labels.pop(i)
131
- master_layer = self.current_stack[i]
132
- self.current_stack = np.delete(self.current_stack, i, axis=0)
133
- break
134
- if order == 'asc':
135
- self.sorted_indices = sorted(range(len(self.current_labels)),
136
- key=lambda i: self.current_labels[i].lower())
137
- elif order == 'desc':
138
- self.sorted_indices = sorted(range(len(self.current_labels)),
139
- key=lambda i: self.current_labels[i].lower(),
140
- reverse=True)
141
- else:
142
- raise ValueError(f"Invalid sorting order: {order}")
143
- self.current_labels = [self.current_labels[i] for i in self.sorted_indices]
144
- self.current_stack = self.current_stack[self.sorted_indices]
145
- if master_index != -1:
146
- self.current_labels.insert(0, master_label)
147
- self.current_stack = np.insert(self.current_stack, 0, master_layer, axis=0)
148
- self.master_layer = master_layer.copy()
149
- self.master_layer.setflags(write=True)
89
+ self.layer_collection.sort_layers(order)
150
90
  self.update_thumbnails()
151
- if self.current_layer >= len(self.current_stack):
152
- self.current_layer = len(self.current_stack) - 1
153
- self.change_layer(self.current_layer)
91
+ self.change_layer(self.layer_collection.current_layer)
154
92
 
155
93
  def update_title(self):
156
94
  title = constants.APP_TITLE
157
- if self.current_file_path:
158
- title += f" - {self.current_file_path.split('/')[-1]}"
95
+ if self.io_manager.current_file_path:
96
+ title += f" - {self.io_manager.current_file_path.split('/')[-1]}"
159
97
  if self.modified:
160
98
  title += " *"
161
99
  self.window().setWindowTitle(title)
@@ -173,7 +111,7 @@ class ImageEditor(QMainWindow):
173
111
  self.import_frames_from_files(file_paths)
174
112
  return
175
113
  path = file_paths[0] if isinstance(file_paths, list) else file_paths
176
- self.current_file_path = path
114
+ self.io_manager.current_file_path = path
177
115
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
178
116
  self.loading_dialog = QDialog(self)
179
117
  self.loading_dialog.setWindowTitle("Loading")
@@ -195,14 +133,12 @@ class ImageEditor(QMainWindow):
195
133
  QApplication.restoreOverrideCursor()
196
134
  self.loading_timer.stop()
197
135
  self.loading_dialog.hide()
198
- self.current_stack = stack
136
+ self.layer_collection.layer_stack = stack
199
137
  if labels is None:
200
- self.current_labels = [f'Layer {i:03d}' for i in range(len(stack))]
138
+ self.layer_collection.layer_labels = [f'Layer {i:03d}' for i in range(len(stack))]
201
139
  else:
202
- self.current_labels = labels
203
- self.master_layer = master_layer
204
- self.shape = np.array(master_layer).shape
205
- self.dtype = master_layer.dtype
140
+ self.layer_collection.layer_labels = labels
141
+ self.layer_collection.master_layer = master_layer
206
142
  self.modified = False
207
143
  self.undo_manager.reset()
208
144
  self.blank_layer = np.zeros(master_layer.shape[:2])
@@ -210,7 +146,7 @@ class ImageEditor(QMainWindow):
210
146
  self.image_viewer.setup_brush_cursor()
211
147
  self.change_layer(0)
212
148
  self.image_viewer.reset_zoom()
213
- self.statusBar().showMessage(f"Loaded: {self.current_file_path}")
149
+ self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
214
150
  self.thumbnail_list.setFocus()
215
151
  self.update_title()
216
152
 
@@ -220,7 +156,7 @@ class ImageEditor(QMainWindow):
220
156
  self.loading_dialog.accept()
221
157
  self.loading_dialog.deleteLater()
222
158
  QMessageBox.critical(self, "Error", error_msg)
223
- self.statusBar().showMessage(f"Error loading: {self.current_file_path}")
159
+ self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
224
160
 
225
161
  def mark_as_modified(self):
226
162
  self.modified = True
@@ -231,68 +167,32 @@ class ImageEditor(QMainWindow):
231
167
  "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
232
168
  if file_paths:
233
169
  self.import_frames_from_files(file_paths)
170
+ self.statusBar().showMessage("Imported selected frames")
234
171
 
235
172
  def import_frames_from_files(self, file_paths):
236
- if file_paths is None or len(file_paths) == 0:
173
+ try:
174
+ stack, labels, master = self.io_manager.import_frames(file_paths)
175
+ except Exception as e:
176
+ msg = QMessageBox()
177
+ msg.setIcon(QMessageBox.Critical)
178
+ msg.setWindowTitle("Import error")
179
+ msg.setText(str(e))
180
+ msg.exec()
237
181
  return
238
- if self.current_stack is None and len(file_paths) > 0:
239
- path = file_paths[0]
240
- img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
241
- self.current_stack = np.array([img])
242
- self.shape, self.dtype = get_img_metadata(img)
243
- label = path.split("/")[-1].split(".")[0]
244
- self.current_labels = [label]
245
- if self.master_layer is None:
246
- self.master_layer = img.copy()
247
- self.blank_layer = np.zeros(self.master_layer.shape[:2])
248
- next_paths = file_paths[1:]
182
+ if self.layer_collection.layer_stack is None and len(stack) > 0:
183
+ self.layer_collection.layer_stack = np.array(stack)
184
+ self.layer_collection.layer_labels = labels
185
+ self.layer_collection.master_layer = master
186
+ self.blank_layer = np.zeros(master.shape[:2])
249
187
  else:
250
- next_paths = file_paths
251
- for path in next_paths:
252
- try:
253
- label = path.split("/")[-1].split(".")[0]
254
- img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
255
- try:
256
- validate_image(img, self.shape, self.dtype)
257
- except ShapeError as e:
258
- msg = QMessageBox()
259
- msg.setIcon(QMessageBox.Critical)
260
- msg.setWindowTitle("Import error")
261
- msg.setText(f"All files must have the same shape.\n{str(e)}")
262
- msg.exec()
263
- return
264
- except BitDepthError as e:
265
- msg = QMessageBox()
266
- msg.setIcon(QMessageBox.Critical)
267
- msg.setWindowTitle("Import error")
268
- msg.setText(f"All flies must have the same bit depth.\n{str(e)}")
269
- msg.exec()
270
- return
271
- except Exception as e:
272
- traceback.print_tb(e.__traceback__)
273
- raise e
274
- return
275
- label_x = label
276
- i = 0
277
- while label_x in self.current_labels:
278
- i += 1
279
- label_x = f"{label} ({i})"
280
- self.current_labels.append(label_x)
281
- self.current_stack = np.append(self.current_stack, [img], axis=0)
282
- except Exception as e:
283
- traceback.print_tb(e.__traceback__)
284
- msg = QMessageBox()
285
- msg.setIcon(QMessageBox.Critical)
286
- msg.setWindowTitle("Import error")
287
- msg.setText(f"Error loading file: {path}.\n{str(e)}")
288
- msg.exec()
289
- self.statusBar().showMessage(f"Error loading file: {path}")
290
- break
188
+ for img, label in zip(stack, labels):
189
+ self.layer_collection.layer_labels.append(label)
190
+ self.layer_collection.layer_stack = np.append(
191
+ self.layer_collection.layer_stack, [img], axis=0)
291
192
  self.mark_as_modified()
292
193
  self.change_layer(0)
293
194
  self.image_viewer.reset_zoom()
294
195
  self.thumbnail_list.setFocus()
295
-
296
196
  self.update_thumbnails()
297
197
 
298
198
  def save_file(self):
@@ -308,7 +208,7 @@ class ImageEditor(QMainWindow):
308
208
  self.save_multilayer_as()
309
209
 
310
210
  def save_multilayer(self):
311
- if self.current_stack is None:
211
+ if self.layer_collection.layer_stack is None:
312
212
  return
313
213
  if self.current_file_path != '':
314
214
  extension = self.current_file_path.split('.')[-1]
@@ -318,7 +218,7 @@ class ImageEditor(QMainWindow):
318
218
  self.save_multilayer_file_as()
319
219
 
320
220
  def save_multilayer_as(self):
321
- if self.current_stack is None:
221
+ if self.layer_collection.layer_stack is None:
322
222
  return
323
223
  path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
324
224
  "TIFF Files (*.tif *.tiff);;All Files (*)")
@@ -329,10 +229,8 @@ class ImageEditor(QMainWindow):
329
229
 
330
230
  def save_multilayer_to_path(self, path):
331
231
  try:
332
- master_layer = {'Master': self.master_layer}
333
- individual_layers = {label: image for label, image in zip(self.current_labels, self.current_stack)}
334
- write_multilayer_tiff_from_images({**master_layer, **individual_layers}, path, exif_path=self.exif_path)
335
- self.current_file_path = path
232
+ self.io_manager.save_multilayer(path)
233
+ self.io_manager.current_file_path = path
336
234
  self.modified = False
337
235
  self.update_title()
338
236
  self.statusBar().showMessage(f"Saved multilayer to: {path}")
@@ -341,7 +239,7 @@ class ImageEditor(QMainWindow):
341
239
  QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
342
240
 
343
241
  def save_master(self):
344
- if self.master_layer is None:
242
+ if self.layer_collection.master_layer is None:
345
243
  return
346
244
  if self.current_file_path != '':
347
245
  self.save_master_to_path(self.current_file_path)
@@ -349,7 +247,7 @@ class ImageEditor(QMainWindow):
349
247
  self.save_master_as()
350
248
 
351
249
  def save_master_as(self):
352
- if self.current_stack is None:
250
+ if self.layer_collection.layer_stack is None:
353
251
  return
354
252
  path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
355
253
  "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
@@ -358,8 +256,8 @@ class ImageEditor(QMainWindow):
358
256
 
359
257
  def save_master_to_path(self, path):
360
258
  try:
361
- write_image_with_exif_data(self.exif_data, cv2.cvtColor(self.master_layer, cv2.COLOR_RGB2BGR), path)
362
- self.current_file_path = path
259
+ self.io_manager.save_master(path)
260
+ self.io_manager.current_file_path = path
363
261
  self.modified = False
364
262
  self.update_title()
365
263
  self.statusBar().showMessage(f"Saved master layer to: {path}")
@@ -370,20 +268,19 @@ class ImageEditor(QMainWindow):
370
268
  def select_exif_path(self):
371
269
  path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
372
270
  if path:
373
- self.exif_path = path
374
- self.exif_data = get_exif(path)
271
+ self.io_manager.set_exif_data(path)
375
272
  self.statusBar().showMessage(f"EXIF data extracted from {path}.")
376
- self._exif_dialog = ExifData(self.exif_data, self)
273
+ self._exif_dialog = ExifData(self.io_manager.exif_data, self)
377
274
  self._exif_dialog.exec()
378
275
 
379
276
  def close_file(self):
380
277
  if self._check_unsaved_changes():
381
- self.master_layer = None
278
+ self.layer_collection.master_layer = None
382
279
  self.blank_layer = None
383
280
  self.current_stack = None
384
- self.master_layer = None
385
- self.current_layer = 0
386
- self.current_file_path = ''
281
+ self.layer_collection.master_layer = None
282
+ self.layer_collection.current_layer_idx = 0
283
+ self.io_manager.current_file_path = ''
387
284
  self.modified = False
388
285
  self.undo_manager.reset()
389
286
  self.image_viewer.clear_image()
@@ -423,10 +320,10 @@ class ImageEditor(QMainWindow):
423
320
  self.display_master_layer()
424
321
 
425
322
  def display_master_layer(self):
426
- if self.master_layer is None:
323
+ if self.layer_collection.master_layer is None:
427
324
  self.image_viewer.clear_image()
428
325
  else:
429
- qimage = self.numpy_to_qimage(self.master_layer)
326
+ qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
430
327
  self.image_viewer.set_image(qimage)
431
328
 
432
329
  def create_thumbnail(self, layer, size):
@@ -440,30 +337,30 @@ class ImageEditor(QMainWindow):
440
337
  return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
441
338
 
442
339
  def update_master_thumbnail(self):
443
- if self.master_layer is None:
340
+ if self.layer_collection.master_layer is None:
444
341
  self.master_thumbnail_label.clear()
445
342
  else:
446
343
  thumb_size = gui_constants.UI_SIZES['thumbnail']
447
- master_thumb = self.create_thumbnail(self.master_layer, thumb_size)
344
+ master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
448
345
  self.master_thumbnail_label.setPixmap(master_thumb)
449
346
 
450
347
  def update_thumbnails(self):
451
348
  self.update_master_thumbnail()
452
349
  self.thumbnail_list.clear()
453
350
  thumb_size = gui_constants.UI_SIZES['thumbnail']
454
- if self.current_stack is None:
351
+ if self.layer_collection.layer_stack is None:
455
352
  return
456
- for i, (layer, label) in enumerate(zip(self.current_stack, self.current_labels)):
353
+ for i, (layer, label) in enumerate(zip(self.layer_collection.layer_stack, self.layer_collection.layer_labels)):
457
354
  thumbnail = self.create_thumbnail(layer, thumb_size)
458
- self._add_thumbnail_item(thumbnail, label, i, i == self.current_layer)
355
+ self._add_thumbnail_item(thumbnail, label, i, i == self.layer_collection.current_layer_idx)
459
356
 
460
357
  def _add_thumbnail_item(self, thumbnail, label, i, is_current):
461
358
  pass
462
359
 
463
360
  def change_layer(self, layer_idx):
464
- if 0 <= layer_idx < len(self.current_stack):
361
+ if 0 <= layer_idx < self.layer_collection.number_of_layers():
465
362
  view_state = self.image_viewer.get_view_state()
466
- self.current_layer = layer_idx
363
+ self.layer_collection.current_layer_idx = layer_idx
467
364
  self.display_current_view()
468
365
  self.image_viewer.set_view_state(view_state)
469
366
  self.thumbnail_list.setCurrentRow(layer_idx)
@@ -471,14 +368,10 @@ class ImageEditor(QMainWindow):
471
368
  self.image_viewer.update_brush_cursor()
472
369
  self.image_viewer.setFocus()
473
370
 
474
- def change_layer_item(self, item):
475
- layer_idx = self.thumbnail_list.row(item)
476
- self.change_layer(layer_idx)
477
-
478
371
  def display_current_layer(self):
479
- if self.current_stack is None:
372
+ if self.layer_collection.layer_stack is None:
480
373
  return
481
- layer = self.current_stack[self.current_layer]
374
+ layer = self.layer_collection.current_layer()
482
375
  qimage = self.numpy_to_qimage(layer)
483
376
  self.image_viewer.set_image(qimage)
484
377
 
@@ -497,65 +390,65 @@ class ImageEditor(QMainWindow):
497
390
  return QImage()
498
391
 
499
392
  def prev_layer(self):
500
- if self.current_stack is not None:
501
- new_idx = max(0, self.current_layer - 1)
502
- if new_idx != self.current_layer:
393
+ if self.layer_collection.layer_stack is not None:
394
+ new_idx = max(0, self.layer_collection.current_layer_idx - 1)
395
+ if new_idx != self.layer_collection.current_layer_idx:
503
396
  self.change_layer(new_idx)
504
397
  self.highlight_thumbnail(new_idx)
505
398
 
506
399
  def next_layer(self):
507
- if self.current_stack is not None:
508
- new_idx = min(len(self.current_stack) - 1, self.current_layer + 1)
509
- if new_idx != self.current_layer:
400
+ if self.layer_collection.layer_stack is not None:
401
+ new_idx = min(self.layer_collection.number_of_layers() - 1, self.layer_collection.current_layer_idx + 1)
402
+ if new_idx != self.layer_collection.current_layer_idx:
510
403
  self.change_layer(new_idx)
511
404
  self.highlight_thumbnail(new_idx)
512
405
 
513
406
  def highlight_thumbnail(self, index):
514
407
  self.thumbnail_list.setCurrentRow(index)
515
- self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index),
516
- QAbstractItemView.PositionAtCenter)
408
+ self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
517
409
 
518
410
  def update_brush_size(self, slider_val):
411
+
412
+ def slider_to_brush_size(slider_val):
413
+ normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
414
+ size = gui_constants.BRUSH_SIZES['min'] + \
415
+ gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
416
+ return max(gui_constants.BRUSH_SIZES['min'], min(gui_constants.BRUSH_SIZES['max'], size))
417
+
519
418
  self.brush.size = slider_to_brush_size(slider_val)
520
419
  self.update_brush_thumb()
521
- self.image_viewer.update_brush_cursor()
522
- self.clear_brush_cache()
523
420
 
524
421
  def increase_brush_size(self, amount=5):
525
- val = self.brush_size_slider.value()
526
- self.brush_size_slider.setValue(min(val + amount, self.brush_size_slider.maximum()))
422
+ val = min(self.brush_size_slider.value() + amount, self.brush_size_slider.maximum())
423
+ self.brush_size_slider.setValue(val)
527
424
  self.update_brush_size(val)
528
425
 
529
426
  def decrease_brush_size(self, amount=5):
530
- val = self.brush_size_slider.value()
531
- self.brush_size_slider.setValue(max(val - amount, self.brush_size_slider.minimum()))
427
+ val = max(self.brush_size_slider.value() - amount, self.brush_size_slider.minimum())
428
+ self.brush_size_slider.setValue(val)
532
429
  self.update_brush_size(val)
533
430
 
534
431
  def increase_brush_hardness(self, amount=2):
535
- val = self.hardness_slider.value()
536
- self.hardness_slider.setValue(min(val + amount, self.hardness_slider.maximum()))
432
+ val = min(self.hardness_slider.value() + amount, self.hardness_slider.maximum())
433
+ self.hardness_slider.setValue(val)
537
434
  self.update_brush_hardness(val)
538
435
 
539
436
  def decrease_brush_hardness(self, amount=2):
540
- val = self.hardness_slider.value()
541
- self.hardness_slider.setValue(max(val - amount, self.hardness_slider.minimum()))
437
+ val = max(self.hardness_slider.value() - amount, self.hardness_slider.minimum())
438
+ self.hardness_slider.setValue(val)
542
439
  self.update_brush_hardness(val)
543
440
 
544
441
  def update_brush_hardness(self, hardness):
545
442
  self.brush.hardness = hardness
546
443
  self.update_brush_thumb()
547
- self.image_viewer.update_brush_cursor()
548
- self.clear_brush_cache()
549
444
 
550
445
  def update_brush_opacity(self, opacity):
551
446
  self.brush.opacity = opacity
552
447
  self.update_brush_thumb()
553
- self.image_viewer.update_brush_cursor()
554
448
 
555
449
  def update_brush_flow(self, flow):
556
450
  self.brush.flow = flow
557
451
  self.update_brush_thumb()
558
- self.image_viewer.update_brush_cursor()
559
452
 
560
453
  def update_brush_thumb(self):
561
454
  width, height = gui_constants.UI_SIZES['brush_preview']
@@ -591,15 +484,13 @@ class ImageEditor(QMainWindow):
591
484
  painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
592
485
  painter.end()
593
486
  self.brush_preview.setPixmap(pixmap)
594
-
595
- def clear_brush_cache(self):
596
- self._brush_mask_cache.clear()
487
+ self.image_viewer.update_brush_cursor()
597
488
 
598
489
  def allow_cursor_preview(self):
599
490
  return self.view_mode == 'master' and not self.temp_view_individual
600
491
 
601
492
  def copy_layer_to_master(self):
602
- if self.current_stack is None or self.master_layer is None:
493
+ if self.layer_collection.layer_stack is None or self.layer_collection.master_layer is None:
603
494
  return
604
495
  reply = QMessageBox.question(
605
496
  self,
@@ -609,27 +500,27 @@ class ImageEditor(QMainWindow):
609
500
  QMessageBox.No
610
501
  )
611
502
  if reply == QMessageBox.Yes:
612
- self.master_layer = self.current_stack[self.current_layer].copy()
613
- self.master_layer.setflags(write=True)
503
+ self.layer_collection.master_layer = self.layer_collection.current_layer().copy()
504
+ self.layer_collection.master_layer.setflags(write=True)
614
505
  self.display_current_view()
615
506
  self.update_thumbnails()
616
507
  self.mark_as_modified()
617
- self.statusBar().showMessage(f"Copied layer {self.current_layer + 1} to master")
508
+ self.statusBar().showMessage(f"Copied layer {self.layer_collection.current_layer_idx + 1} to master")
618
509
 
619
510
  def copy_brush_area_to_master(self, view_pos):
620
- if self.current_layer is None or self.current_stack is None or len(self.current_stack) == 0 \
511
+ if self.layer_collection.layer_stack is None or self.layer_collection.number_of_layers() == 0 \
621
512
  or self.view_mode != 'master' or self.temp_view_individual:
622
513
  return
623
- area = self.brush_controller.apply_brush_operation(self.master_layer_copy,
624
- self.current_stack[self.current_layer],
625
- self.master_layer, self.mask_layer,
514
+ area = self.brush_controller.apply_brush_operation(self.layer_collection.master_layer_copy,
515
+ self.layer_collection.current_layer(),
516
+ self.layer_collection.master_layer, self.mask_layer,
626
517
  view_pos, self.image_viewer)
627
518
  self.undo_manager.extend_undo_area(*area)
628
519
 
629
520
  def begin_copy_brush_area(self, pos):
630
521
  if self.view_mode == 'master' and not self.temp_view_individual:
631
522
  self.mask_layer = self.blank_layer.copy()
632
- self.master_layer_copy = self.master_layer.copy()
523
+ self.layer_collection.copy_master_layer()
633
524
  self.undo_manager.reset_undo_area()
634
525
  self.copy_brush_area_to_master(pos)
635
526
  self.needs_update = True
@@ -649,7 +540,7 @@ class ImageEditor(QMainWindow):
649
540
  if self.update_timer.isActive():
650
541
  self.display_master_layer()
651
542
  self.update_master_thumbnail()
652
- self.undo_manager.save_undo_state(self.master_layer_copy, 'Brush Stroke')
543
+ self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, 'Brush Stroke')
653
544
  self.update_timer.stop()
654
545
  self.mark_as_modified()
655
546
 
@@ -172,7 +172,12 @@ class ImageEditorUI(ImageFilters):
172
172
  self.thumbnail_list.setFixedWidth(gui_constants.THUMB_WIDTH)
173
173
  self.thumbnail_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
174
174
  self.thumbnail_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
175
- self.thumbnail_list.itemClicked.connect(self.change_layer_item)
175
+
176
+ def change_layer_item(item):
177
+ layer_idx = self.thumbnail_list.row(item)
178
+ self.change_layer(layer_idx)
179
+
180
+ self.thumbnail_list.itemClicked.connect(change_layer_item)
176
181
  self.thumbnail_list.setStyleSheet("""
177
182
  QListWidget {
178
183
  background-color: #f5f5f5;
@@ -226,12 +231,12 @@ class ImageEditorUI(ImageFilters):
226
231
  self.undo_action = QAction("Undo", self)
227
232
  self.undo_action.setEnabled(False)
228
233
  self.undo_action.setShortcut("Ctrl+Z")
229
- self.undo_action.triggered.connect(self.undo_last_brush)
234
+ self.undo_action.triggered.connect(self.undo)
230
235
  edit_menu.addAction(self.undo_action)
231
236
  self.redo_action = QAction("Redo", self)
232
237
  self.redo_action.setEnabled(False)
233
238
  self.redo_action.setShortcut("Ctrl+Y")
234
- self.redo_action.triggered.connect(self.redo_last_brush)
239
+ self.redo_action.triggered.connect(self.redo)
235
240
  edit_menu.addAction(self.redo_action)
236
241
  edit_menu.addSeparator()
237
242
 
@@ -381,17 +386,19 @@ class ImageEditorUI(ImageFilters):
381
386
  self._update_label_in_data(old_label, new_label, i)
382
387
 
383
388
  def _update_label_in_data(self, old_label, new_label, i):
384
- self.current_labels[i] = new_label
389
+ self.layer_collection.layer_labels[i] = new_label
385
390
 
386
- def undo_last_brush(self):
387
- if self.undo_manager.undo(self.master_layer):
391
+ def undo(self):
392
+ if self.undo_manager.undo(self.layer_collection.master_layer):
388
393
  self.display_current_view()
394
+ self.update_master_thumbnail()
389
395
  self.mark_as_modified()
390
396
  self.statusBar().showMessage("Undo applied", 2000)
391
397
 
392
- def redo_last_brush(self):
393
- if self.undo_manager.redo(self.master_layer):
398
+ def redo(self):
399
+ if self.undo_manager.redo(self.layer_collection.master_layer):
394
400
  self.display_current_view()
401
+ self.update_master_thumbnail()
395
402
  self.mark_as_modified()
396
403
  self.statusBar().showMessage("Redo applied", 2000)
397
404