shinestacker 1.3.0__py3-none-any.whl → 1.4.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 (50) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +229 -41
  3. shinestacker/algorithms/align_auto.py +15 -3
  4. shinestacker/algorithms/align_parallel.py +81 -25
  5. shinestacker/algorithms/balance.py +23 -13
  6. shinestacker/algorithms/base_stack_algo.py +14 -20
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/noise_detection.py +3 -1
  9. shinestacker/algorithms/pyramid.py +8 -22
  10. shinestacker/algorithms/pyramid_auto.py +5 -14
  11. shinestacker/algorithms/pyramid_tiles.py +18 -20
  12. shinestacker/algorithms/stack_framework.py +1 -1
  13. shinestacker/algorithms/utils.py +37 -10
  14. shinestacker/algorithms/vignetting.py +2 -0
  15. shinestacker/app/gui_utils.py +10 -0
  16. shinestacker/app/main.py +3 -1
  17. shinestacker/app/project.py +3 -1
  18. shinestacker/app/retouch.py +3 -1
  19. shinestacker/config/gui_constants.py +2 -2
  20. shinestacker/core/core_utils.py +10 -1
  21. shinestacker/gui/action_config.py +172 -7
  22. shinestacker/gui/action_config_dialog.py +443 -452
  23. shinestacker/gui/colors.py +1 -0
  24. shinestacker/gui/folder_file_selection.py +5 -0
  25. shinestacker/gui/gui_run.py +2 -2
  26. shinestacker/gui/main_window.py +18 -9
  27. shinestacker/gui/menu_manager.py +26 -2
  28. shinestacker/gui/new_project.py +5 -5
  29. shinestacker/gui/project_controller.py +4 -0
  30. shinestacker/gui/project_editor.py +6 -4
  31. shinestacker/gui/recent_file_manager.py +93 -0
  32. shinestacker/gui/sys_mon.py +24 -23
  33. shinestacker/retouch/base_filter.py +5 -5
  34. shinestacker/retouch/brush_preview.py +3 -0
  35. shinestacker/retouch/brush_tool.py +11 -11
  36. shinestacker/retouch/display_manager.py +21 -37
  37. shinestacker/retouch/image_editor_ui.py +129 -71
  38. shinestacker/retouch/image_view_status.py +61 -0
  39. shinestacker/retouch/image_viewer.py +89 -431
  40. shinestacker/retouch/io_gui_handler.py +12 -2
  41. shinestacker/retouch/overlaid_view.py +212 -0
  42. shinestacker/retouch/shortcuts_help.py +13 -3
  43. shinestacker/retouch/sidebyside_view.py +479 -0
  44. shinestacker/retouch/view_strategy.py +466 -0
  45. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  46. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/RECORD +50 -45
  47. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  48. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  49. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  50. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,465 +1,123 @@
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.update_master_display()
39
+ self.strategy.update_current_display()
40
+
41
+ def empty(self):
42
+ return self.strategy.empty()
43
+
44
+ def set_master_image_np(self, img):
45
+ self.strategy.set_master_image_np(img)
82
46
 
83
47
  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)
48
+ for st in self._strategies.values():
49
+ st.clear_image()
111
50
 
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)
51
+ def show_master(self):
52
+ self.strategy.show_master()
128
53
 
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)
54
+ def show_current(self):
55
+ self.strategy.show_current()
146
56
 
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)
57
+ def update_master_display(self):
58
+ self.strategy.update_master_display()
183
59
 
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)
60
+ def update_current_display(self):
61
+ self.strategy.update_current_display()
203
62
 
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)
63
+ def update_brush_cursor(self):
64
+ self.strategy.update_brush_cursor()
251
65
 
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
66
+ def refresh_display(self):
67
+ self.strategy.refresh_display()
259
68
 
260
- def event(self, event):
261
- if event.type() == QEvent.Gesture:
262
- return self.handle_gesture_event(event)
263
- return super().event(event)
69
+ def set_brush(self, brush):
70
+ for st in self._strategies.values():
71
+ st.set_brush(brush)
264
72
 
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
73
+ def set_preview_brush(self, brush):
74
+ for st in self._strategies.values():
75
+ st.set_preview_brush(brush)
279
76
 
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
77
+ def set_display_manager(self, dm):
78
+ for st in self._strategies.values():
79
+ st.set_display_manager(dm)
297
80
 
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
81
+ def set_allow_cursor_preview(self, state):
82
+ self.strategy.set_allow_cursor_preview(state)
317
83
 
318
84
  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))
85
+ self.strategy.setup_brush_cursor()
375
86
 
376
87
  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()
88
+ self.strategy.zoom_in()
385
89
 
386
90
  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()
91
+ self.strategy.zoom_out()
395
92
 
396
93
  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()
94
+ self.strategy.reset_zoom()
410
95
 
411
96
  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()
97
+ self.strategy.actual_size()
418
98
 
419
99
  def get_current_scale(self):
420
- return self.transform().m11()
100
+ return self.strategy.get_current_scale()
421
101
 
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']
102
+ def get_cursor_style(self):
103
+ return self.strategy.get_cursor_style()
436
104
 
437
105
  def set_cursor_style(self, style):
438
- self.cursor_style = style
439
- if self.brush_cursor:
440
- self.update_brush_cursor()
106
+ self.strategy.set_cursor_style(style)
441
107
 
442
108
  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
109
+ return self.strategy.position_on_image(pos)
455
110
 
456
111
  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)
112
+ return self.strategy.get_visible_image_portion()
113
+
114
+ def connect_signals(
115
+ self, handle_temp_view, begin_copy_brush_area, continue_copy_brush_area,
116
+ end_copy_brush_area, handle_brush_size_change):
117
+ for st in self._strategies.values():
118
+ st.temp_view_requested.connect(handle_temp_view)
119
+ st.brush_operation_started.connect(begin_copy_brush_area)
120
+ st.brush_operation_continued.connect(continue_copy_brush_area)
121
+ st.brush_operation_ended.connect(end_copy_brush_area)
122
+ st.brush_size_change_requested.connect(handle_brush_size_change)
123
+ 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)}")