shinestacker 0.2.2__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)
@@ -65,6 +32,9 @@ class ImageEditor(QMainWindow):
65
32
  self.brush = Brush()
66
33
  self.brush_controller = BrushController(self.brush)
67
34
  self.undo_manager = UndoManager()
35
+ self.undo_action = None
36
+ self.redo_action = None
37
+ self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
68
38
  self.loader_thread = None
69
39
 
70
40
  def keyPressEvent(self, event):
@@ -116,43 +86,14 @@ class ImageEditor(QMainWindow):
116
86
  return True
117
87
 
118
88
  def sort_layers(self, order):
119
- if not hasattr(self, 'current_stack') or not hasattr(self, 'current_labels'):
120
- return
121
- master_index = -1
122
- master_label = None
123
- master_layer = None
124
- for i, label in enumerate(self.current_labels):
125
- if label.lower() == "master":
126
- master_index = i
127
- master_label = self.current_labels.pop(i)
128
- master_layer = self.current_stack[i]
129
- self.current_stack = np.delete(self.current_stack, i, axis=0)
130
- break
131
- if order == 'asc':
132
- self.sorted_indices = sorted(range(len(self.current_labels)),
133
- key=lambda i: self.current_labels[i].lower())
134
- elif order == 'desc':
135
- self.sorted_indices = sorted(range(len(self.current_labels)),
136
- key=lambda i: self.current_labels[i].lower(),
137
- reverse=True)
138
- else:
139
- raise ValueError(f"Invalid sorting order: {order}")
140
- self.current_labels = [self.current_labels[i] for i in self.sorted_indices]
141
- self.current_stack = self.current_stack[self.sorted_indices]
142
- if master_index != -1:
143
- self.current_labels.insert(0, master_label)
144
- self.current_stack = np.insert(self.current_stack, 0, master_layer, axis=0)
145
- self.master_layer = master_layer.copy()
146
- self.master_layer.setflags(write=True)
89
+ self.layer_collection.sort_layers(order)
147
90
  self.update_thumbnails()
148
- if self.current_layer >= len(self.current_stack):
149
- self.current_layer = len(self.current_stack) - 1
150
- self.change_layer(self.current_layer)
91
+ self.change_layer(self.layer_collection.current_layer)
151
92
 
152
93
  def update_title(self):
153
94
  title = constants.APP_TITLE
154
- if self.current_file_path:
155
- 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]}"
156
97
  if self.modified:
157
98
  title += " *"
158
99
  self.window().setWindowTitle(title)
@@ -170,7 +111,7 @@ class ImageEditor(QMainWindow):
170
111
  self.import_frames_from_files(file_paths)
171
112
  return
172
113
  path = file_paths[0] if isinstance(file_paths, list) else file_paths
173
- self.current_file_path = path
114
+ self.io_manager.current_file_path = path
174
115
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
175
116
  self.loading_dialog = QDialog(self)
176
117
  self.loading_dialog.setWindowTitle("Loading")
@@ -192,21 +133,20 @@ class ImageEditor(QMainWindow):
192
133
  QApplication.restoreOverrideCursor()
193
134
  self.loading_timer.stop()
194
135
  self.loading_dialog.hide()
195
- self.current_stack = stack
136
+ self.layer_collection.layer_stack = stack
196
137
  if labels is None:
197
- 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))]
198
139
  else:
199
- self.current_labels = labels
200
- self.master_layer = master_layer
201
- self.shape = np.array(master_layer).shape
202
- self.dtype = master_layer.dtype
140
+ self.layer_collection.layer_labels = labels
141
+ self.layer_collection.master_layer = master_layer
203
142
  self.modified = False
143
+ self.undo_manager.reset()
204
144
  self.blank_layer = np.zeros(master_layer.shape[:2])
205
145
  self.update_thumbnails()
206
146
  self.image_viewer.setup_brush_cursor()
207
147
  self.change_layer(0)
208
148
  self.image_viewer.reset_zoom()
209
- self.statusBar().showMessage(f"Loaded: {self.current_file_path}")
149
+ self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
210
150
  self.thumbnail_list.setFocus()
211
151
  self.update_title()
212
152
 
@@ -216,7 +156,7 @@ class ImageEditor(QMainWindow):
216
156
  self.loading_dialog.accept()
217
157
  self.loading_dialog.deleteLater()
218
158
  QMessageBox.critical(self, "Error", error_msg)
219
- self.statusBar().showMessage(f"Error loading: {self.current_file_path}")
159
+ self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
220
160
 
221
161
  def mark_as_modified(self):
222
162
  self.modified = True
@@ -227,68 +167,32 @@ class ImageEditor(QMainWindow):
227
167
  "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
228
168
  if file_paths:
229
169
  self.import_frames_from_files(file_paths)
170
+ self.statusBar().showMessage("Imported selected frames")
230
171
 
231
172
  def import_frames_from_files(self, file_paths):
232
- 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()
233
181
  return
234
- if self.current_stack is None and len(file_paths) > 0:
235
- path = file_paths[0]
236
- img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
237
- self.current_stack = np.array([img])
238
- self.shape, self.dtype = get_img_metadata(img)
239
- label = path.split("/")[-1].split(".")[0]
240
- self.current_labels = [label]
241
- if self.master_layer is None:
242
- self.master_layer = img.copy()
243
- self.blank_layer = np.zeros(self.master_layer.shape[:2])
244
- 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])
245
187
  else:
246
- next_paths = file_paths
247
- for path in next_paths:
248
- try:
249
- label = path.split("/")[-1].split(".")[0]
250
- img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
251
- try:
252
- validate_image(img, self.shape, self.dtype)
253
- except ShapeError as e:
254
- msg = QMessageBox()
255
- msg.setIcon(QMessageBox.Critical)
256
- msg.setWindowTitle("Import error")
257
- msg.setText(f"All files must have the same shape.\n{str(e)}")
258
- msg.exec()
259
- return
260
- except BitDepthError as e:
261
- msg = QMessageBox()
262
- msg.setIcon(QMessageBox.Critical)
263
- msg.setWindowTitle("Import error")
264
- msg.setText(f"All flies must have the same bit depth.\n{str(e)}")
265
- msg.exec()
266
- return
267
- except Exception as e:
268
- traceback.print_tb(e.__traceback__)
269
- raise e
270
- return
271
- label_x = label
272
- i = 0
273
- while label_x in self.current_labels:
274
- i += 1
275
- label_x = f"{label} ({i})"
276
- self.current_labels.append(label_x)
277
- self.current_stack = np.append(self.current_stack, [img], axis=0)
278
- except Exception as e:
279
- traceback.print_tb(e.__traceback__)
280
- msg = QMessageBox()
281
- msg.setIcon(QMessageBox.Critical)
282
- msg.setWindowTitle("Import error")
283
- msg.setText(f"Error loading file: {path}.\n{str(e)}")
284
- msg.exec()
285
- self.statusBar().showMessage(f"Error loading file: {path}")
286
- 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)
287
192
  self.mark_as_modified()
288
193
  self.change_layer(0)
289
194
  self.image_viewer.reset_zoom()
290
195
  self.thumbnail_list.setFocus()
291
-
292
196
  self.update_thumbnails()
293
197
 
294
198
  def save_file(self):
@@ -304,7 +208,7 @@ class ImageEditor(QMainWindow):
304
208
  self.save_multilayer_as()
305
209
 
306
210
  def save_multilayer(self):
307
- if self.current_stack is None:
211
+ if self.layer_collection.layer_stack is None:
308
212
  return
309
213
  if self.current_file_path != '':
310
214
  extension = self.current_file_path.split('.')[-1]
@@ -314,7 +218,7 @@ class ImageEditor(QMainWindow):
314
218
  self.save_multilayer_file_as()
315
219
 
316
220
  def save_multilayer_as(self):
317
- if self.current_stack is None:
221
+ if self.layer_collection.layer_stack is None:
318
222
  return
319
223
  path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
320
224
  "TIFF Files (*.tif *.tiff);;All Files (*)")
@@ -325,10 +229,8 @@ class ImageEditor(QMainWindow):
325
229
 
326
230
  def save_multilayer_to_path(self, path):
327
231
  try:
328
- master_layer = {'Master': self.master_layer}
329
- individual_layers = {label: image for label, image in zip(self.current_labels, self.current_stack)}
330
- write_multilayer_tiff_from_images({**master_layer, **individual_layers}, path, exif_path=self.exif_path)
331
- self.current_file_path = path
232
+ self.io_manager.save_multilayer(path)
233
+ self.io_manager.current_file_path = path
332
234
  self.modified = False
333
235
  self.update_title()
334
236
  self.statusBar().showMessage(f"Saved multilayer to: {path}")
@@ -337,7 +239,7 @@ class ImageEditor(QMainWindow):
337
239
  QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
338
240
 
339
241
  def save_master(self):
340
- if self.master_layer is None:
242
+ if self.layer_collection.master_layer is None:
341
243
  return
342
244
  if self.current_file_path != '':
343
245
  self.save_master_to_path(self.current_file_path)
@@ -345,7 +247,7 @@ class ImageEditor(QMainWindow):
345
247
  self.save_master_as()
346
248
 
347
249
  def save_master_as(self):
348
- if self.current_stack is None:
250
+ if self.layer_collection.layer_stack is None:
349
251
  return
350
252
  path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
351
253
  "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
@@ -354,8 +256,8 @@ class ImageEditor(QMainWindow):
354
256
 
355
257
  def save_master_to_path(self, path):
356
258
  try:
357
- write_image_with_exif_data(self.exif_data, cv2.cvtColor(self.master_layer, cv2.COLOR_RGB2BGR), path)
358
- self.current_file_path = path
259
+ self.io_manager.save_master(path)
260
+ self.io_manager.current_file_path = path
359
261
  self.modified = False
360
262
  self.update_title()
361
263
  self.statusBar().showMessage(f"Saved master layer to: {path}")
@@ -366,21 +268,21 @@ class ImageEditor(QMainWindow):
366
268
  def select_exif_path(self):
367
269
  path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
368
270
  if path:
369
- self.exif_path = path
370
- self.exif_data = get_exif(path)
271
+ self.io_manager.set_exif_data(path)
371
272
  self.statusBar().showMessage(f"EXIF data extracted from {path}.")
372
- self._exif_dialog = ExifData(self.exif_data, self)
273
+ self._exif_dialog = ExifData(self.io_manager.exif_data, self)
373
274
  self._exif_dialog.exec()
374
275
 
375
276
  def close_file(self):
376
277
  if self._check_unsaved_changes():
377
- self.master_layer = None
278
+ self.layer_collection.master_layer = None
378
279
  self.blank_layer = None
379
280
  self.current_stack = None
380
- self.master_layer = None
381
- self.current_layer = 0
382
- 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 = ''
383
284
  self.modified = False
285
+ self.undo_manager.reset()
384
286
  self.image_viewer.clear_image()
385
287
  self.update_thumbnails()
386
288
  self.update_title()
@@ -418,10 +320,10 @@ class ImageEditor(QMainWindow):
418
320
  self.display_master_layer()
419
321
 
420
322
  def display_master_layer(self):
421
- if self.master_layer is None:
323
+ if self.layer_collection.master_layer is None:
422
324
  self.image_viewer.clear_image()
423
325
  else:
424
- qimage = self.numpy_to_qimage(self.master_layer)
326
+ qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
425
327
  self.image_viewer.set_image(qimage)
426
328
 
427
329
  def create_thumbnail(self, layer, size):
@@ -435,30 +337,30 @@ class ImageEditor(QMainWindow):
435
337
  return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
436
338
 
437
339
  def update_master_thumbnail(self):
438
- if self.master_layer is None:
340
+ if self.layer_collection.master_layer is None:
439
341
  self.master_thumbnail_label.clear()
440
342
  else:
441
343
  thumb_size = gui_constants.UI_SIZES['thumbnail']
442
- master_thumb = self.create_thumbnail(self.master_layer, thumb_size)
344
+ master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
443
345
  self.master_thumbnail_label.setPixmap(master_thumb)
444
346
 
445
347
  def update_thumbnails(self):
446
348
  self.update_master_thumbnail()
447
349
  self.thumbnail_list.clear()
448
350
  thumb_size = gui_constants.UI_SIZES['thumbnail']
449
- if self.current_stack is None:
351
+ if self.layer_collection.layer_stack is None:
450
352
  return
451
- 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)):
452
354
  thumbnail = self.create_thumbnail(layer, thumb_size)
453
- 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)
454
356
 
455
357
  def _add_thumbnail_item(self, thumbnail, label, i, is_current):
456
358
  pass
457
359
 
458
360
  def change_layer(self, layer_idx):
459
- if 0 <= layer_idx < len(self.current_stack):
361
+ if 0 <= layer_idx < self.layer_collection.number_of_layers():
460
362
  view_state = self.image_viewer.get_view_state()
461
- self.current_layer = layer_idx
363
+ self.layer_collection.current_layer_idx = layer_idx
462
364
  self.display_current_view()
463
365
  self.image_viewer.set_view_state(view_state)
464
366
  self.thumbnail_list.setCurrentRow(layer_idx)
@@ -466,14 +368,10 @@ class ImageEditor(QMainWindow):
466
368
  self.image_viewer.update_brush_cursor()
467
369
  self.image_viewer.setFocus()
468
370
 
469
- def change_layer_item(self, item):
470
- layer_idx = self.thumbnail_list.row(item)
471
- self.change_layer(layer_idx)
472
-
473
371
  def display_current_layer(self):
474
- if self.current_stack is None:
372
+ if self.layer_collection.layer_stack is None:
475
373
  return
476
- layer = self.current_stack[self.current_layer]
374
+ layer = self.layer_collection.current_layer()
477
375
  qimage = self.numpy_to_qimage(layer)
478
376
  self.image_viewer.set_image(qimage)
479
377
 
@@ -492,65 +390,65 @@ class ImageEditor(QMainWindow):
492
390
  return QImage()
493
391
 
494
392
  def prev_layer(self):
495
- if self.current_stack is not None:
496
- new_idx = max(0, self.current_layer - 1)
497
- 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:
498
396
  self.change_layer(new_idx)
499
397
  self.highlight_thumbnail(new_idx)
500
398
 
501
399
  def next_layer(self):
502
- if self.current_stack is not None:
503
- new_idx = min(len(self.current_stack) - 1, self.current_layer + 1)
504
- 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:
505
403
  self.change_layer(new_idx)
506
404
  self.highlight_thumbnail(new_idx)
507
405
 
508
406
  def highlight_thumbnail(self, index):
509
407
  self.thumbnail_list.setCurrentRow(index)
510
- self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index),
511
- QAbstractItemView.PositionAtCenter)
408
+ self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
512
409
 
513
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
+
514
418
  self.brush.size = slider_to_brush_size(slider_val)
515
419
  self.update_brush_thumb()
516
- self.image_viewer.update_brush_cursor()
517
- self.clear_brush_cache()
518
420
 
519
421
  def increase_brush_size(self, amount=5):
520
- val = self.brush_size_slider.value()
521
- 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)
522
424
  self.update_brush_size(val)
523
425
 
524
426
  def decrease_brush_size(self, amount=5):
525
- val = self.brush_size_slider.value()
526
- 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)
527
429
  self.update_brush_size(val)
528
430
 
529
431
  def increase_brush_hardness(self, amount=2):
530
- val = self.hardness_slider.value()
531
- 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)
532
434
  self.update_brush_hardness(val)
533
435
 
534
436
  def decrease_brush_hardness(self, amount=2):
535
- val = self.hardness_slider.value()
536
- 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)
537
439
  self.update_brush_hardness(val)
538
440
 
539
441
  def update_brush_hardness(self, hardness):
540
442
  self.brush.hardness = hardness
541
443
  self.update_brush_thumb()
542
- self.image_viewer.update_brush_cursor()
543
- self.clear_brush_cache()
544
444
 
545
445
  def update_brush_opacity(self, opacity):
546
446
  self.brush.opacity = opacity
547
447
  self.update_brush_thumb()
548
- self.image_viewer.update_brush_cursor()
549
448
 
550
449
  def update_brush_flow(self, flow):
551
450
  self.brush.flow = flow
552
451
  self.update_brush_thumb()
553
- self.image_viewer.update_brush_cursor()
554
452
 
555
453
  def update_brush_thumb(self):
556
454
  width, height = gui_constants.UI_SIZES['brush_preview']
@@ -586,15 +484,13 @@ class ImageEditor(QMainWindow):
586
484
  painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
587
485
  painter.end()
588
486
  self.brush_preview.setPixmap(pixmap)
589
-
590
- def clear_brush_cache(self):
591
- self._brush_mask_cache.clear()
487
+ self.image_viewer.update_brush_cursor()
592
488
 
593
489
  def allow_cursor_preview(self):
594
490
  return self.view_mode == 'master' and not self.temp_view_individual
595
491
 
596
492
  def copy_layer_to_master(self):
597
- 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:
598
494
  return
599
495
  reply = QMessageBox.question(
600
496
  self,
@@ -604,27 +500,27 @@ class ImageEditor(QMainWindow):
604
500
  QMessageBox.No
605
501
  )
606
502
  if reply == QMessageBox.Yes:
607
- self.master_layer = self.current_stack[self.current_layer].copy()
608
- 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)
609
505
  self.display_current_view()
610
506
  self.update_thumbnails()
611
507
  self.mark_as_modified()
612
- 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")
613
509
 
614
510
  def copy_brush_area_to_master(self, view_pos):
615
- 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 \
616
512
  or self.view_mode != 'master' or self.temp_view_individual:
617
513
  return
618
- area = self.brush_controller.apply_brush_operation(self.master_layer_copy,
619
- self.current_stack[self.current_layer],
620
- 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,
621
517
  view_pos, self.image_viewer)
622
518
  self.undo_manager.extend_undo_area(*area)
623
519
 
624
520
  def begin_copy_brush_area(self, pos):
625
521
  if self.view_mode == 'master' and not self.temp_view_individual:
626
522
  self.mask_layer = self.blank_layer.copy()
627
- self.master_layer_copy = self.master_layer.copy()
523
+ self.layer_collection.copy_master_layer()
628
524
  self.undo_manager.reset_undo_area()
629
525
  self.copy_brush_area_to_master(pos)
630
526
  self.needs_update = True
@@ -644,6 +540,22 @@ class ImageEditor(QMainWindow):
644
540
  if self.update_timer.isActive():
645
541
  self.display_master_layer()
646
542
  self.update_master_thumbnail()
647
- self.undo_manager.save_undo_state(self.master_layer_copy)
543
+ self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, 'Brush Stroke')
648
544
  self.update_timer.stop()
649
545
  self.mark_as_modified()
546
+
547
+ def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
548
+ if self.undo_action:
549
+ if has_undo:
550
+ self.undo_action.setText(f"Undo {undo_desc}")
551
+ self.undo_action.setEnabled(True)
552
+ else:
553
+ self.undo_action.setText("Undo")
554
+ self.undo_action.setEnabled(False)
555
+ if self.redo_action:
556
+ if has_redo:
557
+ self.redo_action.setText(f"Redo {redo_desc}")
558
+ self.redo_action.setEnabled(True)
559
+ else:
560
+ self.redo_action.setText("Redo")
561
+ self.redo_action.setEnabled(False)