Cleanup
[carla.git] / source / frontend / patchcanvas / scene.py
blob7ed1a9330bb123e189302b958816769bb2b2b0fd
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 from math import floor
10 from qt_compat import qt_config
12 if qt_config == 5:
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
16 elif qt_config == 6:
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 # ------------------------------------------------------------------------------------------------------------
22 # Imports (Custom)
24 from . import (
25 canvas,
26 CanvasBoxType,
27 CanvasIconType,
28 CanvasPortType,
29 CanvasLineType,
30 CanvasBezierLineType,
31 CanvasRubberbandType,
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))
42 self.setZValue(-1)
43 self.hide()
45 scene.addItem(self)
47 def type(self):
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)
70 self.m_view = view
71 if not self.m_view:
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:
82 return 1.0
84 return self.m_view.devicePixelRatioF()
86 def getScaleFactor(self):
87 return self.m_view.transform().m11()
89 def getView(self):
90 return self.m_view
92 def fixScaleFactor(self, transform=None):
93 fix, set_view = False, False
94 if not transform:
95 set_view = True
96 view = self.m_view
97 transform = view.transform()
99 scale = transform.m11()
100 if scale > self.m_scale_max:
101 fix = True
102 transform.reset()
103 transform.scale(self.m_scale_max, self.m_scale_max)
104 elif scale < self.m_scale_min:
105 fix = True
106 transform.reset()
107 transform.scale(self.m_scale_min, self.m_scale_min)
109 if set_view:
110 if fix:
111 view.setTransform(transform)
112 self.scaleChanged.emit(transform.m11())
114 return fix
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)
132 def zoom_fit(self):
133 min_x = min_y = max_x = max_y = None
134 first_value = True
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()
144 x = pos.x()
145 y = pos.y()
146 if first_value:
147 first_value = False
148 min_x, min_y = x, y
149 max_x = x + rect.width()
150 max_y = y + rect.height()
151 else:
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())
157 if not first_value:
158 self.m_view.fitInView(min_x, min_y, abs(max_x - min_x), abs(max_y - min_y), Qt.KeepAspectRatio)
159 self.fixScaleFactor()
161 def zoom_in(self):
162 view = self.m_view
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:
167 transform.reset()
168 transform.scale(self.m_scale_max, self.m_scale_max)
169 view.setTransform(transform)
170 self.scaleChanged.emit(transform.m11())
172 def zoom_out(self):
173 view = self.m_view
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:
178 transform.reset()
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()
203 else:
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)
240 @pyqtSlot()
241 def slot_selectionChanged(self):
242 items_list = self.selectedItems()
244 if len(items_list) == 0:
245 self.pluginSelected.emit([])
246 return
248 plugin_list = []
250 for item in items_list:
251 if item and item.isVisible():
252 group_item = None
254 if item.type() == CanvasBoxType:
255 group_item = item
256 elif item.type() == CanvasPortType:
257 group_item = item.parentItem()
258 #elif item.type() in (CanvasLineType, CanvasBezierLineType, CanvasLineMovType, CanvasBezierLineMovType):
259 #plugin_list = []
260 #break
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:
265 plugin_id = 0
266 plugin_list.append(plugin_id)
268 self.pluginSelected.emit(plugin_list)
270 def keyPressEvent(self, event):
271 if not self.m_view:
272 event.ignore()
273 return
275 if event.key() == Qt.Key_Home:
276 event.accept()
277 self.zoom_fit()
278 return
280 if event.modifiers() & Qt.ControlModifier:
281 if event.key() == Qt.Key_Plus:
282 event.accept()
283 self.zoom_in()
284 return
286 if event.key() == Qt.Key_Minus:
287 event.accept()
288 self.zoom_out()
289 return
291 if event.key() == Qt.Key_1:
292 event.accept()
293 self.zoom_reset()
294 return
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())
313 for item in items:
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())
323 for item in items:
324 if item and item.type() in (CanvasBoxType, CanvasIconType, CanvasPortType):
325 self.m_mouse_rubberband = False
326 break
327 else:
328 self.m_mouse_rubberband = True
330 if self.m_mouse_rubberband:
331 event.accept()
332 pos = event.scenePos()
333 pos_x = pos.x()
334 pos_y = pos.y()
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,
346 y+lineHinting,
347 abs(pos_x - rubberband_orig_point.x()),
348 abs(pos_y - rubberband_orig_point.y()))
349 return
351 if self.m_connection_cut_mode:
352 trail = QPolygonF([event.scenePos(), event.lastScenePos(), event.scenePos()])
353 items = self.items(trail)
354 for item in items:
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()
365 needs_update = False
366 for item in items_list:
367 if item and item.isVisible() and item.type() == CanvasBoxType:
368 item.checkItemPos()
369 needs_update = True
371 if needs_update:
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):
388 if not self.m_view:
389 event.ignore()
390 return
392 if event.modifiers() & Qt.ControlModifier:
393 event.accept()
394 self.zoom_wheel(event.delta())
395 return
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)
403 return
405 if event.modifiers() & Qt.ControlModifier:
406 event.accept()
407 self.triggerRubberbandScale()
408 return
410 if len(self.selectedItems()) == 0:
411 self.m_mouse_down_init = False
412 event.accept()
413 canvas.callback(ACTION_BG_RIGHT_CLICK, 0, 0, "")
414 return
416 self.m_mouse_down_init = False
417 QGraphicsScene.contextMenuEvent(self, event)
419 # ------------------------------------------------------------------------------------------------------------