nodeeditor-qtpy 0.8.1__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.
- nodeeditor/__init__.py +93 -0
- nodeeditor/node_colors_config.py +343 -0
- nodeeditor/node_content_widget.py +129 -0
- nodeeditor/node_decorators.py +65 -0
- nodeeditor/node_edge.py +518 -0
- nodeeditor/node_edge_dragging.py +185 -0
- nodeeditor/node_edge_intersect.py +219 -0
- nodeeditor/node_edge_rerouting.py +216 -0
- nodeeditor/node_edge_snapping.py +95 -0
- nodeeditor/node_edge_validators.py +82 -0
- nodeeditor/node_editor_widget.py +257 -0
- nodeeditor/node_editor_window.py +448 -0
- nodeeditor/node_graph.py +583 -0
- nodeeditor/node_graphics_cutline.py +57 -0
- nodeeditor/node_graphics_edge.py +375 -0
- nodeeditor/node_graphics_edge_path.py +226 -0
- nodeeditor/node_graphics_node.py +693 -0
- nodeeditor/node_graphics_scene.py +369 -0
- nodeeditor/node_graphics_socket.py +262 -0
- nodeeditor/node_graphics_view.py +907 -0
- nodeeditor/node_group.py +1286 -0
- nodeeditor/node_group_node.py +5 -0
- nodeeditor/node_icon_content_widget.py +176 -0
- nodeeditor/node_icon_graphics_node.py +524 -0
- nodeeditor/node_multi_input_node.py +450 -0
- nodeeditor/node_node.py +848 -0
- nodeeditor/node_scene.py +1106 -0
- nodeeditor/node_scene_clipboard.py +314 -0
- nodeeditor/node_scene_history.py +556 -0
- nodeeditor/node_serializable.py +43 -0
- nodeeditor/node_socket.py +445 -0
- nodeeditor/node_socket_shapes.py +261 -0
- nodeeditor/py.typed +0 -0
- nodeeditor/qss/nodestyle.qss +3 -0
- nodeeditor/utils.py +108 -0
- nodeeditor/utils_no_qt.py +21 -0
- nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
- nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
- nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
- nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing `Graphics View` for NodeEditor
|
|
4
|
+
"""
|
|
5
|
+
from qtpy.QtWidgets import QGraphicsView, QApplication, QWidget, QGraphicsItem
|
|
6
|
+
from qtpy.QtCore import Signal, QPoint, Qt, QEvent, QPointF, QRectF
|
|
7
|
+
from qtpy.QtGui import QPainter, QDragEnterEvent, QDropEvent, QMouseEvent, QKeyEvent, QWheelEvent, QInputEvent
|
|
8
|
+
|
|
9
|
+
from nodeeditor import _QT_API_NAME as QT_API
|
|
10
|
+
from nodeeditor.node_graphics_socket import QDMGraphicsSocket
|
|
11
|
+
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
|
|
12
|
+
from nodeeditor.node_edge_dragging import EdgeDragging
|
|
13
|
+
from nodeeditor.node_edge_rerouting import EdgeRerouting
|
|
14
|
+
from nodeeditor.node_edge_intersect import EdgeIntersect
|
|
15
|
+
from nodeeditor.node_edge_snapping import EdgeSnapping
|
|
16
|
+
from nodeeditor.node_graphics_cutline import QDMCutLine
|
|
17
|
+
from nodeeditor.utils import dumpException, pp, isCTRLPressed, isSHIFTPressed, isALTPressed
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from nodeeditor.node_node import Node
|
|
23
|
+
from nodeeditor.node_graphics_scene import QDMGraphicsScene
|
|
24
|
+
|
|
25
|
+
MODE_NOOP = 1 #: Mode representing ready state
|
|
26
|
+
MODE_EDGE_DRAG = 2 #: Mode representing when we drag edge state
|
|
27
|
+
MODE_EDGE_CUT = 3 #: Mode representing when we draw a cutting edge
|
|
28
|
+
MODE_EDGES_REROUTING = 4 #: Mode representing when we re-route existing edges
|
|
29
|
+
#: Mode representing when we drag a node to calculate dropping on intersecting edge
|
|
30
|
+
MODE_NODE_DRAG = 5
|
|
31
|
+
|
|
32
|
+
STATE_STRING = ['', 'Noop', 'Edge Drag',
|
|
33
|
+
'Edge Cut', 'Edge Rerouting', 'Node Drag']
|
|
34
|
+
|
|
35
|
+
#: Distance when click on socket to enable `Drag Edge`
|
|
36
|
+
EDGE_DRAG_START_THRESHOLD = 50
|
|
37
|
+
|
|
38
|
+
#: Enable UnrealEngine style rerouting
|
|
39
|
+
EDGE_REROUTING_UE = True
|
|
40
|
+
|
|
41
|
+
#: Socket snapping distance
|
|
42
|
+
EDGE_SNAPPING_RADIUS = 24
|
|
43
|
+
#: Enable socket snapping feature
|
|
44
|
+
EDGE_SNAPPING = True
|
|
45
|
+
|
|
46
|
+
#: How the view invalidates its viewport after a change.
|
|
47
|
+
#:
|
|
48
|
+
#: ``FullViewportUpdate`` repaints every visible item on every change, and the
|
|
49
|
+
#: old mouse-move path forced one on *every* move (clearing socket highlights).
|
|
50
|
+
#: ``BoundingRectViewportUpdate`` repaints the union of the rectangles that
|
|
51
|
+
#: actually changed, so dragging a node at a zoomed-out fit costs a third of the
|
|
52
|
+
#: edge paints FullViewportUpdate does (36 vs 119 per pass at 120 nodes) and
|
|
53
|
+
#: measures 2.2x faster per step; panning and repainting are within noise of
|
|
54
|
+
#: ``MinimalViewportUpdate``. It is the wider of the two cheap modes on purpose:
|
|
55
|
+
#: every node's look is an embedded ``QGraphicsProxyWidget``, and the narrow
|
|
56
|
+
#: mode's region bookkeeping is what Qt documents as the one that can leave
|
|
57
|
+
#: widget trails across a scroll.
|
|
58
|
+
DEFAULT_VIEWPORT_UPDATE_MODE = (
|
|
59
|
+
QGraphicsView.ViewportUpdateMode.BoundingRectViewportUpdate
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
DEBUG = False
|
|
63
|
+
DEBUG_MMB_SCENE_ITEMS = False
|
|
64
|
+
DEBUG_MMB_LAST_SELECTIONS = False
|
|
65
|
+
DEBUG_EDGE_INTERSECT = False
|
|
66
|
+
DEBUG_STATE = False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class QDMGraphicsView(QGraphicsView):
|
|
70
|
+
"""Class representing NodeEditor's `Graphics View`"""
|
|
71
|
+
#: pyqtSignal emitted when cursor position on the `Scene` has changed
|
|
72
|
+
scenePosChanged = Signal(int, int)
|
|
73
|
+
|
|
74
|
+
def __init__(self, grScene: 'QDMGraphicsScene', parent: QWidget = None) -> None:
|
|
75
|
+
"""
|
|
76
|
+
:param grScene: reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
|
|
77
|
+
:type grScene: :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
|
|
78
|
+
:param parent: parent widget
|
|
79
|
+
:type parent: ``QWidget``
|
|
80
|
+
|
|
81
|
+
:Instance Attributes:
|
|
82
|
+
|
|
83
|
+
- **grScene** - reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
|
|
84
|
+
- **mode** - state of the `Graphics View`
|
|
85
|
+
- **zoomInFactor**- ``float`` - zoom step scaling, default 1.25
|
|
86
|
+
- **zoomClamp** - ``bool`` - do we clamp zooming or is it infinite?
|
|
87
|
+
- **zoom** - current zoom step
|
|
88
|
+
- **zoomStep** - ``int`` - the relative zoom step when zooming in/out
|
|
89
|
+
- **zoomRange** - ``[min, max]``
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(parent)
|
|
93
|
+
self.grScene = grScene
|
|
94
|
+
|
|
95
|
+
self.initUI()
|
|
96
|
+
|
|
97
|
+
self.setScene(self.grScene)
|
|
98
|
+
|
|
99
|
+
self.mode = MODE_NOOP
|
|
100
|
+
self.editingFlag = False
|
|
101
|
+
self.rubberBandDraggingRectangle = False
|
|
102
|
+
|
|
103
|
+
#: The socket currently drawn highlighted while CTRL-snapping, if any.
|
|
104
|
+
#: Tracked so a mouse-move can clear the previous one instead of
|
|
105
|
+
#: scanning a 248-unit square of the scene and repainting the viewport.
|
|
106
|
+
self._highlighted_socket: QDMGraphicsSocket | None = None
|
|
107
|
+
|
|
108
|
+
# edge dragging
|
|
109
|
+
self.dragging = EdgeDragging(self)
|
|
110
|
+
|
|
111
|
+
# edges re-routing
|
|
112
|
+
self.rerouting = EdgeRerouting(self)
|
|
113
|
+
|
|
114
|
+
# drop a node on an existing edge
|
|
115
|
+
self.edgeIntersect = EdgeIntersect(self)
|
|
116
|
+
|
|
117
|
+
# edge snapping
|
|
118
|
+
self.snapping = EdgeSnapping(
|
|
119
|
+
self, snapping_radius=EDGE_SNAPPING_RADIUS)
|
|
120
|
+
|
|
121
|
+
# cutline
|
|
122
|
+
self.cutline = QDMCutLine()
|
|
123
|
+
self.grScene.addItem(self.cutline)
|
|
124
|
+
|
|
125
|
+
self.last_scene_mouse_position = QPointF(0, 0)
|
|
126
|
+
self.zoomInFactor = 1.25
|
|
127
|
+
self.zoomClamp = True
|
|
128
|
+
self.zoom = 10
|
|
129
|
+
self.zoomStep = 1
|
|
130
|
+
self.zoomRange = [0, 10]
|
|
131
|
+
|
|
132
|
+
# listeners
|
|
133
|
+
self._drag_enter_listeners: list = []
|
|
134
|
+
self._drop_listeners: list = []
|
|
135
|
+
|
|
136
|
+
def initUI(self) -> None:
|
|
137
|
+
"""Set up this ``QGraphicsView``"""
|
|
138
|
+
# self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
|
|
139
|
+
self.setRenderHints(
|
|
140
|
+
QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
|
|
141
|
+
|
|
142
|
+
self.setViewportUpdateMode(DEFAULT_VIEWPORT_UPDATE_MODE)
|
|
143
|
+
|
|
144
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
145
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
146
|
+
|
|
147
|
+
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
|
148
|
+
self.setDragMode(QGraphicsView.RubberBandDrag)
|
|
149
|
+
|
|
150
|
+
# enable dropping
|
|
151
|
+
self.setAcceptDrops(True)
|
|
152
|
+
|
|
153
|
+
def isSnappingEnabled(self, event: 'QInputEvent' = None) -> bool:
|
|
154
|
+
"""Returns ``True`` if snapping is currently enabled"""
|
|
155
|
+
return EDGE_SNAPPING and isCTRLPressed(event) if event else True
|
|
156
|
+
|
|
157
|
+
def resetMode(self) -> None:
|
|
158
|
+
"""Helper function to re-set the grView's State Machine state to the default"""
|
|
159
|
+
self.mode = MODE_NOOP
|
|
160
|
+
|
|
161
|
+
def dragEnterEvent(self, event: Optional[QDragEnterEvent]) -> None:
|
|
162
|
+
"""Trigger our registered `Drag Enter` events"""
|
|
163
|
+
for callback in self._drag_enter_listeners:
|
|
164
|
+
callback(event)
|
|
165
|
+
|
|
166
|
+
def dropEvent(self, event: Optional[QDropEvent]) -> None:
|
|
167
|
+
"""Trigger our registered `Drop` events"""
|
|
168
|
+
for callback in self._drop_listeners:
|
|
169
|
+
callback(event)
|
|
170
|
+
|
|
171
|
+
def addDragEnterListener(self, callback: 'function') -> None:
|
|
172
|
+
"""
|
|
173
|
+
Register callback for `Drag Enter` event
|
|
174
|
+
|
|
175
|
+
:param callback: callback function
|
|
176
|
+
"""
|
|
177
|
+
self._drag_enter_listeners.append(callback)
|
|
178
|
+
|
|
179
|
+
def addDropListener(self, callback: 'function') -> None:
|
|
180
|
+
"""
|
|
181
|
+
Register callback for `Drop` event
|
|
182
|
+
|
|
183
|
+
:param callback: callback function
|
|
184
|
+
"""
|
|
185
|
+
self._drop_listeners.append(callback)
|
|
186
|
+
|
|
187
|
+
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
188
|
+
"""Dispatch Qt's mousePress event to corresponding function below"""
|
|
189
|
+
if event is None:
|
|
190
|
+
return
|
|
191
|
+
if event.button() == Qt.MouseButton.MiddleButton:
|
|
192
|
+
self.middleMouseButtonPress(event)
|
|
193
|
+
elif event.button() == Qt.MouseButton.LeftButton:
|
|
194
|
+
self.leftMouseButtonPress(event)
|
|
195
|
+
elif event.button() == Qt.MouseButton.RightButton:
|
|
196
|
+
self.rightMouseButtonPress(event)
|
|
197
|
+
else:
|
|
198
|
+
super().mousePressEvent(event)
|
|
199
|
+
|
|
200
|
+
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
201
|
+
"""Dispatch Qt's mouseRelease event to corresponding function below"""
|
|
202
|
+
if event is None:
|
|
203
|
+
return
|
|
204
|
+
if self.mode == MODE_EDGE_DRAG:
|
|
205
|
+
item = self.getItemAtClick(event)
|
|
206
|
+
if item is None: # Clicked on empty space
|
|
207
|
+
self.dragging.cancelDragEdge()
|
|
208
|
+
return
|
|
209
|
+
if event.button() == Qt.MouseButton.MiddleButton:
|
|
210
|
+
self.middleMouseButtonRelease(event)
|
|
211
|
+
elif event.button() == Qt.MouseButton.LeftButton:
|
|
212
|
+
self.leftMouseButtonRelease(event)
|
|
213
|
+
elif event.button() == Qt.MouseButton.RightButton:
|
|
214
|
+
self.rightMouseButtonRelease(event)
|
|
215
|
+
else:
|
|
216
|
+
super().mouseReleaseEvent(event)
|
|
217
|
+
|
|
218
|
+
def middleMouseButtonPress(self, event: QMouseEvent) -> None:
|
|
219
|
+
"""When Middle mouse button was pressed"""
|
|
220
|
+
|
|
221
|
+
item = self.getItemAtClick(event)
|
|
222
|
+
|
|
223
|
+
# debug printout
|
|
224
|
+
if DEBUG_MMB_SCENE_ITEMS:
|
|
225
|
+
if isinstance(item, QDMGraphicsEdge):
|
|
226
|
+
print("MMB DEBUG:", item.edge, "\n\t",
|
|
227
|
+
item.edge.grEdge if item.edge.grEdge is not None else None)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if isinstance(item, QDMGraphicsSocket):
|
|
231
|
+
print("MMB DEBUG:", item.socket, "socket_type:", item.socket.socket_type,
|
|
232
|
+
"has edges:", "no" if item.socket.edges == [] else "")
|
|
233
|
+
if item.socket.edges:
|
|
234
|
+
for edge in item.socket.edges:
|
|
235
|
+
print("\t", edge)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if DEBUG_MMB_SCENE_ITEMS and (item is None or self.mode == MODE_EDGES_REROUTING):
|
|
239
|
+
print("SCENE:")
|
|
240
|
+
print(" Nodes:")
|
|
241
|
+
for node in self.grScene.scene.nodes:
|
|
242
|
+
print("\t", node)
|
|
243
|
+
print(" Edges:")
|
|
244
|
+
for edge in self.grScene.scene.edges:
|
|
245
|
+
print("\t", edge, "\n\t\tgrEdge:",
|
|
246
|
+
edge.grEdge if edge.grEdge is not None else None)
|
|
247
|
+
|
|
248
|
+
if isCTRLPressed(event):
|
|
249
|
+
print(" Graphic Items in GraphicScene:")
|
|
250
|
+
for item in self.grScene.items():
|
|
251
|
+
print(' ', item)
|
|
252
|
+
|
|
253
|
+
if DEBUG_MMB_LAST_SELECTIONS and isSHIFTPressed(event):
|
|
254
|
+
print("scene _last_selected_items:",
|
|
255
|
+
self.grScene.scene._last_selected_items)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# faking events for enable MMB dragging the scene
|
|
259
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
260
|
+
releaseEvent = QMouseEvent(
|
|
261
|
+
QEvent.Type.MouseButtonRelease,
|
|
262
|
+
event.localPos(), # type: ignore
|
|
263
|
+
event.screenPos(), # type: ignore
|
|
264
|
+
Qt.MouseButton.LeftButton,
|
|
265
|
+
Qt.MouseButton.NoButton,
|
|
266
|
+
event.modifiers())
|
|
267
|
+
|
|
268
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
269
|
+
releaseEvent = QMouseEvent(
|
|
270
|
+
QEvent.Type.MouseButtonRelease,
|
|
271
|
+
# ----------------- Change it to localPos if dont work.----------------------
|
|
272
|
+
event.position(),
|
|
273
|
+
# ---------------------------------------
|
|
274
|
+
Qt.MouseButton.LeftButton,
|
|
275
|
+
Qt.MouseButton.NoButton,
|
|
276
|
+
event.modifiers())
|
|
277
|
+
|
|
278
|
+
super().mouseReleaseEvent(releaseEvent)
|
|
279
|
+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
280
|
+
|
|
281
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
282
|
+
fakeEvent = QMouseEvent(
|
|
283
|
+
event.type(),
|
|
284
|
+
event.localPos(), # type: ignore
|
|
285
|
+
event.screenPos(), # type: ignore
|
|
286
|
+
Qt.MouseButton.LeftButton,
|
|
287
|
+
event.buttons() | Qt.MouseButton.LeftButton,
|
|
288
|
+
event.modifiers())
|
|
289
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
290
|
+
fakeEvent = QMouseEvent(
|
|
291
|
+
event.type(),
|
|
292
|
+
event.position(),
|
|
293
|
+
Qt.MouseButton.LeftButton,
|
|
294
|
+
event.buttons() | Qt.MouseButton.LeftButton,
|
|
295
|
+
event.modifiers())
|
|
296
|
+
super().mousePressEvent(fakeEvent)
|
|
297
|
+
|
|
298
|
+
def middleMouseButtonRelease(self, event: QMouseEvent) -> None:
|
|
299
|
+
"""When Middle mouse button was released"""
|
|
300
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
301
|
+
fakeEvent = QMouseEvent(
|
|
302
|
+
event.type(),
|
|
303
|
+
event.localPos(), # type: ignore
|
|
304
|
+
event.screenPos(), # type: ignore
|
|
305
|
+
Qt.MouseButton.LeftButton,
|
|
306
|
+
event.buttons() & ~Qt.MouseButton.LeftButton,
|
|
307
|
+
event.modifiers())
|
|
308
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
309
|
+
fakeEvent = QMouseEvent(
|
|
310
|
+
event.type(),
|
|
311
|
+
event.position(),
|
|
312
|
+
Qt.MouseButton.LeftButton,
|
|
313
|
+
event.buttons() & ~Qt.MouseButton.LeftButton,
|
|
314
|
+
event.modifiers())
|
|
315
|
+
super().mouseReleaseEvent(fakeEvent)
|
|
316
|
+
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
317
|
+
|
|
318
|
+
def leftMouseButtonPress(self, event: QMouseEvent) -> None:
|
|
319
|
+
"""When Left mouse button was pressed"""
|
|
320
|
+
|
|
321
|
+
# get the item we clicked on
|
|
322
|
+
item = self.getItemAtClick(event)
|
|
323
|
+
|
|
324
|
+
if item is None:
|
|
325
|
+
if isCTRLPressed(event):
|
|
326
|
+
self.mode = MODE_EDGE_CUT
|
|
327
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
328
|
+
fakeEvent = QMouseEvent(
|
|
329
|
+
QEvent.Type.MouseButtonRelease,
|
|
330
|
+
event.localPos(), # type: ignore
|
|
331
|
+
event.screenPos(), # type: ignore
|
|
332
|
+
Qt.MouseButton.LeftButton,
|
|
333
|
+
Qt.MouseButton.NoButton,
|
|
334
|
+
event.modifiers())
|
|
335
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
336
|
+
fakeEvent = QMouseEvent(
|
|
337
|
+
QEvent.Type.MouseButtonRelease,
|
|
338
|
+
event.position(),
|
|
339
|
+
Qt.MouseButton.LeftButton,
|
|
340
|
+
Qt.MouseButton.NoButton,
|
|
341
|
+
event.modifiers())
|
|
342
|
+
super().mouseReleaseEvent(fakeEvent)
|
|
343
|
+
QApplication.setOverrideCursor(Qt.CursorShape.CrossCursor)
|
|
344
|
+
return
|
|
345
|
+
else:
|
|
346
|
+
self.rubberBandDraggingRectangle = True
|
|
347
|
+
|
|
348
|
+
# we store the position of last LMB click
|
|
349
|
+
self.last_lmb_click_scene_pos = self.mapToScene(event.pos())
|
|
350
|
+
|
|
351
|
+
# if DEBUG: print("LMB Click on", item, self.debug_modifiers(event))
|
|
352
|
+
|
|
353
|
+
# logic - Shift + LMB Node
|
|
354
|
+
if hasattr(item, "node") or isinstance(item, QDMGraphicsEdge) or item is None:
|
|
355
|
+
if isSHIFTPressed(event):
|
|
356
|
+
event.ignore()
|
|
357
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
358
|
+
fakeEvent = QMouseEvent(
|
|
359
|
+
QEvent.Type.MouseButtonPress,
|
|
360
|
+
event.localPos(), # type: ignore
|
|
361
|
+
event.screenPos(), # type: ignore
|
|
362
|
+
Qt.MouseButton.LeftButton,
|
|
363
|
+
event.buttons() | Qt.MouseButton.LeftButton,
|
|
364
|
+
event.modifiers() | Qt.KeyboardModifier.ControlModifier)
|
|
365
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
366
|
+
fakeEvent = QMouseEvent(
|
|
367
|
+
QEvent.Type.MouseButtonPress,
|
|
368
|
+
event.position(),
|
|
369
|
+
Qt.MouseButton.LeftButton,
|
|
370
|
+
event.buttons() | Qt.MouseButton.LeftButton,
|
|
371
|
+
event.modifiers() | Qt.KeyboardModifier.ControlModifier)
|
|
372
|
+
super().mousePressEvent(fakeEvent)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
if hasattr(item, "node"):
|
|
376
|
+
if DEBUG_EDGE_INTERSECT:
|
|
377
|
+
print('View::leftMouseButtonPress - Start dragging a node')
|
|
378
|
+
if self.mode == MODE_NOOP:
|
|
379
|
+
self.mode = MODE_NODE_DRAG
|
|
380
|
+
self.edgeIntersect.enterState(item.node)
|
|
381
|
+
if DEBUG_EDGE_INTERSECT:
|
|
382
|
+
print(">> edgeIntersect start:",
|
|
383
|
+
self.edgeIntersect.draggedNode)
|
|
384
|
+
|
|
385
|
+
# support for snapping
|
|
386
|
+
if self.isSnappingEnabled(event):
|
|
387
|
+
item = self.snapping.getSnappedSocketItem(event)
|
|
388
|
+
|
|
389
|
+
if isinstance(item, QDMGraphicsSocket):
|
|
390
|
+
|
|
391
|
+
if isALTPressed(event) and self.mode == MODE_NOOP:
|
|
392
|
+
# ALT + LMB on a socket to see the data processed by that node.
|
|
393
|
+
socket = item.socket
|
|
394
|
+
node = socket.node
|
|
395
|
+
# Emit signal with both socket and node
|
|
396
|
+
self.grScene.socketClicked.emit(socket, node)
|
|
397
|
+
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
if self.mode == MODE_NOOP and isCTRLPressed(event):
|
|
401
|
+
socket = item.socket
|
|
402
|
+
if socket.hasAnyEdge():
|
|
403
|
+
self.mode = MODE_EDGES_REROUTING
|
|
404
|
+
self.rerouting.startRerouting(socket)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
if self.mode == MODE_NOOP:
|
|
408
|
+
self.mode = MODE_EDGE_DRAG
|
|
409
|
+
self.dragging.edgeDragStart(item)
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
if self.mode == MODE_EDGE_DRAG and item is not None:
|
|
413
|
+
res = self.dragging.edgeDragEnd(item)
|
|
414
|
+
if res:
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
super().mousePressEvent(event)
|
|
418
|
+
|
|
419
|
+
def leftMouseButtonRelease(self, event: QMouseEvent) -> None:
|
|
420
|
+
"""When Left mouse button was released"""
|
|
421
|
+
|
|
422
|
+
# get the item on which we release the mouse button on
|
|
423
|
+
item = self.getItemAtClick(event)
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
# logic - Shift + LMB release (add selection)
|
|
427
|
+
if hasattr(item, "node") or isinstance(item, QDMGraphicsEdge) or item is None:
|
|
428
|
+
if isSHIFTPressed(event):
|
|
429
|
+
event.ignore()
|
|
430
|
+
if QT_API in ("pyqt5", "pyside2"):
|
|
431
|
+
fakeEvent = QMouseEvent(
|
|
432
|
+
event.type(),
|
|
433
|
+
event.localPos(), # type: ignore
|
|
434
|
+
event.screenPos(), # type: ignore
|
|
435
|
+
Qt.LeftButton, # type: ignore
|
|
436
|
+
Qt.NoButton, # type: ignore
|
|
437
|
+
event.modifiers() | Qt.ControlModifier # type: ignore
|
|
438
|
+
)
|
|
439
|
+
elif QT_API in ("pyqt6", "pyside6"):
|
|
440
|
+
fakeEvent = QMouseEvent(
|
|
441
|
+
event.type(),
|
|
442
|
+
event.position(),
|
|
443
|
+
Qt.MouseButton.LeftButton,
|
|
444
|
+
Qt.MouseButton.NoButton,
|
|
445
|
+
event.modifiers() | Qt.KeyboardModifier.ControlModifier)
|
|
446
|
+
super().mouseReleaseEvent(fakeEvent)
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
if self.mode == MODE_EDGE_DRAG:
|
|
450
|
+
if self.distanceBetweenClickAndReleaseIsOff(event):
|
|
451
|
+
if self.isSnappingEnabled(event):
|
|
452
|
+
item = self.snapping.getSnappedSocketItem(event)
|
|
453
|
+
if item is not None:
|
|
454
|
+
res = self.dragging.edgeDragEnd(item)
|
|
455
|
+
if res:
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
if self.mode == MODE_EDGES_REROUTING:
|
|
459
|
+
if self.isSnappingEnabled(event):
|
|
460
|
+
item = self.snapping.getSnappedSocketItem(event)
|
|
461
|
+
|
|
462
|
+
if not EDGE_REROUTING_UE:
|
|
463
|
+
# version 2 -- more consistent with the nodeeditor?
|
|
464
|
+
if not self.rerouting.first_mb_release:
|
|
465
|
+
# for confirmation of first MB release
|
|
466
|
+
self.rerouting.first_mb_release = True
|
|
467
|
+
# skip any re-routing until first MB was released
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
self.rerouting.stopRerouting(
|
|
471
|
+
item.socket if isinstance(item, QDMGraphicsSocket) else None)
|
|
472
|
+
|
|
473
|
+
# don't forget to end the REROUTING MODE
|
|
474
|
+
|
|
475
|
+
self.mode = MODE_NOOP
|
|
476
|
+
|
|
477
|
+
if self.mode == MODE_EDGE_CUT:
|
|
478
|
+
self.cutIntersectingEdges()
|
|
479
|
+
self.cutline.line_points = []
|
|
480
|
+
self.cutline.update()
|
|
481
|
+
QApplication.setOverrideCursor(Qt.CursorShape.ArrowCursor)
|
|
482
|
+
self.mode = MODE_NOOP
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
if self.mode == MODE_NODE_DRAG:
|
|
486
|
+
scenepos = self.mapToScene(event.pos())
|
|
487
|
+
self.edgeIntersect.leaveState(scenepos.x(), scenepos.y())
|
|
488
|
+
try:
|
|
489
|
+
self._dropNodesIntoGroupAt(scenepos)
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
try:
|
|
493
|
+
self._clearGroupDropHighlight()
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
self.mode = MODE_NOOP
|
|
497
|
+
self.update()
|
|
498
|
+
|
|
499
|
+
if self.rubberBandDraggingRectangle:
|
|
500
|
+
self.rubberBandDraggingRectangle = False
|
|
501
|
+
current_selected_items = self.grScene.selectedItems()
|
|
502
|
+
|
|
503
|
+
if current_selected_items != self.grScene.scene._last_selected_items:
|
|
504
|
+
if current_selected_items == []:
|
|
505
|
+
self.grScene.itemsDeselected.emit()
|
|
506
|
+
else:
|
|
507
|
+
self.grScene.itemSelected.emit()
|
|
508
|
+
self.grScene.scene._last_selected_items = current_selected_items
|
|
509
|
+
|
|
510
|
+
# the rubber band rectangle doesn't disappear without handling the event
|
|
511
|
+
super().mouseReleaseEvent(event)
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
# otherwise deselect everything
|
|
515
|
+
if item is None:
|
|
516
|
+
self.grScene.itemsDeselected.emit()
|
|
517
|
+
|
|
518
|
+
except:
|
|
519
|
+
dumpException()
|
|
520
|
+
|
|
521
|
+
super().mouseReleaseEvent(event)
|
|
522
|
+
|
|
523
|
+
def rightMouseButtonPress(self, event: QMouseEvent) -> None:
|
|
524
|
+
"""When Right mouse button was pressed"""
|
|
525
|
+
super().mousePressEvent(event)
|
|
526
|
+
|
|
527
|
+
def rightMouseButtonRelease(self, event: QMouseEvent) -> None:
|
|
528
|
+
"""When Right mouse button was release"""
|
|
529
|
+
|
|
530
|
+
# cannot be because with dragging RMB we spawn Create New Node Context Menu
|
|
531
|
+
# However, you could use this if you want to cancel with RMB
|
|
532
|
+
# if self.mode == MODE_EDGE_DRAG:
|
|
533
|
+
# self.dragging.edgeDragEnd(None)
|
|
534
|
+
# return
|
|
535
|
+
|
|
536
|
+
super().mouseReleaseEvent(event)
|
|
537
|
+
|
|
538
|
+
def mouseMoveEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
539
|
+
"""Overriden Qt's ``mouseMoveEvent`` handling Scene/View logic"""
|
|
540
|
+
|
|
541
|
+
if event is None:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
scenepos: QPointF = self.mapToScene(event.pos())
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
# Hover highlight is a CTRL-snapping affordance only. The old code
|
|
548
|
+
# scanned a 248x248-unit square for sockets on every single move
|
|
549
|
+
# and, when it found any, repainted the whole viewport - which is
|
|
550
|
+
# what made panning a large graph stutter. Track the one socket
|
|
551
|
+
# that can be highlighted and repaint just that item.
|
|
552
|
+
if self.isSnappingEnabled(event):
|
|
553
|
+
gr_socket, scenepos = self.snapping.getSnappedToSocketPosition(
|
|
554
|
+
scenepos)
|
|
555
|
+
self._setHighlightedSocket(
|
|
556
|
+
gr_socket if isinstance(gr_socket, QDMGraphicsSocket) else None)
|
|
557
|
+
else:
|
|
558
|
+
self._setHighlightedSocket(None)
|
|
559
|
+
|
|
560
|
+
if self.mode == MODE_EDGE_DRAG:
|
|
561
|
+
self.dragging.updateDestination(scenepos.x(), scenepos.y())
|
|
562
|
+
|
|
563
|
+
if self.mode == MODE_NODE_DRAG:
|
|
564
|
+
self.edgeIntersect.update(scenepos.x(), scenepos.y())
|
|
565
|
+
try:
|
|
566
|
+
self._updateGroupDropHighlight(scenepos)
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
if self.mode == MODE_EDGES_REROUTING:
|
|
571
|
+
self.rerouting.updateScenePos(scenepos.x(), scenepos.y())
|
|
572
|
+
|
|
573
|
+
if self.mode == MODE_EDGE_CUT and self.cutline is not None:
|
|
574
|
+
self.cutline.line_points.append(scenepos)
|
|
575
|
+
self.cutline.update()
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
dumpException()
|
|
579
|
+
|
|
580
|
+
self.last_scene_mouse_position = scenepos
|
|
581
|
+
|
|
582
|
+
self.scenePosChanged.emit(int(scenepos.x()), int(scenepos.y()))
|
|
583
|
+
|
|
584
|
+
super().mouseMoveEvent(event)
|
|
585
|
+
|
|
586
|
+
def keyPressEvent(self, event: Optional[QKeyEvent]) -> None:
|
|
587
|
+
"""
|
|
588
|
+
.. note::
|
|
589
|
+
This overridden Qt's method was used for handling key shortcuts, before we implemented proper
|
|
590
|
+
``QWindow`` with Actions and Menu. Still the commented code serves as an example on how to handle
|
|
591
|
+
key presses without Qt's framework for Actions and shortcuts. There is also an example on
|
|
592
|
+
how to solve the problem when a Node contains Text/LineEdit and we press the `Delete`
|
|
593
|
+
key (also serving to delete `Node`)
|
|
594
|
+
|
|
595
|
+
:param event: Qt's Key event
|
|
596
|
+
:type event: ``QKeyEvent``
|
|
597
|
+
:return:
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
if event.key() == Qt.Key.Key_Escape:
|
|
601
|
+
if self.mode == MODE_EDGE_DRAG:
|
|
602
|
+
self.dragging.cancelDragEdge()
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
super().keyPressEvent(event)
|
|
606
|
+
|
|
607
|
+
def cutIntersectingEdges(self) -> None:
|
|
608
|
+
"""Compare which `Edges` intersect with current `Cut line` and delete them safely"""
|
|
609
|
+
for ix in range(len(self.cutline.line_points) - 1):
|
|
610
|
+
p1 = self.cutline.line_points[ix]
|
|
611
|
+
p2 = self.cutline.line_points[ix + 1]
|
|
612
|
+
|
|
613
|
+
# @TODO: we could collect all touched nodes, and notify them once after all edges removed
|
|
614
|
+
# we could cut 3 edges leading to a single nodeeditor this will notify it 3x
|
|
615
|
+
# maybe we could use some Notifier class with methods collect() and dispatch()
|
|
616
|
+
for edge in self.grScene.scene.edges.copy():
|
|
617
|
+
try:
|
|
618
|
+
gr = getattr(edge, 'grEdge', None)
|
|
619
|
+
if gr is None:
|
|
620
|
+
continue
|
|
621
|
+
# never cut hidden internal edges of collapsed groups
|
|
622
|
+
try:
|
|
623
|
+
if not gr.isVisible():
|
|
624
|
+
continue
|
|
625
|
+
except Exception:
|
|
626
|
+
pass
|
|
627
|
+
if gr.intersectsWith(p1, p2):
|
|
628
|
+
edge.remove()
|
|
629
|
+
except Exception:
|
|
630
|
+
continue
|
|
631
|
+
self.grScene.scene.history.storeHistory(
|
|
632
|
+
"Delete cutted edges", setModified=True)
|
|
633
|
+
|
|
634
|
+
def _setHighlightedSocket(self, gr_socket: Optional['QDMGraphicsSocket']) -> None:
|
|
635
|
+
"""Highlight at most one socket, repainting only the two it touches.
|
|
636
|
+
|
|
637
|
+
``getSnappedToSocketPosition`` sets ``isHighlighted`` on the socket it
|
|
638
|
+
finds; this method keeps that flag in step with what was highlighted
|
|
639
|
+
last move, so removing the highlight is one repaint of one small item
|
|
640
|
+
rather than a full-viewport redraw.
|
|
641
|
+
"""
|
|
642
|
+
if gr_socket is self._highlighted_socket:
|
|
643
|
+
return
|
|
644
|
+
|
|
645
|
+
previous = self._highlighted_socket
|
|
646
|
+
self._highlighted_socket = gr_socket
|
|
647
|
+
|
|
648
|
+
if previous is not None:
|
|
649
|
+
try:
|
|
650
|
+
previous.isHighlighted = False
|
|
651
|
+
previous.update()
|
|
652
|
+
except RuntimeError:
|
|
653
|
+
pass # C++ side already gone
|
|
654
|
+
|
|
655
|
+
if gr_socket is not None:
|
|
656
|
+
gr_socket.isHighlighted = True
|
|
657
|
+
gr_socket.update()
|
|
658
|
+
|
|
659
|
+
def setSocketHighlights(self, scenepos: QPointF, highlighted: bool = True, radius: float = 50):
|
|
660
|
+
"""Set/disable socket highlights in Scene area defined by `scenepos` and `radius`"""
|
|
661
|
+
scanrect = QRectF(
|
|
662
|
+
scenepos.x() - radius,
|
|
663
|
+
scenepos.y() - radius,
|
|
664
|
+
radius * 2,
|
|
665
|
+
radius * 2
|
|
666
|
+
)
|
|
667
|
+
# Get all items and filter for sockets
|
|
668
|
+
scene_items = self.grScene.items(scanrect)
|
|
669
|
+
socket_items: List[QDMGraphicsSocket] = [
|
|
670
|
+
item for item in scene_items if isinstance(item, QDMGraphicsSocket)
|
|
671
|
+
]
|
|
672
|
+
|
|
673
|
+
# Set highlight state
|
|
674
|
+
for grSocket in socket_items:
|
|
675
|
+
grSocket.isHighlighted = highlighted
|
|
676
|
+
|
|
677
|
+
return socket_items
|
|
678
|
+
|
|
679
|
+
def _draggedNodes(self):
|
|
680
|
+
"""Nodes being dragged (selected .node items, fallback to edgeIntersect)."""
|
|
681
|
+
nodes = []
|
|
682
|
+
try:
|
|
683
|
+
for item in list(self.grScene.selectedItems()):
|
|
684
|
+
try:
|
|
685
|
+
n = getattr(item, 'node', None)
|
|
686
|
+
if n is not None and n not in nodes:
|
|
687
|
+
nodes.append(n)
|
|
688
|
+
except Exception:
|
|
689
|
+
continue
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
if not nodes:
|
|
693
|
+
try:
|
|
694
|
+
dn = getattr(getattr(self, 'edgeIntersect', None), 'draggedNode', None)
|
|
695
|
+
if dn is not None:
|
|
696
|
+
nodes = [dn]
|
|
697
|
+
except Exception:
|
|
698
|
+
pass
|
|
699
|
+
return nodes
|
|
700
|
+
|
|
701
|
+
def _clearGroupDropHighlight(self) -> None:
|
|
702
|
+
try:
|
|
703
|
+
scene = self.grScene.scene
|
|
704
|
+
for grp in list(getattr(scene, 'groups', [])):
|
|
705
|
+
try:
|
|
706
|
+
if getattr(grp, '_drop_highlight', False):
|
|
707
|
+
grp.setDropHighlight(False)
|
|
708
|
+
except Exception:
|
|
709
|
+
continue
|
|
710
|
+
except Exception:
|
|
711
|
+
pass
|
|
712
|
+
|
|
713
|
+
def _updateGroupDropHighlight(self, scenepos) -> None:
|
|
714
|
+
try:
|
|
715
|
+
scene = self.grScene.scene
|
|
716
|
+
nodes = self._draggedNodes()
|
|
717
|
+
if not nodes:
|
|
718
|
+
self._clearGroupDropHighlight()
|
|
719
|
+
return
|
|
720
|
+
try:
|
|
721
|
+
target = scene.findGroupForDrop(scenepos, exclude_nodes=nodes)
|
|
722
|
+
except Exception:
|
|
723
|
+
target = None
|
|
724
|
+
for grp in list(getattr(scene, 'groups', [])):
|
|
725
|
+
try:
|
|
726
|
+
grp.setDropHighlight(grp is target)
|
|
727
|
+
except Exception:
|
|
728
|
+
continue
|
|
729
|
+
except Exception:
|
|
730
|
+
pass
|
|
731
|
+
|
|
732
|
+
def _dropNodesIntoGroupAt(self, scenepos) -> bool:
|
|
733
|
+
try:
|
|
734
|
+
scene = self.grScene.scene
|
|
735
|
+
nodes = self._draggedNodes()
|
|
736
|
+
if not nodes:
|
|
737
|
+
return False
|
|
738
|
+
changed = scene.dropNodesIntoGroup(nodes, scenepos)
|
|
739
|
+
if changed:
|
|
740
|
+
try:
|
|
741
|
+
scene.history.storeHistory("Moved nodes into group", setModified=True)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
return changed
|
|
745
|
+
except Exception:
|
|
746
|
+
return False
|
|
747
|
+
|
|
748
|
+
def deleteSelected(self) -> None:
|
|
749
|
+
"""Delete selected items. Delete-key on a Group = delete Group + Children."""
|
|
750
|
+
try:
|
|
751
|
+
from nodeeditor.node_group import Group
|
|
752
|
+
except Exception:
|
|
753
|
+
Group = None # type: ignore
|
|
754
|
+
|
|
755
|
+
selected = list(self.grScene.selectedItems())
|
|
756
|
+
groups = [it for it in selected if Group is not None and isinstance(it, Group)]
|
|
757
|
+
# delete groups + children first
|
|
758
|
+
for grp in groups:
|
|
759
|
+
try:
|
|
760
|
+
grp.deleteWithChildren()
|
|
761
|
+
except Exception:
|
|
762
|
+
continue
|
|
763
|
+
# delete remaining nodes/edges that still exist (nodes from deleted groups are gone)
|
|
764
|
+
scene = self.grScene.scene
|
|
765
|
+
for item in selected:
|
|
766
|
+
try:
|
|
767
|
+
if Group is not None and isinstance(item, Group):
|
|
768
|
+
continue
|
|
769
|
+
if isinstance(item, QDMGraphicsEdge):
|
|
770
|
+
try:
|
|
771
|
+
if getattr(item, 'edge', None) in scene.edges:
|
|
772
|
+
item.edge.remove()
|
|
773
|
+
except Exception:
|
|
774
|
+
pass
|
|
775
|
+
elif hasattr(item, 'node'):
|
|
776
|
+
try:
|
|
777
|
+
if getattr(item, 'node', None) in scene.nodes:
|
|
778
|
+
item.node.remove()
|
|
779
|
+
except Exception:
|
|
780
|
+
pass
|
|
781
|
+
except Exception:
|
|
782
|
+
continue
|
|
783
|
+
self.grScene.scene.history.storeHistory(
|
|
784
|
+
"Delete selected", setModified=True)
|
|
785
|
+
|
|
786
|
+
def debug_modifiers(self, event):
|
|
787
|
+
"""Helper function get string if we hold Ctrl, Shift or Alt modifier keys"""
|
|
788
|
+
out = "MODS: "
|
|
789
|
+
if isSHIFTPressed(event):
|
|
790
|
+
out += "SHIFT "
|
|
791
|
+
if isCTRLPressed(event):
|
|
792
|
+
out += "CTRL "
|
|
793
|
+
if isALTPressed(event):
|
|
794
|
+
out += "ALT "
|
|
795
|
+
return out
|
|
796
|
+
|
|
797
|
+
def getItemAtClick(self, event: QMouseEvent) -> Optional['QGraphicsItem']:
|
|
798
|
+
"""Return the object on which we've clicked/release mouse button
|
|
799
|
+
|
|
800
|
+
:param event: Qt's mouse or key event
|
|
801
|
+
:type event: ``QEvent``
|
|
802
|
+
:return: ``QGraphicsItem`` which the mouse event happened or ``None``
|
|
803
|
+
"""
|
|
804
|
+
pos = event.pos()
|
|
805
|
+
obj = self.itemAt(pos)
|
|
806
|
+
return obj
|
|
807
|
+
|
|
808
|
+
def distanceBetweenClickAndReleaseIsOff(self, event: QMouseEvent) -> bool:
|
|
809
|
+
""" Measures if we are too far from the last Mouse button click scene position.
|
|
810
|
+
This is used for detection if we release too far after we clicked on a `Socket`
|
|
811
|
+
|
|
812
|
+
:param event: Qt's mouse event
|
|
813
|
+
:type event: ``QMouseEvent``
|
|
814
|
+
:return: ``True`` if we released too far from where we clicked before
|
|
815
|
+
"""
|
|
816
|
+
new_lmb_release_scene_pos = self.mapToScene(event.pos())
|
|
817
|
+
dist_scene = new_lmb_release_scene_pos - self.last_lmb_click_scene_pos
|
|
818
|
+
edge_drag_threshold_sq = EDGE_DRAG_START_THRESHOLD*EDGE_DRAG_START_THRESHOLD
|
|
819
|
+
return (dist_scene.x()*dist_scene.x() + dist_scene.y()*dist_scene.y()) > edge_drag_threshold_sq
|
|
820
|
+
|
|
821
|
+
def zoomIn(self):
|
|
822
|
+
"""Step the zoom level up and return the factor to scale the scene by.
|
|
823
|
+
|
|
824
|
+
:return: the zoom factor to apply
|
|
825
|
+
:rtype: ``float``
|
|
826
|
+
"""
|
|
827
|
+
# self.scale(self.zoomInFactor, self.zoomInFactor)
|
|
828
|
+
self.zoom += self.zoomStep
|
|
829
|
+
return self.zoomInFactor
|
|
830
|
+
|
|
831
|
+
def zoomOut(self):
|
|
832
|
+
"""Step the zoom level down and return the factor to scale the scene by.
|
|
833
|
+
|
|
834
|
+
:return: the zoom factor to apply
|
|
835
|
+
:rtype: ``float``
|
|
836
|
+
"""
|
|
837
|
+
# self.scale(1 / self.zoomInFactor, 1 / self.zoomInFactor)
|
|
838
|
+
self.zoom -= self.zoomStep
|
|
839
|
+
return 1 / self.zoomInFactor
|
|
840
|
+
|
|
841
|
+
def applyZoom(self, zoomFactor: float) -> None:
|
|
842
|
+
"""Scale the scene by ``zoomFactor``, keeping the zoom level inside the configured range.
|
|
843
|
+
|
|
844
|
+
:param zoomFactor: the factor to scale the scene by
|
|
845
|
+
:type zoomFactor: ``float``
|
|
846
|
+
"""
|
|
847
|
+
clamped = False
|
|
848
|
+
if self.zoom < self.zoomRange[0]:
|
|
849
|
+
self.zoom, clamped = self.zoomRange[0], True
|
|
850
|
+
if self.zoom > self.zoomRange[1]:
|
|
851
|
+
self.zoom, clamped = self.zoomRange[1], True
|
|
852
|
+
# set scene scale
|
|
853
|
+
if not clamped or self.zoomClamp is False:
|
|
854
|
+
self.scale(zoomFactor, zoomFactor)
|
|
855
|
+
self._syncDetailLod()
|
|
856
|
+
|
|
857
|
+
def _syncDetailLod(self) -> None:
|
|
858
|
+
"""Match the scene's item detail to the view's current zoom.
|
|
859
|
+
|
|
860
|
+
Called before the scene is painted, never from inside the paint pass:
|
|
861
|
+
hiding an item while Qt is painting leaves the pixels it already claimed
|
|
862
|
+
on screen, and those ghosts then drift with every scroll.
|
|
863
|
+
"""
|
|
864
|
+
gr_scene = self.scene()
|
|
865
|
+
updater = getattr(gr_scene, "updateDetailLod", None)
|
|
866
|
+
if updater is None:
|
|
867
|
+
return
|
|
868
|
+
if updater(abs(self.transform().m11())):
|
|
869
|
+
# The flip changed which items are visible: repaint so the area the
|
|
870
|
+
# now hidden items occupied is cleared in this pass, not the next.
|
|
871
|
+
self.viewport().update()
|
|
872
|
+
|
|
873
|
+
def paintEvent(self, event) -> None:
|
|
874
|
+
"""Sync the level of detail with the zoom, then paint the scene."""
|
|
875
|
+
self._syncDetailLod()
|
|
876
|
+
super().paintEvent(event)
|
|
877
|
+
|
|
878
|
+
def wheelEvent(self, event: Optional[QWheelEvent]) -> None:
|
|
879
|
+
"""overridden Qt's ``wheelEvent``. This handles zooming"""
|
|
880
|
+
if event is None:
|
|
881
|
+
return
|
|
882
|
+
|
|
883
|
+
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
884
|
+
# calculate our zoom Factor
|
|
885
|
+
zoomOutFactor = 1 / self.zoomInFactor
|
|
886
|
+
|
|
887
|
+
# calculate zoom
|
|
888
|
+
if event.angleDelta().y() > 0:
|
|
889
|
+
zoomFactor = self.zoomIn()
|
|
890
|
+
else:
|
|
891
|
+
zoomFactor = self.zoomOut()
|
|
892
|
+
|
|
893
|
+
self.applyZoom(zoomFactor)
|
|
894
|
+
|
|
895
|
+
elif event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
|
|
896
|
+
# Redirect vertical scrolling to horizontal scrolling
|
|
897
|
+
scroll_amount = event.angleDelta().y() # Get the vertical delta
|
|
898
|
+
scroll_bar = self.horizontalScrollBar()
|
|
899
|
+
if scroll_bar is not None:
|
|
900
|
+
scroll_bar.setValue(scroll_bar.value() - scroll_amount)
|
|
901
|
+
# self.horizontalScrollBar().setValue(
|
|
902
|
+
# self.horizontalScrollBar().value() - scroll_amount)
|
|
903
|
+
# self.horizontalScrollBar().setValue(2)
|
|
904
|
+
|
|
905
|
+
else:
|
|
906
|
+
# Normal scrolling
|
|
907
|
+
super().wheelEvent(event)
|