shinestacker 1.3.1__py3-none-any.whl → 1.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 (38) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +198 -18
  3. shinestacker/algorithms/align_parallel.py +17 -1
  4. shinestacker/algorithms/balance.py +23 -13
  5. shinestacker/algorithms/noise_detection.py +3 -1
  6. shinestacker/algorithms/utils.py +21 -10
  7. shinestacker/algorithms/vignetting.py +2 -0
  8. shinestacker/app/main.py +1 -1
  9. shinestacker/config/gui_constants.py +7 -2
  10. shinestacker/core/core_utils.py +10 -1
  11. shinestacker/gui/action_config.py +172 -7
  12. shinestacker/gui/action_config_dialog.py +246 -285
  13. shinestacker/gui/gui_run.py +2 -2
  14. shinestacker/gui/main_window.py +14 -5
  15. shinestacker/gui/menu_manager.py +26 -2
  16. shinestacker/gui/project_controller.py +4 -0
  17. shinestacker/gui/recent_file_manager.py +93 -0
  18. shinestacker/retouch/base_filter.py +13 -15
  19. shinestacker/retouch/brush_preview.py +3 -1
  20. shinestacker/retouch/brush_tool.py +11 -11
  21. shinestacker/retouch/display_manager.py +43 -59
  22. shinestacker/retouch/image_editor_ui.py +161 -82
  23. shinestacker/retouch/image_view_status.py +65 -0
  24. shinestacker/retouch/image_viewer.py +95 -431
  25. shinestacker/retouch/io_gui_handler.py +12 -2
  26. shinestacker/retouch/layer_collection.py +3 -0
  27. shinestacker/retouch/overlaid_view.py +215 -0
  28. shinestacker/retouch/shortcuts_help.py +13 -3
  29. shinestacker/retouch/sidebyside_view.py +477 -0
  30. shinestacker/retouch/transformation_manager.py +43 -0
  31. shinestacker/retouch/undo_manager.py +22 -3
  32. shinestacker/retouch/view_strategy.py +557 -0
  33. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/METADATA +7 -7
  34. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/RECORD +38 -32
  35. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/WHEEL +0 -0
  36. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/entry_points.txt +0 -0
  37. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/licenses/LICENSE +0 -0
  38. {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/top_level.txt +0 -0
@@ -1,465 +1,129 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914, R0912
2
- import math
3
- from PySide6.QtWidgets import (QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
4
- QGraphicsEllipseItem)
5
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor
6
- from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal, QEvent
7
- from .. config.gui_constants import gui_constants
8
- from .brush_preview import BrushPreviewItem
9
- from .brush_gradient import create_default_brush_gradient
10
- from .layer_collection import LayerCollectionHandler
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914, R0912, R0913, R0917
2
+ from PySide6.QtCore import Qt
3
+ from PySide6.QtWidgets import QWidget, QVBoxLayout
4
+ from .image_view_status import ImageViewStatus
5
+ from .overlaid_view import OverlaidView
6
+ from .sidebyside_view import SideBySideView, TopBottomView
11
7
 
12
8
 
13
- class ImageViewer(QGraphicsView, LayerCollectionHandler):
14
- temp_view_requested = Signal(bool)
15
- brush_operation_started = Signal(QPoint)
16
- brush_operation_continued = Signal(QPoint)
17
- brush_operation_ended = Signal()
18
- brush_size_change_requested = Signal(int) # +1 or -1
19
-
9
+ class ImageViewer(QWidget):
20
10
  def __init__(self, layer_collection, parent=None):
21
- QGraphicsView.__init__(self, parent)
22
- LayerCollectionHandler.__init__(self)
23
- self.display_manager = None
24
- self.layer_collection = layer_collection
25
- self.brush = None
26
- self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
27
- self.scene = QGraphicsScene(self)
28
- self.setScene(self.scene)
29
- self.pixmap_item = QGraphicsPixmapItem()
30
- self.scene.addItem(self.pixmap_item)
31
- self.pixmap_item.setPixmap(QPixmap())
32
- self.scene.setBackgroundBrush(QBrush(QColor(120, 120, 120)))
33
- self.zoom_factor = 1.0
34
- self.min_scale = 0.0
35
- self.max_scale = 0.0
36
- self.last_mouse_pos = None
37
- self.setRenderHint(QPainter.Antialiasing)
38
- self.setRenderHint(QPainter.SmoothPixmapTransform)
39
- self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
40
- self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
41
- self.setDragMode(QGraphicsView.ScrollHandDrag)
42
- self.brush_cursor = None
43
- self.setMouseTracking(True)
44
- self.space_pressed = False
45
- self.control_pressed = False
46
- self.setDragMode(QGraphicsView.NoDrag)
47
- self.scrolling = False
48
- self.dragging = False
49
- self.last_update_time = QTime.currentTime()
50
- self.set_layer_collection(layer_collection)
51
- self.brush_preview = BrushPreviewItem(self.layer_collection)
52
- self.scene.addItem(self.brush_preview)
53
- self.empty = True
54
- self.allow_cursor_preview = True
55
- self.last_brush_pos = None
56
- self.grabGesture(Qt.PanGesture)
57
- self.grabGesture(Qt.PinchGesture)
58
- self.pinch_start_scale = 1.0
59
- self.last_scroll_pos = QPointF()
60
- self.gesture_active = False
61
- self.pinch_center_view = None
62
- self.pinch_center_scene = None
63
-
64
- def set_image(self, qimage):
65
- pixmap = QPixmap.fromImage(qimage)
66
- self.pixmap_item.setPixmap(pixmap)
67
- self.setSceneRect(QRectF(pixmap.rect()))
68
- img_width, img_height = pixmap.width(), pixmap.height()
69
- self.min_scale = min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
70
- gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height)
71
- self.max_scale = gui_constants.MAX_ZOOMED_IMG_PX_SIZE
72
- if self.zoom_factor == 1.0:
73
- self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
74
- self.zoom_factor = self.get_current_scale()
75
- self.zoom_factor = max(self.min_scale, min(self.max_scale, self.zoom_factor))
76
- self.resetTransform()
77
- self.scale(self.zoom_factor, self.zoom_factor)
78
- self.empty = False
79
- self.setFocus()
80
- self.activateWindow()
81
- self.brush_preview.brush = self.brush
11
+ super().__init__(parent)
12
+ self.status = ImageViewStatus()
13
+ self._strategies = {
14
+ 'overlaid': OverlaidView(layer_collection, self.status, self),
15
+ 'sidebyside': SideBySideView(layer_collection, self.status, self),
16
+ 'topbottom': TopBottomView(layer_collection, self.status, self)
17
+ }
18
+ for strategy in self._strategies.values():
19
+ strategy.hide()
20
+ self.strategy = self._strategies['overlaid']
21
+ self.layout = QVBoxLayout(self)
22
+ self.layout.setContentsMargins(0, 0, 0, 0)
23
+ self.strategy = self._strategies['overlaid']
24
+ self.layout.addWidget(self.strategy)
25
+ self.strategy.show()
26
+
27
+ def set_strategy(self, label):
28
+ new_strategy = self._strategies.get(label, None)
29
+ if new_strategy is None:
30
+ raise RuntimeError(f"View strategy {label} is invalid.")
31
+ self.layout.removeWidget(self.strategy)
32
+ self.strategy.hide()
33
+ self.strategy = new_strategy
34
+ self.layout.addWidget(self.strategy)
35
+ self.strategy.show()
36
+ self.strategy.resize(self.size())
37
+ if not self.strategy.empty():
38
+ self.strategy.cleanup_brush_preview()
39
+ self.strategy.update_master_display()
40
+ self.strategy.update_current_display()
41
+ self.strategy.setup_brush_cursor()
42
+ self.strategy.update_brush_cursor()
43
+ self.strategy.show_master()
44
+ self.strategy.setFocus()
45
+ self.strategy.activateWindow()
46
+
47
+ def empty(self):
48
+ return self.strategy.empty()
49
+
50
+ def set_master_image_np(self, img):
51
+ self.strategy.set_master_image_np(img)
82
52
 
83
53
  def clear_image(self):
84
- self.scene.clear()
85
- self.pixmap_item = QGraphicsPixmapItem()
86
- self.scene.addItem(self.pixmap_item)
87
- self.zoom_factor = 1.0
88
- self.setup_brush_cursor()
89
- self.brush_preview = BrushPreviewItem(self.layer_collection)
90
- self.scene.addItem(self.brush_preview)
91
- self.setCursor(Qt.ArrowCursor)
92
- self.brush_cursor.hide()
93
- self.empty = True
94
-
95
- # pylint: disable=C0103
96
- def keyPressEvent(self, event):
97
- if self.empty:
98
- return
99
- if event.key() == Qt.Key_Space and not self.scrolling:
100
- self.space_pressed = True
101
- self.setCursor(Qt.OpenHandCursor)
102
- if self.brush_cursor:
103
- self.brush_cursor.hide()
104
- elif event.key() == Qt.Key_X:
105
- self.temp_view_requested.emit(True)
106
- self.update_brush_cursor()
107
- return
108
- if event.key() == Qt.Key_Control and not self.scrolling:
109
- self.control_pressed = True
110
- super().keyPressEvent(event)
54
+ for st in self._strategies.values():
55
+ st.clear_image()
111
56
 
112
- def keyReleaseEvent(self, event):
113
- if self.empty:
114
- return
115
- self.update_brush_cursor()
116
- if event.key() == Qt.Key_Space:
117
- self.space_pressed = False
118
- if not self.scrolling:
119
- self.setCursor(Qt.BlankCursor)
120
- if self.brush_cursor:
121
- self.brush_cursor.show()
122
- elif event.key() == Qt.Key_X:
123
- self.temp_view_requested.emit(False)
124
- return
125
- if event.key() == Qt.Key_Control:
126
- self.control_pressed = False
127
- super().keyReleaseEvent(event)
57
+ def show_master(self):
58
+ self.strategy.show_master()
128
59
 
129
- def mousePressEvent(self, event):
130
- if self.empty:
131
- return
132
- if event.button() == Qt.LeftButton and self.layer_collection.has_master_layer():
133
- if self.space_pressed:
134
- self.scrolling = True
135
- self.last_mouse_pos = event.position()
136
- self.setCursor(Qt.ClosedHandCursor)
137
- if self.brush_cursor:
138
- self.brush_cursor.hide()
139
- else:
140
- self.last_brush_pos = event.position()
141
- self.brush_operation_started.emit(event.position().toPoint())
142
- self.dragging = True
143
- if self.brush_cursor:
144
- self.brush_cursor.show()
145
- super().mousePressEvent(event)
60
+ def show_current(self):
61
+ self.strategy.show_current()
146
62
 
147
- def mouseMoveEvent(self, event):
148
- if self.empty:
149
- return
150
- position = event.position()
151
- brush_size = self.brush.size
152
- if not self.space_pressed:
153
- self.update_brush_cursor()
154
- if self.dragging and event.buttons() & Qt.LeftButton:
155
- current_time = QTime.currentTime()
156
- if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
157
- min_step = brush_size * \
158
- gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor
159
- x, y = position.x(), position.y()
160
- xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
161
- distance = math.sqrt((x - xp)**2 + (y - yp)**2)
162
- n_steps = int(float(distance) / min_step)
163
- if n_steps > 0:
164
- delta_x = (position.x() - self.last_brush_pos.x()) / n_steps
165
- delta_y = (position.y() - self.last_brush_pos.y()) / n_steps
166
- for i in range(0, n_steps + 1):
167
- pos = QPoint(self.last_brush_pos.x() + i * delta_x,
168
- self.last_brush_pos.y() + i * delta_y)
169
- self.brush_operation_continued.emit(pos)
170
- self.last_brush_pos = position
171
- self.last_update_time = current_time
172
- if self.scrolling and event.buttons() & Qt.LeftButton:
173
- if self.space_pressed:
174
- self.setCursor(Qt.ClosedHandCursor)
175
- if self.brush_cursor:
176
- self.brush_cursor.hide()
177
- delta = position - self.last_mouse_pos
178
- self.last_mouse_pos = position
179
- self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
180
- self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
181
- else:
182
- super().mouseMoveEvent(event)
63
+ def update_master_display(self):
64
+ self.strategy.update_master_display()
183
65
 
184
- def mouseReleaseEvent(self, event):
185
- if self.empty:
186
- return
187
- if self.space_pressed:
188
- self.setCursor(Qt.OpenHandCursor)
189
- if self.brush_cursor:
190
- self.brush_cursor.hide()
191
- else:
192
- self.setCursor(Qt.BlankCursor)
193
- if self.brush_cursor:
194
- self.brush_cursor.show()
195
- if event.button() == Qt.LeftButton:
196
- if self.scrolling:
197
- self.scrolling = False
198
- self.last_mouse_pos = None
199
- elif hasattr(self, 'dragging') and self.dragging:
200
- self.dragging = False
201
- self.brush_operation_ended.emit()
202
- super().mouseReleaseEvent(event)
66
+ def update_current_display(self):
67
+ self.strategy.update_current_display()
203
68
 
204
- def wheelEvent(self, event):
205
- if self.empty or self.gesture_active:
206
- return
207
- if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
208
- if self.control_pressed:
209
- self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
210
- else:
211
- zoom_in_factor = 1.10
212
- zoom_out_factor = 1 / zoom_in_factor
213
- current_scale = self.get_current_scale()
214
- if event.angleDelta().y() > 0: # Zoom in
215
- new_scale = current_scale * zoom_in_factor
216
- if new_scale <= self.max_scale:
217
- self.scale(zoom_in_factor, zoom_in_factor)
218
- self.zoom_factor = new_scale
219
- else: # Zoom out
220
- new_scale = current_scale * zoom_out_factor
221
- if new_scale >= self.min_scale:
222
- self.scale(zoom_out_factor, zoom_out_factor)
223
- self.zoom_factor = new_scale
224
- self.update_brush_cursor()
225
- else: # Touchpad event - fallback for systems without gesture recognition
226
- if not self.control_pressed:
227
- delta = event.pixelDelta() or event.angleDelta() / 8
228
- if delta:
229
- self.horizontalScrollBar().setValue(
230
- self.horizontalScrollBar().value() - delta.x()
231
- )
232
- self.verticalScrollBar().setValue(
233
- self.verticalScrollBar().value() - delta.y()
234
- )
235
- else: # Control + touchpad scroll for zoom
236
- zoom_in = event.angleDelta().y() > 0
237
- if zoom_in:
238
- self.zoom_in()
239
- else:
240
- self.zoom_out()
241
- event.accept()
242
-
243
- def enterEvent(self, event):
244
- self.activateWindow()
245
- self.setFocus()
246
- if not self.empty:
247
- self.setCursor(Qt.BlankCursor)
248
- if self.brush_cursor:
249
- self.brush_cursor.show()
250
- super().enterEvent(event)
69
+ def update_brush_cursor(self):
70
+ self.strategy.update_brush_cursor()
251
71
 
252
- def leaveEvent(self, event):
253
- if not self.empty:
254
- self.setCursor(Qt.ArrowCursor)
255
- if self.brush_cursor:
256
- self.brush_cursor.hide()
257
- super().leaveEvent(event)
258
- # pylint: enable=C0103
72
+ def refresh_display(self):
73
+ self.strategy.refresh_display()
259
74
 
260
- def event(self, event):
261
- if event.type() == QEvent.Gesture:
262
- return self.handle_gesture_event(event)
263
- return super().event(event)
75
+ def set_brush(self, brush):
76
+ for st in self._strategies.values():
77
+ st.set_brush(brush)
264
78
 
265
- def handle_gesture_event(self, event):
266
- handled = False
267
- pan_gesture = event.gesture(Qt.PanGesture)
268
- if pan_gesture:
269
- self.handle_pan_gesture(pan_gesture)
270
- handled = True
271
- pinch_gesture = event.gesture(Qt.PinchGesture)
272
- if pinch_gesture:
273
- self.handle_pinch_gesture(pinch_gesture)
274
- handled = True
275
- if handled:
276
- event.accept()
277
- return True
278
- return False
79
+ def set_preview_brush(self, brush):
80
+ for st in self._strategies.values():
81
+ st.set_preview_brush(brush)
279
82
 
280
- def handle_pan_gesture(self, pan_gesture):
281
- if pan_gesture.state() == Qt.GestureStarted:
282
- self.last_scroll_pos = pan_gesture.delta()
283
- self.gesture_active = True
284
- elif pan_gesture.state() == Qt.GestureUpdated:
285
- delta = pan_gesture.delta() - self.last_scroll_pos
286
- self.last_scroll_pos = pan_gesture.delta()
287
- zoom_factor = self.get_current_scale()
288
- scaled_delta = delta * (1.0 / zoom_factor)
289
- self.horizontalScrollBar().setValue(
290
- self.horizontalScrollBar().value() - int(scaled_delta.x())
291
- )
292
- self.verticalScrollBar().setValue(
293
- self.verticalScrollBar().value() - int(scaled_delta.y())
294
- )
295
- elif pan_gesture.state() == Qt.GestureFinished:
296
- self.gesture_active = False
83
+ def set_display_manager(self, dm):
84
+ for st in self._strategies.values():
85
+ st.set_display_manager(dm)
297
86
 
298
- def handle_pinch_gesture(self, pinch):
299
- if pinch.state() == Qt.GestureStarted:
300
- self.pinch_start_scale = self.get_current_scale()
301
- self.pinch_center_view = pinch.centerPoint()
302
- self.pinch_center_scene = self.mapToScene(self.pinch_center_view.toPoint())
303
- self.gesture_active = True
304
- elif pinch.state() == Qt.GestureUpdated:
305
- new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
306
- new_scale = max(self.min_scale, min(new_scale, self.max_scale))
307
- if abs(new_scale - self.get_current_scale()) > 0.01:
308
- self.resetTransform()
309
- self.scale(new_scale, new_scale)
310
- self.zoom_factor = new_scale
311
- new_center = self.mapToScene(self.pinch_center_view.toPoint())
312
- delta = self.pinch_center_scene - new_center
313
- self.translate(delta.x(), delta.y())
314
- self.update_brush_cursor()
315
- elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
316
- self.gesture_active = False
87
+ def set_allow_cursor_preview(self, state):
88
+ self.strategy.set_allow_cursor_preview(state)
317
89
 
318
90
  def setup_brush_cursor(self):
319
- self.setCursor(Qt.BlankCursor)
320
- pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
321
- brush = QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner']))
322
- for item in self.scene.items():
323
- if isinstance(item, QGraphicsEllipseItem):
324
- self.scene.removeItem(item)
325
- self.brush_cursor = self.scene.addEllipse(
326
- 0, 0, self.brush.size, self.brush.size, pen, brush)
327
- self.brush_cursor.setZValue(1000)
328
- self.brush_cursor.hide()
329
-
330
- def update_brush_cursor(self):
331
- if self.empty:
332
- return
333
- if not self.brush_cursor or not self.isVisible():
334
- return
335
- size = self.brush.size
336
- mouse_pos = self.mapFromGlobal(QCursor.pos())
337
- if not self.rect().contains(mouse_pos):
338
- self.brush_cursor.hide()
339
- return
340
- scene_pos = self.mapToScene(mouse_pos)
341
- center_x = scene_pos.x()
342
- center_y = scene_pos.y()
343
- radius = size / 2
344
- self.brush_cursor.setRect(center_x - radius, center_y - radius, size, size)
345
- allow_cursor_preview = self.display_manager.allow_cursor_preview()
346
- if self.cursor_style == 'preview' and allow_cursor_preview:
347
- self._setup_outline_style()
348
- self.brush_cursor.hide()
349
- pos = QCursor.pos()
350
- if isinstance(pos, QPointF):
351
- scene_pos = pos
352
- else:
353
- cursor_pos = self.mapFromGlobal(pos)
354
- scene_pos = self.mapToScene(cursor_pos)
355
- self.brush_preview.update(scene_pos, int(size))
356
- else:
357
- self.brush_preview.hide()
358
- if self.cursor_style == 'outline' or not allow_cursor_preview:
359
- self._setup_outline_style()
360
- else:
361
- self._setup_simple_brush_style(center_x, center_y, radius)
362
- if not self.brush_cursor.isVisible():
363
- self.brush_cursor.show()
364
-
365
- def _setup_outline_style(self):
366
- self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
367
- gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
368
- self.brush_cursor.setBrush(Qt.NoBrush)
369
-
370
- def _setup_simple_brush_style(self, center_x, center_y, radius):
371
- gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
372
- self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
373
- gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
374
- self.brush_cursor.setBrush(QBrush(gradient))
91
+ self.strategy.setup_brush_cursor()
375
92
 
376
93
  def zoom_in(self):
377
- if self.empty:
378
- return
379
- current_scale = self.get_current_scale()
380
- new_scale = current_scale * gui_constants.ZOOM_IN_FACTOR
381
- if new_scale <= self.max_scale:
382
- self.scale(gui_constants.ZOOM_IN_FACTOR, gui_constants.ZOOM_IN_FACTOR)
383
- self.zoom_factor = new_scale
384
- self.update_brush_cursor()
94
+ self.strategy.zoom_in()
385
95
 
386
96
  def zoom_out(self):
387
- if self.empty:
388
- return
389
- current_scale = self.get_current_scale()
390
- new_scale = current_scale * gui_constants.ZOOM_OUT_FACTOR
391
- if new_scale >= self.min_scale:
392
- self.scale(gui_constants.ZOOM_OUT_FACTOR, gui_constants.ZOOM_OUT_FACTOR)
393
- self.zoom_factor = new_scale
394
- self.update_brush_cursor()
97
+ self.strategy.zoom_out()
395
98
 
396
99
  def reset_zoom(self):
397
- if self.empty:
398
- return
399
- self.pinch_start_scale = 1.0
400
- self.last_scroll_pos = QPointF()
401
- self.gesture_active = False
402
- self.pinch_center_view = None
403
- self.pinch_center_scene = None
404
- self.fitInView(self.pixmap_item, Qt.KeepAspectRatio)
405
- self.zoom_factor = self.get_current_scale()
406
- self.zoom_factor = max(self.min_scale, min(self.max_scale, self.zoom_factor))
407
- self.resetTransform()
408
- self.scale(self.zoom_factor, self.zoom_factor)
409
- self.update_brush_cursor()
100
+ self.strategy.reset_zoom()
410
101
 
411
102
  def actual_size(self):
412
- if self.empty:
413
- return
414
- self.zoom_factor = max(self.min_scale, min(self.max_scale, 1.0))
415
- self.resetTransform()
416
- self.scale(self.zoom_factor, self.zoom_factor)
417
- self.update_brush_cursor()
103
+ self.strategy.actual_size()
418
104
 
419
105
  def get_current_scale(self):
420
- return self.transform().m11()
106
+ return self.strategy.get_current_scale()
421
107
 
422
- def get_view_state(self):
423
- return {
424
- 'zoom': self.zoom_factor,
425
- 'h_scroll': self.horizontalScrollBar().value(),
426
- 'v_scroll': self.verticalScrollBar().value()
427
- }
428
-
429
- def set_view_state(self, state):
430
- if state:
431
- self.resetTransform()
432
- self.scale(state['zoom'], state['zoom'])
433
- self.horizontalScrollBar().setValue(state['h_scroll'])
434
- self.verticalScrollBar().setValue(state['v_scroll'])
435
- self.zoom_factor = state['zoom']
108
+ def get_cursor_style(self):
109
+ return self.strategy.get_cursor_style()
436
110
 
437
111
  def set_cursor_style(self, style):
438
- self.cursor_style = style
439
- if self.brush_cursor:
440
- self.update_brush_cursor()
112
+ self.strategy.set_cursor_style(style)
441
113
 
442
114
  def position_on_image(self, pos):
443
- scene_pos = self.mapToScene(pos)
444
- item_pos = self.pixmap_item.mapFromScene(scene_pos)
445
- return item_pos
446
-
447
- def get_visible_image_region(self):
448
- if self.empty:
449
- return None
450
- view_rect = self.viewport().rect()
451
- scene_rect = self.mapToScene(view_rect).boundingRect()
452
- image_rect = self.pixmap_item.mapFromScene(scene_rect).boundingRect()
453
- image_rect = image_rect.intersected(self.pixmap_item.boundingRect().toRect())
454
- return image_rect
115
+ return self.strategy.position_on_image(pos)
455
116
 
456
117
  def get_visible_image_portion(self):
457
- if self.has_no_master_layer():
458
- return None
459
- visible_rect = self.get_visible_image_region()
460
- if not visible_rect:
461
- return self.master_layer()
462
- x, y = int(visible_rect.x()), int(visible_rect.y())
463
- w, h = int(visible_rect.width()), int(visible_rect.height())
464
- master_img = self.master_layer()
465
- return master_img[y:y + h, x:x + w], (x, y, w, h)
118
+ return self.strategy.get_visible_image_portion()
119
+
120
+ def connect_signals(
121
+ self, handle_temp_view, begin_copy_brush_area, continue_copy_brush_area,
122
+ end_copy_brush_area, handle_brush_size_change):
123
+ for st in self._strategies.values():
124
+ st.temp_view_requested.connect(handle_temp_view)
125
+ st.brush_operation_started.connect(begin_copy_brush_area)
126
+ st.brush_operation_continued.connect(continue_copy_brush_area)
127
+ st.brush_operation_ended.connect(end_copy_brush_area)
128
+ st.brush_size_change_requested.connect(handle_brush_size_change)
129
+ st.setFocusPolicy(Qt.StrongFocus)
@@ -16,6 +16,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
16
16
  update_title_requested = Signal()
17
17
  mark_as_modified_requested = Signal(bool)
18
18
  change_layer_requested = Signal(int)
19
+ add_recent_file_requested = Signal(str)
19
20
 
20
21
  def __init__(self, layer_collection, undo_manager, parent):
21
22
  QObject.__init__(self, parent)
@@ -54,9 +55,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
54
55
  else:
55
56
  self.set_layer_labels(labels)
56
57
  self.set_master_layer(master_layer)
58
+ self.image_viewer.set_master_image_np(master_layer)
59
+ self.image_viewer.show_master()
60
+ self.image_viewer.update_master_display()
57
61
  self.undo_manager.reset()
58
62
  self.blank_layer = np.zeros(master_layer.shape[:2])
59
63
  self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
64
+ self.image_viewer.reset_zoom()
60
65
 
61
66
  def on_file_error(self, error_msg):
62
67
  QApplication.restoreOverrideCursor()
@@ -75,6 +80,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
75
80
  self.saving_dialog.deleteLater()
76
81
  self.mark_as_modified_requested.emit(False)
77
82
  self.update_title_requested.emit()
83
+ self.add_recent_file_requested.emit(self.current_file_path_multi)
78
84
  self.status_message_requested.emit(f"Saved multilayer to: {self.current_file_path_multi}")
79
85
 
80
86
  def on_multilayer_save_error(self, error_msg):
@@ -126,6 +132,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
126
132
  self.status_message_requested.emit("Imported selected frames")
127
133
 
128
134
  def import_frames_from_files(self, file_paths):
135
+ empty_viewer = self.image_viewer.empty()
129
136
  try:
130
137
  stack, labels, master = self.io_manager.import_frames(file_paths)
131
138
  except Exception as e:
@@ -135,6 +142,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
135
142
  msg.setText(str(e))
136
143
  msg.exec()
137
144
  return
145
+ self.image_viewer.set_master_image_np(master)
138
146
  if self.layer_stack() is None and len(stack) > 0:
139
147
  self.set_layer_stack(np.array(stack))
140
148
  if labels is None:
@@ -150,15 +158,17 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
150
158
  self.add_layer_label(label)
151
159
  self.add_layer(img)
152
160
  self.finish_loading_setup("Selected frames imported")
161
+ if empty_viewer:
162
+ self.image_viewer.reset_zoom()
153
163
 
154
164
  def finish_loading_setup(self, message):
155
165
  self.display_manager.update_thumbnails()
156
166
  self.mark_as_modified_requested.emit(True)
157
167
  self.change_layer_requested.emit(0)
158
168
  self.image_viewer.setup_brush_cursor()
159
- self.image_viewer.reset_zoom()
160
169
  self.status_message_requested.emit(message)
161
170
  self.update_title_requested.emit()
171
+ self.add_recent_file_requested.emit(self.current_file_path_master)
162
172
 
163
173
  def save_file(self):
164
174
  if self.save_master_only.isChecked():
@@ -218,7 +228,6 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
218
228
  self.saving_timer.timeout.connect(self.saving_dialog.show)
219
229
  self.saving_timer.start(100)
220
230
  self.saver_thread.start()
221
-
222
231
  except Exception as e:
223
232
  traceback.print_tb(e.__traceback__)
224
233
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -247,6 +256,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
247
256
  self.mark_as_modified_requested.emit(False)
248
257
  self.update_title_requested.emit()
249
258
  self.status_message_requested.emit(f"Saved master layer to: {path}")
259
+ self.add_recent_file_requested.emit(self.current_file_path_master)
250
260
  except Exception as e:
251
261
  traceback.print_tb(e.__traceback__)
252
262
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -143,6 +143,9 @@ class LayerCollectionHandler:
143
143
  def has_master_layer(self):
144
144
  return self.layer_collection.has_master_layer()
145
145
 
146
+ def set_layer(self, idx, img):
147
+ self.layer_collection.layer_stack[idx] = img
148
+
146
149
  def set_layer_stack(self, stk):
147
150
  self.layer_collection.set_layer_stack(stk)
148
151