shinestacker 0.3.1__py3-none-any.whl → 0.3.3__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 (39) hide show
  1. shinestacker/__init__.py +6 -6
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/balance.py +6 -7
  4. shinestacker/algorithms/noise_detection.py +2 -0
  5. shinestacker/app/open_frames.py +6 -4
  6. shinestacker/config/__init__.py +2 -1
  7. shinestacker/config/config.py +1 -0
  8. shinestacker/config/constants.py +1 -0
  9. shinestacker/config/gui_constants.py +1 -0
  10. shinestacker/core/__init__.py +4 -3
  11. shinestacker/core/colors.py +1 -0
  12. shinestacker/core/core_utils.py +6 -6
  13. shinestacker/core/exceptions.py +1 -0
  14. shinestacker/core/framework.py +2 -1
  15. shinestacker/gui/action_config.py +47 -42
  16. shinestacker/gui/actions_window.py +8 -5
  17. shinestacker/retouch/brush_preview.py +5 -6
  18. shinestacker/retouch/brush_tool.py +164 -0
  19. shinestacker/retouch/denoise_filter.py +56 -0
  20. shinestacker/retouch/display_manager.py +177 -0
  21. shinestacker/retouch/exif_data.py +2 -1
  22. shinestacker/retouch/filter_base.py +114 -0
  23. shinestacker/retouch/filter_manager.py +14 -0
  24. shinestacker/retouch/image_editor.py +104 -430
  25. shinestacker/retouch/image_editor_ui.py +32 -72
  26. shinestacker/retouch/image_filters.py +25 -349
  27. shinestacker/retouch/image_viewer.py +22 -14
  28. shinestacker/retouch/io_gui_handler.py +208 -0
  29. shinestacker/retouch/io_manager.py +9 -13
  30. shinestacker/retouch/layer_collection.py +65 -1
  31. shinestacker/retouch/unsharp_mask_filter.py +84 -0
  32. shinestacker/retouch/white_balance_filter.py +111 -0
  33. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/METADATA +3 -2
  34. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/RECORD +38 -31
  35. shinestacker/retouch/brush_controller.py +0 -57
  36. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/WHEEL +0 -0
  37. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/entry_points.txt +0 -0
  38. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/licenses/LICENSE +0 -0
  39. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/top_level.txt +0 -0
@@ -1,73 +1,68 @@
1
- import traceback
2
- import numpy as np
3
- from PySide6.QtWidgets import (QMainWindow, QFileDialog, QMessageBox, QAbstractItemView,
4
- QVBoxLayout, QLabel, QDialog, QApplication)
5
- from PySide6.QtGui import QPixmap, QPainter, QColor, QImage, QPen, QBrush, QGuiApplication, QCursor
6
- from PySide6.QtCore import Qt, QTimer, QEvent, QPoint
1
+ from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
2
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
+ from PySide6.QtCore import Qt, QPoint
7
4
  from .. config.constants import constants
8
5
  from .. config.gui_constants import gui_constants
9
- from .brush import Brush
10
- from .brush_controller import BrushController
11
6
  from .undo_manager import UndoManager
12
- from .file_loader import FileLoader
13
- from .exif_data import ExifData
14
7
  from .layer_collection import LayerCollection
15
- from .io_manager import IOManager
8
+ from .io_gui_handler import IOGuiHandler
16
9
  from .brush_gradient import create_brush_gradient
10
+ from .display_manager import DisplayManager
11
+ from .brush_tool import BrushTool
17
12
 
18
13
 
19
14
  class ImageEditor(QMainWindow):
20
15
  def __init__(self):
21
16
  super().__init__()
22
- self.layer_collection = LayerCollection()
23
- self.io_manager = IOManager(self.layer_collection)
24
- self.view_mode = 'master'
25
- self.temp_view_individual = False
26
- self.modified = False
27
- self.installEventFilter(self)
28
- self.update_timer = QTimer(self)
29
- self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
30
- self.update_timer.timeout.connect(self.process_pending_updates)
31
- self.needs_update = False
32
- self.brush = Brush()
33
- self.brush_controller = BrushController(self.brush)
17
+ layer_collection = LayerCollection()
18
+ layer_collection.add_to(self)
34
19
  self.undo_manager = UndoManager()
35
20
  self.undo_action = None
36
21
  self.redo_action = None
37
22
  self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
38
- self.loader_thread = None
23
+ self.io_gui_handler = None
24
+ self.display_manager = None
25
+ self.brush_tool = BrushTool()
26
+ self.modified = False
27
+ self.installEventFilter(self)
28
+
29
+ def setup_ui(self):
30
+ self.display_manager = DisplayManager(self.layer_collection, self.image_viewer,
31
+ self.master_thumbnail_label, self.thumbnail_list, parent=self)
32
+ self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
33
+ self.display_manager.status_message_requested.connect(self.show_status_message)
34
+ self.display_manager.cursor_preview_state_changed.connect(
35
+ lambda state: setattr(self.image_viewer, 'allow_cursor_preview', state))
36
+ self.io_gui_handler.status_message_requested.connect(self.show_status_message)
37
+ self.io_gui_handler.update_title_requested.connect(self.update_title)
38
+ self.brush_tool.setup_ui(self.brush, self.brush_preview, self.image_viewer,
39
+ self.brush_size_slider, self.hardness_slider, self.opacity_slider,
40
+ self.flow_slider)
41
+ self.image_viewer.brush = self.brush_tool.brush
42
+ self.brush_tool.update_brush_thumb()
43
+ self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
44
+ self.image_viewer.display_manager = self.display_manager
45
+
46
+ def show_status_message(self, message):
47
+ self.statusBar().showMessage(message)
39
48
 
40
49
  def keyPressEvent(self, event):
41
50
  if self.image_viewer.empty:
42
51
  return
43
- elif event.text() == '[':
44
- self.decrease_brush_size()
52
+ if event.text() == '[':
53
+ self.brush_tool.decrease_brush_size()
45
54
  return
46
- elif event.text() == ']':
47
- self.increase_brush_size()
55
+ if event.text() == ']':
56
+ self.brush_tool.increase_brush_size()
48
57
  return
49
- elif event.text() == '{':
50
- self.decrease_brush_hardness()
58
+ if event.text() == '{':
59
+ self.brush_tool.decrease_brush_hardness()
51
60
  return
52
- elif event.text() == '}':
53
- self.increase_brush_hardness()
61
+ if event.text() == '}':
62
+ self.brush_tool.increase_brush_hardness()
54
63
  return
55
64
  super().keyPressEvent(event)
56
65
 
57
- def process_pending_updates(self):
58
- if self.needs_update:
59
- self.display_master_layer()
60
- self.needs_update = False
61
-
62
- def eventFilter(self, obj, event):
63
- if event.type() == QEvent.KeyPress and event.key() == Qt.Key_X:
64
- self.start_temp_view()
65
- return True
66
- elif event.type() == QEvent.KeyRelease and event.key() == Qt.Key_X:
67
- self.end_temp_view()
68
- return True
69
- return super().eventFilter(obj, event)
70
-
71
66
  def _check_unsaved_changes(self) -> bool:
72
67
  if self.modified:
73
68
  reply = QMessageBox.question(
@@ -78,328 +73,52 @@ class ImageEditor(QMainWindow):
78
73
  if reply == QMessageBox.Save:
79
74
  self.save_file()
80
75
  return True
81
- elif reply == QMessageBox.Discard:
76
+ if reply == QMessageBox.Discard:
82
77
  return True
83
- else:
84
- return False
85
- else:
86
- return True
78
+ return False
79
+ return True
87
80
 
88
81
  def sort_layers(self, order):
89
- self.layer_collection.sort_layers(order)
90
- self.update_thumbnails()
91
- self.change_layer(self.layer_collection.current_layer)
82
+ self.sort_layers(order)
83
+ self.display_manager.update_thumbnails()
84
+ self.change_layer(self.current_layer())
92
85
 
93
86
  def update_title(self):
94
87
  title = constants.APP_TITLE
95
- if self.io_manager.current_file_path:
96
- title += f" - {self.io_manager.current_file_path.split('/')[-1]}"
97
- if self.modified:
98
- title += " *"
88
+ if self.io_gui_handler is not None:
89
+ path = self.io_gui_handler.io_manager.current_file_path
90
+ if path != '':
91
+ title += f" - {path.split('/')[-1]}"
92
+ if self.modified:
93
+ title += " *"
99
94
  self.window().setWindowTitle(title)
100
95
 
101
- def open_file(self, file_paths=None):
102
- if file_paths is None:
103
- file_paths, _ = QFileDialog.getOpenFileNames(
104
- self, "Open Image", "", "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
105
- if not file_paths:
106
- return
107
- if self.loader_thread and self.loader_thread.isRunning():
108
- if not self.loader_thread.wait(10000):
109
- raise RuntimeError("Loading timeout error.")
110
- if isinstance(file_paths, list) and len(file_paths) > 1:
111
- self.import_frames_from_files(file_paths)
112
- return
113
- path = file_paths[0] if isinstance(file_paths, list) else file_paths
114
- self.io_manager.current_file_path = path
115
- QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
116
- self.loading_dialog = QDialog(self)
117
- self.loading_dialog.setWindowTitle("Loading")
118
- self.loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
119
- self.loading_dialog.setModal(True)
120
- layout = QVBoxLayout()
121
- layout.addWidget(QLabel("File loading..."))
122
- self.loading_dialog.setLayout(layout)
123
- self.loading_timer = QTimer()
124
- self.loading_timer.setSingleShot(True)
125
- self.loading_timer.timeout.connect(self.loading_dialog.show)
126
- self.loading_timer.start(100)
127
- self.loader_thread = FileLoader(path)
128
- self.loader_thread.finished.connect(self.on_file_loaded)
129
- self.loader_thread.error.connect(self.on_file_error)
130
- self.loader_thread.start()
131
-
132
- def on_file_loaded(self, stack, labels, master_layer):
133
- QApplication.restoreOverrideCursor()
134
- self.loading_timer.stop()
135
- self.loading_dialog.hide()
136
- self.layer_collection.layer_stack = stack
137
- if labels is None:
138
- self.layer_collection.layer_labels = [f'Layer {i:03d}' for i in range(len(stack))]
139
- else:
140
- self.layer_collection.layer_labels = labels
141
- self.layer_collection.master_layer = master_layer
142
- self.modified = False
143
- self.undo_manager.reset()
144
- self.blank_layer = np.zeros(master_layer.shape[:2])
145
- self.update_thumbnails()
146
- self.image_viewer.setup_brush_cursor()
147
- self.change_layer(0)
148
- self.image_viewer.reset_zoom()
149
- self.statusBar().showMessage(f"Loaded: {self.io_manager.current_file_path}")
150
- self.thumbnail_list.setFocus()
151
- self.update_title()
152
-
153
- def on_file_error(self, error_msg):
154
- QApplication.restoreOverrideCursor()
155
- self.loading_timer.stop()
156
- self.loading_dialog.accept()
157
- self.loading_dialog.deleteLater()
158
- QMessageBox.critical(self, "Error", error_msg)
159
- self.statusBar().showMessage(f"Error loading: {self.io_manager.current_file_path}")
160
-
161
96
  def mark_as_modified(self):
162
97
  self.modified = True
163
98
  self.update_title()
164
99
 
165
- def import_frames(self):
166
- file_paths, _ = QFileDialog.getOpenFileNames(self, "Select frames", "",
167
- "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
168
- if file_paths:
169
- self.import_frames_from_files(file_paths)
170
- self.statusBar().showMessage("Imported selected frames")
171
-
172
- def import_frames_from_files(self, file_paths):
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()
181
- return
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])
187
- else:
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)
192
- self.mark_as_modified()
193
- self.change_layer(0)
194
- self.image_viewer.reset_zoom()
195
- self.thumbnail_list.setFocus()
196
- self.update_thumbnails()
197
-
198
- def save_file(self):
199
- if self.save_master_only.isChecked():
200
- self.save_master()
201
- else:
202
- self.save_multilayer()
203
-
204
- def save_file_as(self):
205
- if self.save_master_only.isChecked():
206
- self.save_master_as()
207
- else:
208
- self.save_multilayer_as()
209
-
210
- def save_multilayer(self):
211
- if self.layer_collection.layer_stack is None:
212
- return
213
- if self.current_file_path != '':
214
- extension = self.current_file_path.split('.')[-1]
215
- if extension in ['tif', 'tiff']:
216
- self.save_multilayer_to_path(self.current_file_path)
217
- return
218
- self.save_multilayer_file_as()
219
-
220
- def save_multilayer_as(self):
221
- if self.layer_collection.layer_stack is None:
222
- return
223
- path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
224
- "TIFF Files (*.tif *.tiff);;All Files (*)")
225
- if path:
226
- if not path.lower().endswith(('.tif', '.tiff')):
227
- path += '.tif'
228
- self.save_multilayer_to_path(path)
229
-
230
- def save_multilayer_to_path(self, path):
231
- try:
232
- self.io_manager.save_multilayer(path)
233
- self.io_manager.current_file_path = path
234
- self.modified = False
235
- self.update_title()
236
- self.statusBar().showMessage(f"Saved multilayer to: {path}")
237
- except Exception as e:
238
- traceback.print_tb(e.__traceback__)
239
- QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
240
-
241
- def save_master(self):
242
- if self.layer_collection.master_layer is None:
243
- return
244
- if self.current_file_path != '':
245
- self.save_master_to_path(self.current_file_path)
246
- return
247
- self.save_master_as()
248
-
249
- def save_master_as(self):
250
- if self.layer_collection.layer_stack is None:
251
- return
252
- path, _ = QFileDialog.getSaveFileName(self, "Save Image", "",
253
- "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
254
- if path:
255
- self.save_master_to_path(path)
256
-
257
- def save_master_to_path(self, path):
258
- try:
259
- self.io_manager.save_master(path)
260
- self.io_manager.current_file_path = path
261
- self.modified = False
262
- self.update_title()
263
- self.statusBar().showMessage(f"Saved master layer to: {path}")
264
- except Exception as e:
265
- traceback.print_tb(e.__traceback__)
266
- QMessageBox.critical(self, "Save Error", f"Could not save file: {str(e)}")
267
-
268
- def select_exif_path(self):
269
- path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
270
- if path:
271
- self.io_manager.set_exif_data(path)
272
- self.statusBar().showMessage(f"EXIF data extracted from {path}.")
273
- self._exif_dialog = ExifData(self.io_manager.exif_data, self)
274
- self._exif_dialog.exec()
275
-
276
- def close_file(self):
277
- if self._check_unsaved_changes():
278
- self.layer_collection.master_layer = None
279
- self.blank_layer = None
280
- self.current_stack = None
281
- self.layer_collection.master_layer = None
282
- self.layer_collection.current_layer_idx = 0
283
- self.io_manager.current_file_path = ''
284
- self.modified = False
285
- self.undo_manager.reset()
286
- self.image_viewer.clear_image()
287
- self.update_thumbnails()
288
- self.update_title()
289
-
290
- def set_view_master(self):
291
- self.view_mode = 'master'
292
- self.temp_view_individual = False
293
- self.display_master_layer()
294
- self.statusBar().showMessage("View mode: Master")
295
-
296
- def set_view_individual(self):
297
- self.view_mode = 'individual'
298
- self.temp_view_individual = False
299
- self.display_current_layer()
300
- self.statusBar().showMessage("View mode: Individual layers")
301
-
302
- def start_temp_view(self):
303
- if not self.temp_view_individual and self.view_mode == 'master':
304
- self.temp_view_individual = True
305
- self.image_viewer.update_brush_cursor()
306
- self.display_current_layer()
307
- self.statusBar().showMessage("Temporary view: Individual layer (hold X)")
308
-
309
- def end_temp_view(self):
310
- if self.temp_view_individual:
311
- self.temp_view_individual = False
312
- self.image_viewer.update_brush_cursor()
313
- self.display_master_layer()
314
- self.statusBar().showMessage("View mode: Master")
315
-
316
- def display_current_view(self):
317
- if self.temp_view_individual or self.view_mode == 'individual':
318
- self.display_current_layer()
319
- else:
320
- self.display_master_layer()
321
-
322
- def display_master_layer(self):
323
- if self.layer_collection.master_layer is None:
324
- self.image_viewer.clear_image()
325
- else:
326
- qimage = self.numpy_to_qimage(self.layer_collection.master_layer)
327
- self.image_viewer.set_image(qimage)
328
-
329
- def create_thumbnail(self, layer, size):
330
- if layer.dtype == np.uint16:
331
- layer = (layer // 256).astype(np.uint8)
332
- height, width = layer.shape[:2]
333
- if layer.ndim == 3 and layer.shape[-1] == 3:
334
- qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
335
- else:
336
- qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
337
- return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
338
-
339
- def update_master_thumbnail(self):
340
- if self.layer_collection.master_layer is None:
341
- self.master_thumbnail_label.clear()
342
- else:
343
- thumb_size = gui_constants.UI_SIZES['thumbnail']
344
- master_thumb = self.create_thumbnail(self.layer_collection.master_layer, thumb_size)
345
- self.master_thumbnail_label.setPixmap(master_thumb)
346
-
347
- def update_thumbnails(self):
348
- self.update_master_thumbnail()
349
- self.thumbnail_list.clear()
350
- thumb_size = gui_constants.UI_SIZES['thumbnail']
351
- if self.layer_collection.layer_stack is None:
352
- return
353
- for i, (layer, label) in enumerate(zip(self.layer_collection.layer_stack, self.layer_collection.layer_labels)):
354
- thumbnail = self.create_thumbnail(layer, thumb_size)
355
- self._add_thumbnail_item(thumbnail, label, i, i == self.layer_collection.current_layer_idx)
356
-
357
- def _add_thumbnail_item(self, thumbnail, label, i, is_current):
358
- pass
359
-
360
100
  def change_layer(self, layer_idx):
361
- if 0 <= layer_idx < self.layer_collection.number_of_layers():
101
+ if 0 <= layer_idx < self.number_of_layers():
362
102
  view_state = self.image_viewer.get_view_state()
363
- self.layer_collection.current_layer_idx = layer_idx
364
- self.display_current_view()
103
+ self.set_current_layer_idx(layer_idx)
104
+ self.display_manager.display_current_view()
365
105
  self.image_viewer.set_view_state(view_state)
366
106
  self.thumbnail_list.setCurrentRow(layer_idx)
367
107
  self.thumbnail_list.setFocus()
368
108
  self.image_viewer.update_brush_cursor()
369
109
  self.image_viewer.setFocus()
370
110
 
371
- def display_current_layer(self):
372
- if self.layer_collection.layer_stack is None:
373
- return
374
- layer = self.layer_collection.current_layer()
375
- qimage = self.numpy_to_qimage(layer)
376
- self.image_viewer.set_image(qimage)
377
-
378
- def numpy_to_qimage(self, array):
379
- if array.dtype == np.uint16:
380
- array = np.right_shift(array, 8).astype(np.uint8)
381
-
382
- if array.ndim == 2:
383
- height, width = array.shape
384
- return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
385
- elif array.ndim == 3:
386
- height, width, _ = array.shape
387
- if not array.flags['C_CONTIGUOUS']:
388
- array = np.ascontiguousarray(array)
389
- return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
390
- return QImage()
391
-
392
111
  def prev_layer(self):
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:
112
+ if self.layer_stack() is not None:
113
+ new_idx = max(0, self.current_layer_idx() - 1)
114
+ if new_idx != self.current_layer_idx():
396
115
  self.change_layer(new_idx)
397
116
  self.highlight_thumbnail(new_idx)
398
117
 
399
118
  def next_layer(self):
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:
119
+ if self.layer_stack() is not None:
120
+ new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
121
+ if new_idx != self.current_layer_idx():
403
122
  self.change_layer(new_idx)
404
123
  self.highlight_thumbnail(new_idx)
405
124
 
@@ -407,48 +126,34 @@ class ImageEditor(QMainWindow):
407
126
  self.thumbnail_list.setCurrentRow(index)
408
127
  self.thumbnail_list.scrollToItem(self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
409
128
 
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
-
418
- self.brush.size = slider_to_brush_size(slider_val)
419
- self.update_brush_thumb()
420
-
421
- def increase_brush_size(self, amount=5):
422
- val = min(self.brush_size_slider.value() + amount, self.brush_size_slider.maximum())
423
- self.brush_size_slider.setValue(val)
424
- self.update_brush_size(val)
425
-
426
- def decrease_brush_size(self, amount=5):
427
- val = max(self.brush_size_slider.value() - amount, self.brush_size_slider.minimum())
428
- self.brush_size_slider.setValue(val)
429
- self.update_brush_size(val)
430
-
431
- def increase_brush_hardness(self, amount=2):
432
- val = min(self.hardness_slider.value() + amount, self.hardness_slider.maximum())
433
- self.hardness_slider.setValue(val)
434
- self.update_brush_hardness(val)
435
-
436
- def decrease_brush_hardness(self, amount=2):
437
- val = max(self.hardness_slider.value() - amount, self.hardness_slider.minimum())
438
- self.hardness_slider.setValue(val)
439
- self.update_brush_hardness(val)
440
-
441
- def update_brush_hardness(self, hardness):
442
- self.brush.hardness = hardness
443
- self.update_brush_thumb()
444
-
445
- def update_brush_opacity(self, opacity):
446
- self.brush.opacity = opacity
447
- self.update_brush_thumb()
129
+ def copy_layer_to_master(self):
130
+ if self.layer_stack() is None or self.master_layer() is None:
131
+ return
132
+ reply = QMessageBox.question(
133
+ self,
134
+ "Confirm Copy",
135
+ "Warning: the current master layer will be erased\n\nDo you want to continue?",
136
+ QMessageBox.Yes | QMessageBox.No,
137
+ QMessageBox.No
138
+ )
139
+ if reply == QMessageBox.Yes:
140
+ self.set_master_layer(self.current_layer().copy())
141
+ self.master_layer().setflags(write=True)
142
+ self.display_manager.display_current_view()
143
+ self.display_manager.update_thumbnails()
144
+ self.mark_as_modified()
145
+ self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
448
146
 
449
- def update_brush_flow(self, flow):
450
- self.brush.flow = flow
451
- self.update_brush_thumb()
147
+ def copy_brush_area_to_master(self, view_pos):
148
+ if self.layer_stack() is None or self.number_of_layers() == 0 \
149
+ or not self.display_manager.allow_cursor_preview():
150
+ return
151
+ area = self.brush_tool.apply_brush_operation(
152
+ self.master_layer_copy(),
153
+ self.current_layer(),
154
+ self.master_layer(), self.mask_layer,
155
+ view_pos, self.image_viewer)
156
+ self.undo_manager.extend_undo_area(*area)
452
157
 
453
158
  def update_brush_thumb(self):
454
159
  width, height = gui_constants.UI_SIZES['brush_preview']
@@ -486,62 +191,31 @@ class ImageEditor(QMainWindow):
486
191
  self.brush_preview.setPixmap(pixmap)
487
192
  self.image_viewer.update_brush_cursor()
488
193
 
489
- def allow_cursor_preview(self):
490
- return self.view_mode == 'master' and not self.temp_view_individual
491
-
492
- def copy_layer_to_master(self):
493
- if self.layer_collection.layer_stack is None or self.layer_collection.master_layer is None:
494
- return
495
- reply = QMessageBox.question(
496
- self,
497
- "Confirm Copy",
498
- "Warning: the current master layer will be erased\n\nDo you want to continue?",
499
- QMessageBox.Yes | QMessageBox.No,
500
- QMessageBox.No
501
- )
502
- if reply == QMessageBox.Yes:
503
- self.layer_collection.master_layer = self.layer_collection.current_layer().copy()
504
- self.layer_collection.master_layer.setflags(write=True)
505
- self.display_current_view()
506
- self.update_thumbnails()
507
- self.mark_as_modified()
508
- self.statusBar().showMessage(f"Copied layer {self.layer_collection.current_layer_idx + 1} to master")
509
-
510
- def copy_brush_area_to_master(self, view_pos):
511
- if self.layer_collection.layer_stack is None or self.layer_collection.number_of_layers() == 0 \
512
- or self.view_mode != 'master' or self.temp_view_individual:
513
- return
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,
517
- view_pos, self.image_viewer)
518
- self.undo_manager.extend_undo_area(*area)
519
-
520
194
  def begin_copy_brush_area(self, pos):
521
- if self.view_mode == 'master' and not self.temp_view_individual:
522
- self.mask_layer = self.blank_layer.copy()
523
- self.layer_collection.copy_master_layer()
195
+ if self.display_manager.allow_cursor_preview():
196
+ self.mask_layer = self.io_gui_handler.blank_layer.copy()
197
+ self.copy_master_layer()
524
198
  self.undo_manager.reset_undo_area()
525
199
  self.copy_brush_area_to_master(pos)
526
- self.needs_update = True
527
- if not self.update_timer.isActive():
528
- self.update_timer.start()
200
+ self.display_manager.needs_update = True
201
+ if not self.display_manager.update_timer.isActive():
202
+ self.display_manager.update_timer.start()
529
203
  self.mark_as_modified()
530
204
 
531
205
  def continue_copy_brush_area(self, pos):
532
- if self.view_mode == 'master' and not self.temp_view_individual:
206
+ if self.display_manager.allow_cursor_preview():
533
207
  self.copy_brush_area_to_master(pos)
534
- self.needs_update = True
535
- if not self.update_timer.isActive():
536
- self.update_timer.start()
208
+ self.display_manager.needs_update = True
209
+ if not self.display_manager.update_timer.isActive():
210
+ self.display_manager.update_timer.start()
537
211
  self.mark_as_modified()
538
212
 
539
213
  def end_copy_brush_area(self):
540
- if self.update_timer.isActive():
541
- self.display_master_layer()
542
- self.update_master_thumbnail()
543
- self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, 'Brush Stroke')
544
- self.update_timer.stop()
214
+ if self.display_manager.update_timer.isActive():
215
+ self.display_manager.display_master_layer()
216
+ self.display_manager.update_master_thumbnail()
217
+ self.undo_manager.save_undo_state(self.master_layer_copy(), 'Brush Stroke')
218
+ self.display_manager.update_timer.stop()
545
219
  self.mark_as_modified()
546
220
 
547
221
  def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):