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
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
|
|
2
|
+
from PySide6.QtCore import Qt, Signal, QEvent, QRectF
|
|
3
|
+
from PySide6.QtGui import QPen, QColor, QCursor
|
|
4
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QFrame, QGraphicsEllipseItem
|
|
5
|
+
from .. config.gui_constants import gui_constants
|
|
6
|
+
from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImageGraphicsView(ImageGraphicsViewBase):
|
|
10
|
+
mouse_pressed = Signal(QEvent)
|
|
11
|
+
mouse_moved = Signal(QEvent)
|
|
12
|
+
mouse_released = Signal(QEvent)
|
|
13
|
+
gesture_event = Signal(QEvent)
|
|
14
|
+
wheel_event = Signal(QEvent)
|
|
15
|
+
|
|
16
|
+
# pylint: disable=C0103
|
|
17
|
+
def event(self, event):
|
|
18
|
+
if event.type() == QEvent.Gesture:
|
|
19
|
+
self.gesture_event.emit(event)
|
|
20
|
+
return True
|
|
21
|
+
return super().event(event)
|
|
22
|
+
|
|
23
|
+
def mousePressEvent(self, event):
|
|
24
|
+
self.mouse_pressed.emit(event)
|
|
25
|
+
super().mousePressEvent(event)
|
|
26
|
+
|
|
27
|
+
def mouseMoveEvent(self, event):
|
|
28
|
+
self.mouse_moved.emit(event)
|
|
29
|
+
|
|
30
|
+
def mouseReleaseEvent(self, event):
|
|
31
|
+
self.mouse_released.emit(event)
|
|
32
|
+
super().mouseReleaseEvent(event)
|
|
33
|
+
|
|
34
|
+
def wheelEvent(self, event):
|
|
35
|
+
self.wheel_event.emit(event)
|
|
36
|
+
event.accept()
|
|
37
|
+
# pylint: enable=C0103
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
41
|
+
def __init__(self, layer_collection, status, parent):
|
|
42
|
+
ViewStrategy.__init__(self, layer_collection, status)
|
|
43
|
+
QWidget.__init__(self, parent)
|
|
44
|
+
self.current_view = ImageGraphicsView(parent)
|
|
45
|
+
self.master_view = ImageGraphicsView(parent)
|
|
46
|
+
self.current_scene = self.create_scene(self.current_view)
|
|
47
|
+
self.master_scene = self.create_scene(self.master_view)
|
|
48
|
+
self.create_pixmaps()
|
|
49
|
+
self.master_scene.addItem(self.brush_preview)
|
|
50
|
+
self.setup_layout()
|
|
51
|
+
self._connect_signals()
|
|
52
|
+
self.panning_current = False
|
|
53
|
+
self.brush_cursor = None
|
|
54
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
|
55
|
+
self.pan_start = None
|
|
56
|
+
self.pinch_start_scale = None
|
|
57
|
+
self.current_view.installEventFilter(self)
|
|
58
|
+
self.master_view.installEventFilter(self)
|
|
59
|
+
self.current_view.setFocusPolicy(Qt.NoFocus)
|
|
60
|
+
self.master_view.setFocusPolicy(Qt.NoFocus)
|
|
61
|
+
self.current_brush_cursor = None
|
|
62
|
+
self.setup_current_brush_cursor()
|
|
63
|
+
|
|
64
|
+
def setup_layout(self):
|
|
65
|
+
raise NotImplementedError("Subclasses must implement setup_layout")
|
|
66
|
+
|
|
67
|
+
def create_pixmaps(self):
|
|
68
|
+
self.pixmap_item_current = self.create_pixmap(self.current_scene)
|
|
69
|
+
self.pixmap_item_master = self.create_pixmap(self.master_scene)
|
|
70
|
+
|
|
71
|
+
def _connect_signals(self):
|
|
72
|
+
self.current_view.mouse_pressed.connect(self.handle_current_mouse_press)
|
|
73
|
+
self.current_view.mouse_moved.connect(self.handle_current_mouse_move)
|
|
74
|
+
self.current_view.mouse_released.connect(self.handle_current_mouse_release)
|
|
75
|
+
self.current_view.gesture_event.connect(self.handle_gesture_event)
|
|
76
|
+
self.master_view.mouse_pressed.connect(self.handle_master_mouse_press)
|
|
77
|
+
self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
|
|
78
|
+
self.master_view.mouse_released.connect(self.handle_master_mouse_release)
|
|
79
|
+
self.master_view.gesture_event.connect(self.handle_gesture_event)
|
|
80
|
+
self.current_view.horizontalScrollBar().valueChanged.connect(
|
|
81
|
+
self.master_view.horizontalScrollBar().setValue)
|
|
82
|
+
self.current_view.verticalScrollBar().valueChanged.connect(
|
|
83
|
+
self.master_view.verticalScrollBar().setValue)
|
|
84
|
+
self.master_view.horizontalScrollBar().valueChanged.connect(
|
|
85
|
+
self.current_view.horizontalScrollBar().setValue)
|
|
86
|
+
self.master_view.verticalScrollBar().valueChanged.connect(
|
|
87
|
+
self.current_view.verticalScrollBar().setValue)
|
|
88
|
+
self.current_view.wheel_event.connect(self.handle_wheel_event)
|
|
89
|
+
self.master_view.wheel_event.connect(self.handle_wheel_event)
|
|
90
|
+
# pylint: disable=C0103, W0201
|
|
91
|
+
self.current_view.enterEvent = self.current_view_enter_event
|
|
92
|
+
self.current_view.leaveEvent = self.current_view_leave_event
|
|
93
|
+
self.master_view.enterEvent = self.master_view_enter_event
|
|
94
|
+
self.master_view.leaveEvent = self.master_view_leave_event
|
|
95
|
+
# pylint: enable=C0103, W0201
|
|
96
|
+
|
|
97
|
+
def current_view_enter_event(self, event):
|
|
98
|
+
self.activateWindow()
|
|
99
|
+
self.setFocus()
|
|
100
|
+
if not self.empty():
|
|
101
|
+
self.update_brush_cursor()
|
|
102
|
+
super(ImageGraphicsView, self.current_view).enterEvent(event)
|
|
103
|
+
|
|
104
|
+
def current_view_leave_event(self, event):
|
|
105
|
+
if not self.empty():
|
|
106
|
+
self.update_brush_cursor()
|
|
107
|
+
super(ImageGraphicsView, self.current_view).leaveEvent(event)
|
|
108
|
+
|
|
109
|
+
def master_view_enter_event(self, event):
|
|
110
|
+
self.activateWindow()
|
|
111
|
+
self.setFocus()
|
|
112
|
+
if not self.empty():
|
|
113
|
+
self.update_brush_cursor()
|
|
114
|
+
super(ImageGraphicsView, self.master_view).enterEvent(event)
|
|
115
|
+
|
|
116
|
+
def master_view_leave_event(self, event):
|
|
117
|
+
if not self.empty():
|
|
118
|
+
self.update_brush_cursor()
|
|
119
|
+
super(ImageGraphicsView, self.master_view).leaveEvent(event)
|
|
120
|
+
|
|
121
|
+
def get_master_view(self):
|
|
122
|
+
return self.master_view
|
|
123
|
+
|
|
124
|
+
def get_current_view(self):
|
|
125
|
+
return self.current_view
|
|
126
|
+
|
|
127
|
+
def get_master_scene(self):
|
|
128
|
+
return self.master_scene
|
|
129
|
+
|
|
130
|
+
def get_current_scene(self):
|
|
131
|
+
return self.current_scene
|
|
132
|
+
|
|
133
|
+
def get_master_pixmap(self):
|
|
134
|
+
return self.pixmap_item_master
|
|
135
|
+
|
|
136
|
+
def get_current_pixmap(self):
|
|
137
|
+
return self.pixmap_item_current
|
|
138
|
+
|
|
139
|
+
def get_views(self):
|
|
140
|
+
return [self.master_view, self.current_view]
|
|
141
|
+
|
|
142
|
+
def get_scenes(self):
|
|
143
|
+
return [self.master_scene, self.current_scene]
|
|
144
|
+
|
|
145
|
+
def get_pixmaps(self):
|
|
146
|
+
return {
|
|
147
|
+
self.pixmap_item_master: self.master_view,
|
|
148
|
+
self.pixmap_item_current: self.current_view
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# pylint: disable=C0103
|
|
152
|
+
def focusInEvent(self, event):
|
|
153
|
+
super().focusInEvent(event)
|
|
154
|
+
self.activateWindow()
|
|
155
|
+
self.setFocus()
|
|
156
|
+
|
|
157
|
+
def eventFilter(self, obj, event):
|
|
158
|
+
if obj in [self.current_view, self.master_view]:
|
|
159
|
+
if event.type() == QEvent.KeyPress:
|
|
160
|
+
self.keyPressEvent(event)
|
|
161
|
+
return True
|
|
162
|
+
if event.type() == QEvent.KeyRelease:
|
|
163
|
+
self.keyReleaseEvent(event)
|
|
164
|
+
return True
|
|
165
|
+
return super().eventFilter(obj, event)
|
|
166
|
+
|
|
167
|
+
def showEvent(self, event):
|
|
168
|
+
super().showEvent(event)
|
|
169
|
+
self.update_brush_cursor()
|
|
170
|
+
|
|
171
|
+
def enterEvent(self, event):
|
|
172
|
+
self.activateWindow()
|
|
173
|
+
self.setFocus()
|
|
174
|
+
if self.empty():
|
|
175
|
+
self.setCursor(Qt.ArrowCursor)
|
|
176
|
+
self.master_view.setCursor(Qt.ArrowCursor)
|
|
177
|
+
self.current_view.setCursor(Qt.ArrowCursor)
|
|
178
|
+
else:
|
|
179
|
+
if self.space_pressed:
|
|
180
|
+
self.master_view.setCursor(Qt.OpenHandCursor)
|
|
181
|
+
self.current_view.setCursor(Qt.OpenHandCursor)
|
|
182
|
+
else:
|
|
183
|
+
self.master_view.setCursor(Qt.BlankCursor)
|
|
184
|
+
self.current_view.setCursor(Qt.BlankCursor)
|
|
185
|
+
if self.brush_cursor:
|
|
186
|
+
self.brush_cursor.show()
|
|
187
|
+
super().enterEvent(event)
|
|
188
|
+
|
|
189
|
+
def leaveEvent(self, event):
|
|
190
|
+
if self.empty():
|
|
191
|
+
self.setCursor(Qt.ArrowCursor)
|
|
192
|
+
self.master_view.setCursor(Qt.ArrowCursor)
|
|
193
|
+
self.current_view.setCursor(Qt.ArrowCursor)
|
|
194
|
+
else:
|
|
195
|
+
if self.brush_cursor is not None:
|
|
196
|
+
self.brush_cursor.hide()
|
|
197
|
+
if self.current_brush_cursor is not None:
|
|
198
|
+
self.current_brush_cursor.hide()
|
|
199
|
+
self.master_view.setCursor(Qt.ArrowCursor)
|
|
200
|
+
self.current_view.setCursor(Qt.ArrowCursor)
|
|
201
|
+
super().leaveEvent(event)
|
|
202
|
+
|
|
203
|
+
def keyPressEvent(self, event):
|
|
204
|
+
super().keyPressEvent(event)
|
|
205
|
+
if event.key() == Qt.Key_Space:
|
|
206
|
+
self.update_brush_cursor()
|
|
207
|
+
|
|
208
|
+
def keyReleaseEvent(self, event):
|
|
209
|
+
super().keyReleaseEvent(event)
|
|
210
|
+
if event.key() == Qt.Key_Space:
|
|
211
|
+
self.update_brush_cursor()
|
|
212
|
+
# pylint: enable=C0103
|
|
213
|
+
|
|
214
|
+
# pylint: enable=R0801
|
|
215
|
+
def handle_wheel_event(self, event):
|
|
216
|
+
if self.empty() or self.gesture_active:
|
|
217
|
+
return
|
|
218
|
+
if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
|
|
219
|
+
if self.control_pressed:
|
|
220
|
+
self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
221
|
+
else:
|
|
222
|
+
if event.angleDelta().y() > 0: # Zoom in
|
|
223
|
+
self.zoom_in()
|
|
224
|
+
else: # Zoom out
|
|
225
|
+
self.zoom_out()
|
|
226
|
+
else: # Touchpad event - handle scrolling
|
|
227
|
+
if not self.control_pressed:
|
|
228
|
+
delta = event.pixelDelta() or event.angleDelta() / 8
|
|
229
|
+
if delta:
|
|
230
|
+
self.scroll_view(self.master_view, delta.x(), delta.y())
|
|
231
|
+
self.scroll_view(self.current_view, delta.x(), delta.y())
|
|
232
|
+
# pylint: disable=R0801
|
|
233
|
+
|
|
234
|
+
def _apply_zoom_to_view(self, view, factor):
|
|
235
|
+
view.scale(factor, factor)
|
|
236
|
+
other_view = self.current_view if view == self.master_view else self.master_view
|
|
237
|
+
other_view.resetTransform()
|
|
238
|
+
other_view.scale(self.zoom_factor(), self.zoom_factor())
|
|
239
|
+
|
|
240
|
+
def show_master(self):
|
|
241
|
+
self.pixmap_item_master.setVisible(True)
|
|
242
|
+
self.pixmap_item_current.setVisible(True)
|
|
243
|
+
|
|
244
|
+
def show_current(self):
|
|
245
|
+
self.pixmap_item_master.setVisible(True)
|
|
246
|
+
self.pixmap_item_current.setVisible(True)
|
|
247
|
+
|
|
248
|
+
def setup_brush_cursor(self):
|
|
249
|
+
super().setup_brush_cursor()
|
|
250
|
+
self.setup_current_brush_cursor()
|
|
251
|
+
self.update_cursor_pen_width()
|
|
252
|
+
|
|
253
|
+
def setup_current_brush_cursor(self):
|
|
254
|
+
if not self.brush:
|
|
255
|
+
return
|
|
256
|
+
for item in self.current_scene.items():
|
|
257
|
+
if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
|
|
258
|
+
self.current_scene.removeItem(item)
|
|
259
|
+
pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
|
|
260
|
+
pen = QPen(QColor(255, 0, 0), pen_width, Qt.DotLine)
|
|
261
|
+
brush = Qt.NoBrush
|
|
262
|
+
self.current_brush_cursor = self.current_scene.addEllipse(
|
|
263
|
+
0, 0, self.brush.size, self.brush.size, pen, brush)
|
|
264
|
+
self.current_brush_cursor.setZValue(1000)
|
|
265
|
+
self.current_brush_cursor.hide()
|
|
266
|
+
|
|
267
|
+
def update_current_brush_cursor(self, scene_pos):
|
|
268
|
+
if not self.current_brush_cursor or not self.isVisible():
|
|
269
|
+
return
|
|
270
|
+
size = self.brush.size
|
|
271
|
+
radius = size / 2
|
|
272
|
+
self.current_brush_cursor.setRect(
|
|
273
|
+
scene_pos.x() - radius, scene_pos.y() - radius, size, size)
|
|
274
|
+
if self.brush_cursor and self.brush_cursor.isVisible():
|
|
275
|
+
self.current_brush_cursor.show()
|
|
276
|
+
else:
|
|
277
|
+
self.current_brush_cursor.hide()
|
|
278
|
+
|
|
279
|
+
def update_cursor_pen_width(self):
|
|
280
|
+
pen_width = super().update_cursor_pen_width()
|
|
281
|
+
if self.current_brush_cursor:
|
|
282
|
+
current_pen = self.current_brush_cursor.pen()
|
|
283
|
+
current_pen.setWidthF(pen_width)
|
|
284
|
+
self.current_brush_cursor.setPen(current_pen)
|
|
285
|
+
return pen_width
|
|
286
|
+
|
|
287
|
+
def update_brush_cursor(self):
|
|
288
|
+
if self.empty():
|
|
289
|
+
return
|
|
290
|
+
self.update_cursor_pen_width()
|
|
291
|
+
mouse_pos_global = QCursor.pos()
|
|
292
|
+
mouse_pos_current = self.current_view.mapFromGlobal(mouse_pos_global)
|
|
293
|
+
mouse_pos_master = self.master_view.mapFromGlobal(mouse_pos_global)
|
|
294
|
+
current_has_mouse = self.current_view.rect().contains(mouse_pos_current)
|
|
295
|
+
master_has_mouse = self.master_view.rect().contains(mouse_pos_master)
|
|
296
|
+
if master_has_mouse:
|
|
297
|
+
self.brush_preview.show()
|
|
298
|
+
super().update_brush_cursor()
|
|
299
|
+
self.sync_current_cursor_with_master()
|
|
300
|
+
if self.space_pressed:
|
|
301
|
+
cursor_style = Qt.OpenHandCursor if not self.scrolling else Qt.ClosedHandCursor
|
|
302
|
+
self.master_view.setCursor(cursor_style)
|
|
303
|
+
self.current_view.setCursor(cursor_style)
|
|
304
|
+
else:
|
|
305
|
+
self.master_view.setCursor(Qt.BlankCursor)
|
|
306
|
+
self.current_view.setCursor(Qt.BlankCursor)
|
|
307
|
+
elif current_has_mouse:
|
|
308
|
+
self.brush_preview.hide()
|
|
309
|
+
scene_pos = self.current_view.mapToScene(mouse_pos_current)
|
|
310
|
+
size = self.brush.size
|
|
311
|
+
radius = size / 2
|
|
312
|
+
self.current_brush_cursor.setRect(
|
|
313
|
+
scene_pos.x() - radius, scene_pos.y() - radius, size, size)
|
|
314
|
+
self.current_brush_cursor.show()
|
|
315
|
+
if self.brush_cursor:
|
|
316
|
+
self.brush_cursor.setRect(
|
|
317
|
+
scene_pos.x() - radius, scene_pos.y() - radius, size, size)
|
|
318
|
+
self.brush_cursor.show()
|
|
319
|
+
if self.space_pressed:
|
|
320
|
+
cursor_style = Qt.OpenHandCursor \
|
|
321
|
+
if not self.panning_current else Qt.ClosedHandCursor
|
|
322
|
+
self.current_view.setCursor(cursor_style)
|
|
323
|
+
self.master_view.setCursor(cursor_style)
|
|
324
|
+
else:
|
|
325
|
+
self.current_view.setCursor(Qt.BlankCursor)
|
|
326
|
+
self.master_view.setCursor(Qt.BlankCursor)
|
|
327
|
+
else:
|
|
328
|
+
if self.brush_cursor:
|
|
329
|
+
self.brush_cursor.hide()
|
|
330
|
+
if self.current_brush_cursor:
|
|
331
|
+
self.current_brush_cursor.hide()
|
|
332
|
+
self.master_view.setCursor(Qt.ArrowCursor)
|
|
333
|
+
self.current_view.setCursor(Qt.ArrowCursor)
|
|
334
|
+
|
|
335
|
+
def handle_master_mouse_press(self, event):
|
|
336
|
+
self.setFocus()
|
|
337
|
+
self.mouse_press_event(event)
|
|
338
|
+
|
|
339
|
+
def handle_master_mouse_move(self, event):
|
|
340
|
+
self.mouse_move_event(event)
|
|
341
|
+
self.update_brush_cursor()
|
|
342
|
+
|
|
343
|
+
def handle_master_mouse_release(self, event):
|
|
344
|
+
self.mouse_release_event(event)
|
|
345
|
+
|
|
346
|
+
def handle_current_mouse_press(self, event):
|
|
347
|
+
position = event.position()
|
|
348
|
+
if self.space_pressed:
|
|
349
|
+
self.pan_start = position
|
|
350
|
+
self.panning_current = True
|
|
351
|
+
self.update_brush_cursor()
|
|
352
|
+
|
|
353
|
+
def handle_current_mouse_move(self, event):
|
|
354
|
+
position = event.position()
|
|
355
|
+
if self.panning_current and self.space_pressed:
|
|
356
|
+
delta = position - self.pan_start
|
|
357
|
+
self.pan_start = position
|
|
358
|
+
self.scroll_view(self.current_view, delta.x(), delta.y())
|
|
359
|
+
else:
|
|
360
|
+
self.update_brush_cursor()
|
|
361
|
+
|
|
362
|
+
def handle_current_mouse_release(self, _event):
|
|
363
|
+
if self.panning_current:
|
|
364
|
+
self.panning_current = False
|
|
365
|
+
self.update_brush_cursor()
|
|
366
|
+
|
|
367
|
+
def handle_gesture_event(self, event):
|
|
368
|
+
if self.empty():
|
|
369
|
+
return
|
|
370
|
+
pinch_gesture = event.gesture(Qt.PinchGesture)
|
|
371
|
+
if pinch_gesture:
|
|
372
|
+
self.handle_pinch_gesture(pinch_gesture)
|
|
373
|
+
event.accept()
|
|
374
|
+
|
|
375
|
+
def set_master_image(self, qimage):
|
|
376
|
+
self.status.set_master_image(qimage)
|
|
377
|
+
pixmap = self.status.pixmap_master
|
|
378
|
+
self.master_view.setSceneRect(QRectF(pixmap.rect()))
|
|
379
|
+
self.pixmap_item_master.setPixmap(pixmap)
|
|
380
|
+
img_width, img_height = pixmap.width(), pixmap.height()
|
|
381
|
+
self.set_max_min_scales(img_width, img_height)
|
|
382
|
+
self.set_zoom_factor(1.0)
|
|
383
|
+
self.master_view.fitInView(self.pixmap_item_master, Qt.KeepAspectRatio)
|
|
384
|
+
self.set_zoom_factor(self.get_current_scale())
|
|
385
|
+
self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
|
|
386
|
+
self.master_view.resetTransform()
|
|
387
|
+
self.master_scene.scale(self.zoom_factor(), self.zoom_factor())
|
|
388
|
+
self.master_view.centerOn(self.pixmap_item_master)
|
|
389
|
+
center = self.master_scene.sceneRect().center()
|
|
390
|
+
self.brush_preview.setPos(max(0, min(center.x(), img_width)),
|
|
391
|
+
max(0, min(center.y(), img_height)))
|
|
392
|
+
self.master_scene.setSceneRect(QRectF(self.pixmap_item_master.boundingRect()))
|
|
393
|
+
self.center_image(self.master_view)
|
|
394
|
+
self.update_cursor_pen_width()
|
|
395
|
+
|
|
396
|
+
def set_current_image(self, qimage):
|
|
397
|
+
self.status.set_current_image(qimage)
|
|
398
|
+
pixmap = self.status.pixmap_current
|
|
399
|
+
self.current_scene.setSceneRect(QRectF(pixmap.rect()))
|
|
400
|
+
self.pixmap_item_current.setPixmap(pixmap)
|
|
401
|
+
self.current_view.resetTransform()
|
|
402
|
+
self.current_scene.scale(self.zoom_factor(), self.zoom_factor())
|
|
403
|
+
self.current_scene.setSceneRect(QRectF(self.pixmap_item_current.boundingRect()))
|
|
404
|
+
self.center_image(self.current_view)
|
|
405
|
+
self.update_cursor_pen_width()
|
|
406
|
+
|
|
407
|
+
def arrange_images(self):
|
|
408
|
+
if self.status.empty():
|
|
409
|
+
return
|
|
410
|
+
if self.pixmap_item_master.pixmap().height() == 0:
|
|
411
|
+
self.update_master_display()
|
|
412
|
+
self.update_current_display()
|
|
413
|
+
self.reset_zoom()
|
|
414
|
+
else:
|
|
415
|
+
self.center_image(self.master_view)
|
|
416
|
+
self.apply_zoom()
|
|
417
|
+
|
|
418
|
+
def set_brush(self, brush):
|
|
419
|
+
super().set_brush(brush)
|
|
420
|
+
if self.brush_cursor:
|
|
421
|
+
self.master_scene.removeItem(self.brush_cursor)
|
|
422
|
+
self.setup_brush_cursor()
|
|
423
|
+
self.setup_current_brush_cursor()
|
|
424
|
+
|
|
425
|
+
def clear_image(self):
|
|
426
|
+
super().clear_image()
|
|
427
|
+
self.setCursor(Qt.ArrowCursor)
|
|
428
|
+
self.master_view.setCursor(Qt.ArrowCursor)
|
|
429
|
+
self.current_view.setCursor(Qt.ArrowCursor)
|
|
430
|
+
if self.current_brush_cursor:
|
|
431
|
+
self.current_scene.removeItem(self.current_brush_cursor)
|
|
432
|
+
self.current_brush_cursor = None
|
|
433
|
+
|
|
434
|
+
def sync_current_cursor_with_master(self):
|
|
435
|
+
if not self.brush_cursor or not self.current_brush_cursor:
|
|
436
|
+
return
|
|
437
|
+
master_rect = self.brush_cursor.rect()
|
|
438
|
+
scene_pos = master_rect.center()
|
|
439
|
+
size = self.brush.size
|
|
440
|
+
radius = size / 2
|
|
441
|
+
self.current_brush_cursor.setRect(
|
|
442
|
+
scene_pos.x() - radius,
|
|
443
|
+
scene_pos.y() - radius,
|
|
444
|
+
size, size
|
|
445
|
+
)
|
|
446
|
+
if self.brush_cursor.isVisible():
|
|
447
|
+
self.current_brush_cursor.show()
|
|
448
|
+
else:
|
|
449
|
+
self.current_brush_cursor.hide()
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class SideBySideView(DoubleViewBase):
|
|
453
|
+
def setup_layout(self):
|
|
454
|
+
layout = QHBoxLayout(self)
|
|
455
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
456
|
+
layout.setSpacing(0)
|
|
457
|
+
layout.addWidget(self.current_view, 1)
|
|
458
|
+
separator = QFrame()
|
|
459
|
+
separator.setFrameShape(QFrame.VLine)
|
|
460
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
461
|
+
separator.setLineWidth(2)
|
|
462
|
+
layout.addWidget(separator, 0)
|
|
463
|
+
layout.addWidget(self.master_view, 1)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class TopBottomView(DoubleViewBase):
|
|
467
|
+
def setup_layout(self):
|
|
468
|
+
layout = QVBoxLayout(self)
|
|
469
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
470
|
+
layout.setSpacing(0)
|
|
471
|
+
layout.addWidget(self.current_view, 1)
|
|
472
|
+
separator = QFrame()
|
|
473
|
+
separator.setFrameShape(QFrame.HLine)
|
|
474
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
475
|
+
separator.setLineWidth(2)
|
|
476
|
+
layout.addWidget(separator, 0)
|
|
477
|
+
layout.addWidget(self.master_view, 1)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, W0718
|
|
2
|
+
import traceback
|
|
3
|
+
import cv2
|
|
4
|
+
from .. config.gui_constants import gui_constants
|
|
5
|
+
from .layer_collection import LayerCollectionHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TransfromationManager(LayerCollectionHandler):
|
|
9
|
+
def __init__(self, editor):
|
|
10
|
+
super().__init__(editor.layer_collection)
|
|
11
|
+
self.editor = editor
|
|
12
|
+
|
|
13
|
+
def transform(self, transf_func, label, undoable=True):
|
|
14
|
+
if self.has_no_master_layer():
|
|
15
|
+
return
|
|
16
|
+
if undoable:
|
|
17
|
+
try:
|
|
18
|
+
undo = self.editor.undo_manager
|
|
19
|
+
undo.x_start, undo.x_stop = 0, 1
|
|
20
|
+
undo.y_start, undo.y_stop = 0, 1
|
|
21
|
+
undo.save_undo_state(self.editor.master_layer(), label)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
traceback.print_tb(e.__traceback__)
|
|
24
|
+
self.set_master_layer(transf_func(self.master_layer()))
|
|
25
|
+
self.set_layer_stack([transf_func(layer) for layer in self.layer_stack()])
|
|
26
|
+
self.copy_master_layer()
|
|
27
|
+
self.editor.image_viewer.update_master_display()
|
|
28
|
+
self.editor.image_viewer.update_current_display()
|
|
29
|
+
self.editor.image_viewer.refresh_display()
|
|
30
|
+
self.editor.display_manager.update_thumbnails()
|
|
31
|
+
self.editor.mark_as_modified()
|
|
32
|
+
|
|
33
|
+
def rotate_90_cw(self, undoable=True):
|
|
34
|
+
self.transform(lambda img: cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE),
|
|
35
|
+
gui_constants.ROTATE_90_CW_LABEL, undoable)
|
|
36
|
+
|
|
37
|
+
def rotate_90_ccw(self, undoable=True):
|
|
38
|
+
self.transform(lambda img: cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE),
|
|
39
|
+
gui_constants.ROTATE_90_CCW_LABEL, undoable)
|
|
40
|
+
|
|
41
|
+
def rotate_180(self, undoable=True):
|
|
42
|
+
self.transform(lambda img: cv2.rotate(img, cv2.ROTATE_180),
|
|
43
|
+
gui_constants.ROTATE_180_LABEL, undoable)
|
|
@@ -6,8 +6,9 @@ from .. config.gui_constants import gui_constants
|
|
|
6
6
|
class UndoManager(QObject):
|
|
7
7
|
stack_changed = Signal(bool, str, bool, str)
|
|
8
8
|
|
|
9
|
-
def __init__(self):
|
|
9
|
+
def __init__(self, transformation_manager):
|
|
10
10
|
super().__init__()
|
|
11
|
+
self.transformation_manager = transformation_manager
|
|
11
12
|
self.x_start = None
|
|
12
13
|
self.y_start = None
|
|
13
14
|
self.x_end = None
|
|
@@ -59,7 +60,16 @@ class UndoManager(QObject):
|
|
|
59
60
|
'description': undo_state['description']
|
|
60
61
|
}
|
|
61
62
|
self.redo_stack.append(redo_state)
|
|
62
|
-
|
|
63
|
+
descr = undo_state['description']
|
|
64
|
+
if descr.startswith(gui_constants.ROTATE_LABEL):
|
|
65
|
+
if descr == gui_constants.ROTATE_90_CW_LABEL:
|
|
66
|
+
self.transformation_manager.rotate_90_ccw(False)
|
|
67
|
+
elif descr == gui_constants.ROTATE_90_CCW_LABEL:
|
|
68
|
+
self.transformation_manager.rotate_90_cw(False)
|
|
69
|
+
elif descr == gui_constants.ROTATE_180_LABEL:
|
|
70
|
+
self.transformation_manager.rotate_180(False)
|
|
71
|
+
else:
|
|
72
|
+
layer[y_start:y_end, x_start:x_end] = undo_state['master']
|
|
63
73
|
undo_desc = self.undo_stack[-1]['description'] if self.undo_stack else ""
|
|
64
74
|
redo_desc = redo_state['description']
|
|
65
75
|
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|
|
@@ -76,7 +86,16 @@ class UndoManager(QObject):
|
|
|
76
86
|
'description': redo_state['description']
|
|
77
87
|
}
|
|
78
88
|
self.undo_stack.append(undo_state)
|
|
79
|
-
|
|
89
|
+
descr = undo_state['description']
|
|
90
|
+
if descr.startswith(gui_constants.ROTATE_LABEL):
|
|
91
|
+
if descr == gui_constants.ROTATE_90_CW_LABEL:
|
|
92
|
+
self.transformation_manager.rotate_90_cw(False)
|
|
93
|
+
elif descr == gui_constants.ROTATE_90_CCW_LABEL:
|
|
94
|
+
self.transformation_manager.rotate_90_ccw(False)
|
|
95
|
+
elif descr == gui_constants.ROTATE_180_LABEL:
|
|
96
|
+
self.transformation_manager.rotate_180(False)
|
|
97
|
+
else:
|
|
98
|
+
layer[y_start:y_end, x_start:x_end] = redo_state['master']
|
|
80
99
|
undo_desc = undo_state['description']
|
|
81
100
|
redo_desc = self.redo_stack[-1]['description'] if self.redo_stack else ""
|
|
82
101
|
self.stack_changed.emit(bool(self.undo_stack), undo_desc, bool(self.redo_stack), redo_desc)
|