Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

scene.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # PatchBay Canvas engine using QGraphicsView/Scene
  4. # Copyright (C) 2010-2019 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Global)
  19. from math import floor
  20. from PyQt5.QtCore import pyqtSignal, pyqtSlot, qFatal, Qt, QPointF, QRectF
  21. from PyQt5.QtGui import QCursor, QPixmap, QPolygonF
  22. from PyQt5.QtWidgets import QGraphicsRectItem, QGraphicsScene
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Imports (Custom)
  25. from . import (
  26. canvas,
  27. CanvasBoxType,
  28. CanvasPortType,
  29. CanvasLineType,
  30. CanvasBezierLineType,
  31. CanvasRubberbandType,
  32. ACTION_BG_RIGHT_CLICK,
  33. MAX_PLUGIN_ID_ALLOWED,
  34. )
  35. # ------------------------------------------------------------------------------------------------------------
  36. class RubberbandRect(QGraphicsRectItem):
  37. def __init__(self, scene):
  38. QGraphicsRectItem.__init__(self, QRectF(0, 0, 0, 0))
  39. self.setZValue(-1)
  40. self.hide()
  41. scene.addItem(self)
  42. def type(self):
  43. return CanvasRubberbandType
  44. # ------------------------------------------------------------------------------------------------------------
  45. class PatchScene(QGraphicsScene):
  46. scaleChanged = pyqtSignal(float)
  47. sceneGroupMoved = pyqtSignal(int, int, QPointF)
  48. pluginSelected = pyqtSignal(list)
  49. def __init__(self, parent, view):
  50. QGraphicsScene.__init__(self, parent)
  51. self.m_ctrl_down = False
  52. self.m_scale_area = False
  53. self.m_mouse_down_init = False
  54. self.m_mouse_rubberband = False
  55. self.m_mid_button_down = False
  56. self.m_pointer_border = QRectF(0.0, 0.0, 1.0, 1.0)
  57. self.m_scale_min = 0.1
  58. self.m_rubberband = RubberbandRect(self)
  59. self.m_rubberband_selection = False
  60. self.m_rubberband_orig_point = QPointF(0, 0)
  61. self.m_view = view
  62. if not self.m_view:
  63. qFatal("PatchCanvas::PatchScene() - invalid view")
  64. self.curCut = None
  65. self.curZoomArea = None
  66. self.selectionChanged.connect(self.slot_selectionChanged)
  67. def fixScaleFactor(self, transform=None):
  68. fix, set_view = False, False
  69. if not transform:
  70. set_view = True
  71. view = self.m_view
  72. transform = view.transform()
  73. scale = transform.m11()
  74. if scale > 3.0:
  75. fix = True
  76. transform.reset()
  77. transform.scale(3.0, 3.0)
  78. elif scale < self.m_scale_min:
  79. fix = True
  80. transform.reset()
  81. transform.scale(self.m_scale_min, self.m_scale_min)
  82. if set_view:
  83. if fix:
  84. view.setTransform(transform)
  85. self.scaleChanged.emit(transform.m11())
  86. return fix
  87. def updateLimits(self):
  88. w0 = canvas.size_rect.width()
  89. h0 = canvas.size_rect.height()
  90. w1 = self.m_view.width()
  91. h1 = self.m_view.height()
  92. self.m_scale_min = w1/w0 if w0/h0 > w1/h1 else h1/h0
  93. def updateTheme(self):
  94. self.setBackgroundBrush(canvas.theme.canvas_bg)
  95. self.m_rubberband.setPen(canvas.theme.rubberband_pen)
  96. self.m_rubberband.setBrush(canvas.theme.rubberband_brush)
  97. cur_color = "black" if canvas.theme.canvas_bg.blackF() < 0.5 else "white"
  98. self.curCut = QCursor(QPixmap(":/cursors/cut-"+cur_color+".png"), 1, 1)
  99. self.curZoomArea = QCursor(QPixmap(":/cursors/zoom-area-"+cur_color+".png"), 8, 7)
  100. def zoom_fit(self):
  101. min_x = min_y = max_x = max_y = None
  102. first_value = True
  103. items_list = self.items()
  104. if len(items_list) > 0:
  105. for item in items_list:
  106. if item and item.isVisible() and item.type() == CanvasBoxType:
  107. pos = item.scenePos()
  108. rect = item.boundingRect()
  109. x = pos.x()
  110. y = pos.y()
  111. if first_value:
  112. first_value = False
  113. min_x, min_y = x, y
  114. max_x = x + rect.width()
  115. max_y = y + rect.height()
  116. else:
  117. min_x = min(min_x, x)
  118. min_y = min(min_y, y)
  119. max_x = max(max_x, x + rect.width())
  120. max_y = max(max_y, y + rect.height())
  121. if not first_value:
  122. self.m_view.fitInView(min_x, min_y, abs(max_x - min_x), abs(max_y - min_y), Qt.KeepAspectRatio)
  123. self.fixScaleFactor()
  124. def zoom_in(self):
  125. view = self.m_view
  126. transform = view.transform()
  127. if transform.m11() < 3.0:
  128. transform.scale(1.2, 1.2)
  129. view.setTransform(transform)
  130. self.scaleChanged.emit(transform.m11())
  131. def zoom_out(self):
  132. view = self.m_view
  133. transform = view.transform()
  134. if transform.m11() > self.m_scale_min:
  135. transform.scale(0.833333333333333, 0.833333333333333)
  136. view.setTransform(transform)
  137. self.scaleChanged.emit(transform.m11())
  138. def zoom_reset(self):
  139. self.m_view.resetTransform()
  140. self.scaleChanged.emit(1.0)
  141. @pyqtSlot()
  142. def slot_selectionChanged(self):
  143. items_list = self.selectedItems()
  144. if len(items_list) == 0:
  145. self.pluginSelected.emit([])
  146. return
  147. plugin_list = []
  148. for item in items_list:
  149. if item and item.isVisible():
  150. group_item = None
  151. if item.type() == CanvasBoxType:
  152. group_item = item
  153. elif item.type() == CanvasPortType:
  154. group_item = item.parentItem()
  155. #elif item.type() in (CanvasLineType, CanvasBezierLineType, CanvasLineMovType, CanvasBezierLineMovType):
  156. #plugin_list = []
  157. #break
  158. if group_item is not None and group_item.m_plugin_id >= 0:
  159. plugin_id = group_item.m_plugin_id
  160. if plugin_id > MAX_PLUGIN_ID_ALLOWED:
  161. plugin_id = 0
  162. plugin_list.append(plugin_id)
  163. self.pluginSelected.emit(plugin_list)
  164. def triggerRubberbandScale(self):
  165. self.m_scale_area = True
  166. if self.curZoomArea:
  167. self.m_view.viewport().setCursor(self.curZoomArea)
  168. def keyPressEvent(self, event):
  169. if not self.m_view:
  170. event.ignore()
  171. return
  172. if event.key() == Qt.Key_Control:
  173. self.m_ctrl_down = True
  174. if self.m_mid_button_down:
  175. self.startConnectionCut()
  176. elif event.key() == Qt.Key_Home:
  177. event.accept()
  178. self.zoom_fit()
  179. return
  180. elif self.m_ctrl_down:
  181. if event.key() == Qt.Key_Plus:
  182. event.accept()
  183. self.zoom_in()
  184. return
  185. if event.key() == Qt.Key_Minus:
  186. event.accept()
  187. self.zoom_out()
  188. return
  189. if event.key() == Qt.Key_1:
  190. event.accept()
  191. self.zoom_reset()
  192. return
  193. QGraphicsScene.keyPressEvent(self, event)
  194. def keyReleaseEvent(self, event):
  195. if event.key() == Qt.Key_Control:
  196. self.m_ctrl_down = False
  197. # Connection cut mode off
  198. if self.m_mid_button_down:
  199. self.m_view.viewport().unsetCursor()
  200. QGraphicsScene.keyReleaseEvent(self, event)
  201. def startConnectionCut(self):
  202. if self.curCut:
  203. self.m_view.viewport().setCursor(self.curCut)
  204. def mousePressEvent(self, event):
  205. self.m_mouse_down_init = (
  206. (event.button() == Qt.LeftButton) or ((event.button() == Qt.RightButton) and self.m_ctrl_down)
  207. )
  208. self.m_mouse_rubberband = False
  209. if event.button() == Qt.MidButton and self.m_ctrl_down:
  210. self.m_mid_button_down = True
  211. self.startConnectionCut()
  212. pos = event.scenePos()
  213. self.m_pointer_border.moveTo(floor(pos.x()), floor(pos.y()))
  214. items = self.items(self.m_pointer_border)
  215. for item in items:
  216. if item and item.type() in [CanvasLineType, CanvasBezierLineType, CanvasPortType]:
  217. item.triggerDisconnect()
  218. QGraphicsScene.mousePressEvent(self, event)
  219. def mouseMoveEvent(self, event):
  220. if self.m_mouse_down_init:
  221. self.m_mouse_down_init = False
  222. topmost = self.itemAt(event.scenePos(), self.m_view.transform())
  223. self.m_mouse_rubberband = not (topmost and topmost.type() in [CanvasBoxType, CanvasPortType])
  224. if self.m_mouse_rubberband:
  225. event.accept()
  226. pos = event.scenePos()
  227. pos_x = pos.x()
  228. pos_y = pos.y()
  229. if not self.m_rubberband_selection:
  230. self.m_rubberband.show()
  231. self.m_rubberband_selection = True
  232. self.m_rubberband_orig_point = pos
  233. rubberband_orig_point = self.m_rubberband_orig_point
  234. x = min(pos_x, rubberband_orig_point.x())
  235. y = min(pos_y, rubberband_orig_point.y())
  236. lineHinting = canvas.theme.rubberband_pen.widthF() / 2
  237. self.m_rubberband.setRect(x+lineHinting,
  238. y+lineHinting,
  239. abs(pos_x - rubberband_orig_point.x()),
  240. abs(pos_y - rubberband_orig_point.y()))
  241. return
  242. if self.m_mid_button_down and self.m_ctrl_down:
  243. trail = QPolygonF([event.scenePos(), event.lastScenePos(), event.scenePos()])
  244. items = self.items(trail)
  245. for item in items:
  246. if item and item.type() in [CanvasLineType, CanvasBezierLineType]:
  247. item.triggerDisconnect()
  248. QGraphicsScene.mouseMoveEvent(self, event)
  249. def mouseReleaseEvent(self, event):
  250. if self.m_scale_area and not self.m_rubberband_selection:
  251. self.m_scale_area = False
  252. self.m_view.viewport().unsetCursor()
  253. if self.m_rubberband_selection:
  254. if self.m_scale_area:
  255. self.m_scale_area = False
  256. self.m_view.viewport().unsetCursor()
  257. rect = self.m_rubberband.rect()
  258. self.m_view.fitInView(rect.x(), rect.y(), rect.width(), rect.height(), Qt.KeepAspectRatio)
  259. self.fixScaleFactor()
  260. else:
  261. items_list = self.items()
  262. for item in items_list:
  263. if item and item.isVisible() and item.type() == CanvasBoxType:
  264. item_rect = item.sceneBoundingRect()
  265. item_top_left = QPointF(item_rect.x(), item_rect.y())
  266. item_bottom_right = QPointF(item_rect.x() + item_rect.width(),
  267. item_rect.y() + item_rect.height())
  268. if self.m_rubberband.contains(item_top_left) and self.m_rubberband.contains(item_bottom_right):
  269. item.setSelected(True)
  270. self.m_rubberband.hide()
  271. self.m_rubberband.setRect(0, 0, 0, 0)
  272. self.m_rubberband_selection = False
  273. else:
  274. items_list = self.selectedItems()
  275. for item in items_list:
  276. if item and item.isVisible() and item.type() == CanvasBoxType:
  277. item.checkItemPos()
  278. self.sceneGroupMoved.emit(item.getGroupId(), item.getSplittedMode(), item.scenePos())
  279. if len(items_list) > 1:
  280. canvas.scene.update()
  281. self.m_mouse_down_init = False
  282. self.m_mouse_rubberband = False
  283. if event.button() == Qt.MidButton:
  284. event.accept()
  285. self.m_mid_button_down = False
  286. # Connection cut mode off
  287. if self.m_ctrl_down:
  288. self.m_view.viewport().unsetCursor()
  289. return
  290. QGraphicsScene.mouseReleaseEvent(self, event)
  291. def zoom_wheel(self, delta):
  292. transform = self.m_view.transform()
  293. scale = transform.m11()
  294. if (delta > 0 and scale < 3.0) or (delta < 0 and scale > self.m_scale_min):
  295. factor = 1.41 ** (delta / 240.0)
  296. transform.scale(factor, factor)
  297. self.fixScaleFactor(transform)
  298. self.m_view.setTransform(transform)
  299. self.scaleChanged.emit(transform.m11())
  300. def wheelEvent(self, event):
  301. if not self.m_view:
  302. event.ignore()
  303. return
  304. if self.m_ctrl_down:
  305. event.accept()
  306. self.zoom_wheel(event.delta())
  307. return
  308. QGraphicsScene.wheelEvent(self, event)
  309. def contextMenuEvent(self, event):
  310. if self.m_ctrl_down:
  311. event.accept()
  312. self.triggerRubberbandScale()
  313. return
  314. if len(self.selectedItems()) == 0:
  315. event.accept()
  316. canvas.callback(ACTION_BG_RIGHT_CLICK, 0, 0, "")
  317. return
  318. QGraphicsScene.contextMenuEvent(self, event)
  319. # ------------------------------------------------------------------------------------------------------------