2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
10 from qt_compat
import qt_config
13 from PyQt5
.QtCore
import QT_VERSION
, pyqtSignal
, pyqtSlot
, qFatal
, Qt
, QPointF
, QRectF
14 from PyQt5
.QtGui
import QCursor
, QPixmap
, QPolygonF
15 from PyQt5
.QtWidgets
import QGraphicsRectItem
, QGraphicsScene
17 from PyQt6
.QtCore
import QT_VERSION
, pyqtSignal
, pyqtSlot
, qFatal
, Qt
, QPointF
, QRectF
18 from PyQt6
.QtGui
import QCursor
, QPixmap
, QPolygonF
19 from PyQt6
.QtWidgets
import QGraphicsRectItem
, QGraphicsScene
21 # ------------------------------------------------------------------------------------------------------------
32 ACTION_BG_RIGHT_CLICK
,
33 MAX_PLUGIN_ID_ALLOWED
,
36 # ------------------------------------------------------------------------------------------------------------
38 class RubberbandRect(QGraphicsRectItem
):
39 def __init__(self
, scene
):
40 QGraphicsRectItem
.__init
__(self
, QRectF(0, 0, 0, 0))
48 return CanvasRubberbandType
50 # ------------------------------------------------------------------------------------------------------------
52 class PatchScene(QGraphicsScene
):
53 scaleChanged
= pyqtSignal(float)
54 pluginSelected
= pyqtSignal(list)
56 def __init__(self
, parent
, view
):
57 QGraphicsScene
.__init
__(self
, parent
)
59 self
.m_connection_cut_mode
= False
60 self
.m_scale_area
= False
61 self
.m_mouse_down_init
= False
62 self
.m_mouse_rubberband
= False
63 self
.m_scale_min
= 0.1
64 self
.m_scale_max
= 4.0
66 self
.m_rubberband
= RubberbandRect(self
)
67 self
.m_rubberband_selection
= False
68 self
.m_rubberband_orig_point
= QPointF(0, 0)
72 qFatal("PatchCanvas::PatchScene() - invalid view")
74 self
.m_cursor_cut
= None
75 self
.m_cursor_zoom
= None
77 self
.setItemIndexMethod(QGraphicsScene
.NoIndex
)
78 self
.selectionChanged
.connect(self
.slot_selectionChanged
)
80 def getDevicePixelRatioF(self
):
81 if QT_VERSION
< 0x50600:
84 return self
.m_view
.devicePixelRatioF()
86 def getScaleFactor(self
):
87 return self
.m_view
.transform().m11()
92 def fixScaleFactor(self
, transform
=None):
93 fix
, set_view
= False, False
97 transform
= view
.transform()
99 scale
= transform
.m11()
100 if scale
> self
.m_scale_max
:
103 transform
.scale(self
.m_scale_max
, self
.m_scale_max
)
104 elif scale
< self
.m_scale_min
:
107 transform
.scale(self
.m_scale_min
, self
.m_scale_min
)
111 view
.setTransform(transform
)
112 self
.scaleChanged
.emit(transform
.m11())
116 def updateLimits(self
):
117 w0
= canvas
.size_rect
.width()
118 h0
= canvas
.size_rect
.height()
119 w1
= self
.m_view
.width()
120 h1
= self
.m_view
.height()
121 self
.m_scale_min
= w1
/w0
if w0
/h0
> w1
/h1
else h1
/h0
123 def updateTheme(self
):
124 self
.setBackgroundBrush(canvas
.theme
.canvas_bg
)
125 self
.m_rubberband
.setPen(canvas
.theme
.rubberband_pen
)
126 self
.m_rubberband
.setBrush(canvas
.theme
.rubberband_brush
)
128 cur_color
= "black" if canvas
.theme
.canvas_bg
.blackF() < 0.5 else "white"
129 self
.m_cursor_cut
= QCursor(QPixmap(":/cursors/cut_"+cur_color
+".png"), 1, 1)
130 self
.m_cursor_zoom
= QCursor(QPixmap(":/cursors/zoom-area_"+cur_color
+".png"), 8, 7)
133 min_x
= min_y
= max_x
= max_y
= None
136 items_list
= self
.items()
138 if len(items_list
) > 0:
139 for item
in items_list
:
140 if item
and item
.isVisible() and item
.type() == CanvasBoxType
:
141 pos
= item
.scenePos()
142 rect
= item
.boundingRect()
149 max_x
= x
+ rect
.width()
150 max_y
= y
+ rect
.height()
152 min_x
= min(min_x
, x
)
153 min_y
= min(min_y
, y
)
154 max_x
= max(max_x
, x
+ rect
.width())
155 max_y
= max(max_y
, y
+ rect
.height())
158 self
.m_view
.fitInView(min_x
, min_y
, abs(max_x
- min_x
), abs(max_y
- min_y
), Qt
.KeepAspectRatio
)
159 self
.fixScaleFactor()
163 transform
= view
.transform()
164 if transform
.m11() < self
.m_scale_max
:
165 transform
.scale(1.2, 1.2)
166 if transform
.m11() > self
.m_scale_max
:
168 transform
.scale(self
.m_scale_max
, self
.m_scale_max
)
169 view
.setTransform(transform
)
170 self
.scaleChanged
.emit(transform
.m11())
174 transform
= view
.transform()
175 if transform
.m11() > self
.m_scale_min
:
176 transform
.scale(0.833333333333333, 0.833333333333333)
177 if transform
.m11() < self
.m_scale_min
:
179 transform
.scale(self
.m_scale_min
, self
.m_scale_min
)
180 view
.setTransform(transform
)
181 self
.scaleChanged
.emit(transform
.m11())
183 def zoom_reset(self
):
184 self
.m_view
.resetTransform()
185 self
.scaleChanged
.emit(1.0)
187 def handleMouseRelease(self
):
188 rubberband_active
= self
.m_rubberband_selection
190 if self
.m_scale_area
and not self
.m_rubberband_selection
:
191 self
.m_scale_area
= False
192 self
.m_view
.viewport().unsetCursor()
194 if self
.m_rubberband_selection
:
195 if self
.m_scale_area
:
196 self
.m_scale_area
= False
197 self
.m_view
.viewport().unsetCursor()
199 rect
= self
.m_rubberband
.rect()
200 self
.m_view
.fitInView(rect
.x(), rect
.y(), rect
.width(), rect
.height(), Qt
.KeepAspectRatio
)
201 self
.fixScaleFactor()
204 items_list
= self
.items()
205 for item
in items_list
:
206 if item
and item
.isVisible() and item
.type() == CanvasBoxType
:
207 item_rect
= item
.sceneBoundingRect()
208 item_top_left
= QPointF(item_rect
.x(), item_rect
.y())
209 item_bottom_right
= QPointF(item_rect
.x() + item_rect
.width(),
210 item_rect
.y() + item_rect
.height())
212 if self
.m_rubberband
.contains(item_top_left
) and self
.m_rubberband
.contains(item_bottom_right
):
213 item
.setSelected(True)
215 self
.m_rubberband
.hide()
216 self
.m_rubberband
.setRect(0, 0, 0, 0)
217 self
.m_rubberband_selection
= False
219 self
.m_mouse_rubberband
= False
221 self
.stopConnectionCut()
223 return rubberband_active
225 def startConnectionCut(self
):
226 if self
.m_cursor_cut
:
227 self
.m_connection_cut_mode
= True
228 self
.m_view
.viewport().setCursor(self
.m_cursor_cut
)
230 def stopConnectionCut(self
):
231 if self
.m_connection_cut_mode
:
232 self
.m_connection_cut_mode
= False
233 self
.m_view
.viewport().unsetCursor()
235 def triggerRubberbandScale(self
):
236 self
.m_scale_area
= True
237 if self
.m_cursor_zoom
:
238 self
.m_view
.viewport().setCursor(self
.m_cursor_zoom
)
241 def slot_selectionChanged(self
):
242 items_list
= self
.selectedItems()
244 if len(items_list
) == 0:
245 self
.pluginSelected
.emit([])
250 for item
in items_list
:
251 if item
and item
.isVisible():
254 if item
.type() == CanvasBoxType
:
256 elif item
.type() == CanvasPortType
:
257 group_item
= item
.parentItem()
258 #elif item.type() in (CanvasLineType, CanvasBezierLineType, CanvasLineMovType, CanvasBezierLineMovType):
262 if group_item
is not None and group_item
.m_plugin_id
>= 0:
263 plugin_id
= group_item
.m_plugin_id
264 if plugin_id
> MAX_PLUGIN_ID_ALLOWED
:
266 plugin_list
.append(plugin_id
)
268 self
.pluginSelected
.emit(plugin_list
)
270 def keyPressEvent(self
, event
):
275 if event
.key() == Qt
.Key_Home
:
280 if event
.modifiers() & Qt
.ControlModifier
:
281 if event
.key() == Qt
.Key_Plus
:
286 if event
.key() == Qt
.Key_Minus
:
291 if event
.key() == Qt
.Key_1
:
296 QGraphicsScene
.keyPressEvent(self
, event
)
298 def keyReleaseEvent(self
, event
):
299 self
.stopConnectionCut()
300 QGraphicsScene
.keyReleaseEvent(self
, event
)
302 def mousePressEvent(self
, event
):
303 ctrlDown
= bool(event
.modifiers() & Qt
.ControlModifier
)
305 self
.m_mouse_down_init
= (
306 (event
.button() == Qt
.LeftButton
) or ((event
.button() == Qt
.RightButton
) and ctrlDown
)
308 self
.m_mouse_rubberband
= False
310 if event
.button() == Qt
.MiddleButton
and ctrlDown
:
311 self
.startConnectionCut()
312 items
= self
.items(event
.scenePos())
314 if item
and item
.type() in (CanvasLineType
, CanvasBezierLineType
, CanvasPortType
):
315 item
.triggerDisconnect()
317 QGraphicsScene
.mousePressEvent(self
, event
)
319 def mouseMoveEvent(self
, event
):
320 if self
.m_mouse_down_init
:
321 self
.m_mouse_down_init
= False
322 items
= self
.items(event
.scenePos())
324 if item
and item
.type() in (CanvasBoxType
, CanvasIconType
, CanvasPortType
):
325 self
.m_mouse_rubberband
= False
328 self
.m_mouse_rubberband
= True
330 if self
.m_mouse_rubberband
:
332 pos
= event
.scenePos()
335 if not self
.m_rubberband_selection
:
336 self
.m_rubberband
.show()
337 self
.m_rubberband_selection
= True
338 self
.m_rubberband_orig_point
= pos
339 rubberband_orig_point
= self
.m_rubberband_orig_point
341 x
= min(pos_x
, rubberband_orig_point
.x())
342 y
= min(pos_y
, rubberband_orig_point
.y())
344 lineHinting
= canvas
.theme
.rubberband_pen
.widthF() / 2
345 self
.m_rubberband
.setRect(x
+lineHinting
,
347 abs(pos_x
- rubberband_orig_point
.x()),
348 abs(pos_y
- rubberband_orig_point
.y()))
351 if self
.m_connection_cut_mode
:
352 trail
= QPolygonF([event
.scenePos(), event
.lastScenePos(), event
.scenePos()])
353 items
= self
.items(trail
)
355 if item
and item
.type() in (CanvasLineType
, CanvasBezierLineType
):
356 item
.triggerDisconnect()
358 QGraphicsScene
.mouseMoveEvent(self
, event
)
360 def mouseReleaseEvent(self
, event
):
361 self
.m_mouse_down_init
= False
363 if not self
.handleMouseRelease():
364 items_list
= self
.selectedItems()
366 for item
in items_list
:
367 if item
and item
.isVisible() and item
.type() == CanvasBoxType
:
372 canvas
.scene
.update()
374 QGraphicsScene
.mouseReleaseEvent(self
, event
)
376 def zoom_wheel(self
, delta
):
377 transform
= self
.m_view
.transform()
378 scale
= transform
.m11()
380 if (delta
> 0 and scale
< self
.m_scale_max
) or (delta
< 0 and scale
> self
.m_scale_min
):
381 factor
= 1.41 ** (delta
/ 240.0)
382 transform
.scale(factor
, factor
)
383 self
.fixScaleFactor(transform
)
384 self
.m_view
.setTransform(transform
)
385 self
.scaleChanged
.emit(transform
.m11())
387 def wheelEvent(self
, event
):
392 if event
.modifiers() & Qt
.ControlModifier
:
394 self
.zoom_wheel(event
.delta())
397 QGraphicsScene
.wheelEvent(self
, event
)
399 def contextMenuEvent(self
, event
):
400 if self
.handleMouseRelease():
401 self
.m_mouse_down_init
= False
402 QGraphicsScene
.contextMenuEvent(self
, event
)
405 if event
.modifiers() & Qt
.ControlModifier
:
407 self
.triggerRubberbandScale()
410 if len(self
.selectedItems()) == 0:
411 self
.m_mouse_down_init
= False
413 canvas
.callback(ACTION_BG_RIGHT_CLICK
, 0, 0, "")
416 self
.m_mouse_down_init
= False
417 QGraphicsScene
.contextMenuEvent(self
, event
)
419 # ------------------------------------------------------------------------------------------------------------