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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +198 -18
- shinestacker/algorithms/align_parallel.py +17 -1
- shinestacker/algorithms/balance.py +23 -13
- shinestacker/algorithms/noise_detection.py +3 -1
- shinestacker/algorithms/utils.py +21 -10
- shinestacker/algorithms/vignetting.py +2 -0
- shinestacker/app/main.py +1 -1
- shinestacker/config/gui_constants.py +7 -2
- shinestacker/core/core_utils.py +10 -1
- shinestacker/gui/action_config.py +172 -7
- shinestacker/gui/action_config_dialog.py +246 -285
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/main_window.py +14 -5
- shinestacker/gui/menu_manager.py +26 -2
- shinestacker/gui/project_controller.py +4 -0
- shinestacker/gui/recent_file_manager.py +93 -0
- shinestacker/retouch/base_filter.py +13 -15
- shinestacker/retouch/brush_preview.py +3 -1
- shinestacker/retouch/brush_tool.py +11 -11
- shinestacker/retouch/display_manager.py +43 -59
- shinestacker/retouch/image_editor_ui.py +161 -82
- shinestacker/retouch/image_view_status.py +65 -0
- shinestacker/retouch/image_viewer.py +95 -431
- shinestacker/retouch/io_gui_handler.py +12 -2
- shinestacker/retouch/layer_collection.py +3 -0
- shinestacker/retouch/overlaid_view.py +215 -0
- shinestacker/retouch/shortcuts_help.py +13 -3
- shinestacker/retouch/sidebyside_view.py +477 -0
- shinestacker/retouch/transformation_manager.py +43 -0
- shinestacker/retouch/undo_manager.py +22 -3
- shinestacker/retouch/view_strategy.py +557 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/METADATA +7 -7
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/RECORD +38 -32
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
3
|
-
from PySide6.QtWidgets import
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
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(
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
self.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.
|
|
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.
|
|
85
|
-
|
|
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
|
|
113
|
-
|
|
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
|
|
130
|
-
|
|
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
|
|
148
|
-
|
|
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
|
|
185
|
-
|
|
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
|
|
205
|
-
|
|
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
|
|
253
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
299
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
106
|
+
return self.strategy.get_current_scale()
|
|
421
107
|
|
|
422
|
-
def
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|