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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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 QT_VERSION, 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. CanvasIconType,
  29. CanvasPortType,
  30. CanvasLineType,
  31. CanvasBezierLineType,
  32. CanvasRubberbandType,
  33. ACTION_BG_RIGHT_CLICK,
  34. MAX_PLUGIN_ID_ALLOWED,
  35. )
  36. # ------------------------------------------------------------------------------------------------------------
  37. class RubberbandRect(QGraphicsRectItem):
  38. def __init__(self, scene):
  39. QGraphicsRectItem.__init__(self, QRectF(0, 0, 0, 0))
  40. self.setZValue(-1)
  41. self.hide()
  42. scene.addItem(self)
  43. def type(self):
  44. return CanvasRubberbandType
  45. # ------------------------------------------------------------------------------------------------------------
  46. class PatchScene(QGraphicsScene):
  47. scaleChanged = pyqtSignal(float)
  48. pluginSelected = pyqtSignal(list)
  49. def __init__(self, parent, view):
  50. QGraphicsScene.__init__(self, parent)
  51. self.m_connection_cut_mode = False
  52. self.m_scale_area = False
  53. self.m_mouse_down_init = False
  54. self.m_mouse_rubberband = False
  55. self.m_scale_min = 0.1
  56. self.m_scale_max = 4.0
  57. self.m_rubberband = RubberbandRect(self)
  58. self.m_rubberband_selection = False
  59. self.m_rubberband_orig_point = QPointF(0, 0)
  60. self.m_view = view
  61. if not self.m_view:
  62. qFatal("PatchCanvas::PatchScene() - invalid view")
  63. self.m_cursor_cut = None
  64. self.m_cursor_zoom = None
  65. self.setItemIndexMethod(QGraphicsScene.NoIndex)
  66. self.selectionChanged.connect(self.slot_selectionChanged)
  67. def getDevicePixelRatioF(self):
  68. if QT_VERSION < 0x50600:
  69. return 1.0
  70. return self.m_view.devicePixelRatioF()
  71. def getScaleFactor(self):
  72. return self.m_view.transform().m11()
  73. def getView(self):
  74. return self.m_view
  75. def fixScaleFactor(self, transform=None):
  76. fix, set_view = False, False
  77. if not transform:
  78. set_view = True
  79. view = self.m_view
  80. transform = view.transform()
  81. scale = transform.m11()
  82. if scale > self.m_scale_max:
  83. fix = True
  84. transform.reset()
  85. transform.scale(self.m_scale_max, self.m_scale_max)
  86. elif scale < self.m_scale_min:
  87. fix = True
  88. transform.reset()
  89. transform.scale(self.m_scale_min, self.m_scale_min)
  90. if set_view:
  91. if fix:
  92. view.setTransform(transform)
  93. self.scaleChanged.emit(transform.m11())
  94. return fix
  95. def updateLimits(self):
  96. w0 = canvas.size_rect.width()
  97. h0 = canvas.size_rect.height()
  98. w1 = self.m_view.width()
  99. h1 = self.m_view.height()
  100. self.m_scale_min = w1/w0 if w0/h0 > w1/h1 else h1/h0
  101. def updateTheme(self):
  102. self.setBackgroundBrush(canvas.theme.canvas_bg)
  103. self.m_rubberband.setPen(canvas.theme.rubberband_pen)
  104. self.m_rubberband.setBrush(canvas.theme.rubberband_brush)
  105. cur_color = "black" if canvas.theme.canvas_bg.blackF() < 0.5 else "white"
  106. self.m_cursor_cut = QCursor(QPixmap(":/cursors/cut_"+cur_color+".png"), 1, 1)
  107. self.m_cursor_zoom = QCursor(QPixmap(":/cursors/zoom-area_"+cur_color+".png"), 8, 7)
  108. def zoom_fit(self):
  109. min_x = min_y = max_x = max_y = None
  110. first_value = True
  111. items_list = self.items()
  112. if len(items_list) > 0:
  113. for item in items_list:
  114. if item and item.isVisible() and item.type() == CanvasBoxType:
  115. pos = item.scenePos()
  116. rect = item.boundingRect()
  117. x = pos.x()
  118. y = pos.y()
  119. if first_value:
  120. first_value = False
  121. min_x, min_y = x, y
  122. max_x = x + rect.width()
  123. max_y = y + rect.height()
  124. else:
  125. min_x = min(min_x, x)
  126. min_y = min(min_y, y)
  127. max_x = max(max_x, x + rect.width())
  128. max_y = max(max_y, y + rect.height())
  129. if not first_value:
  130. self.m_view.fitInView(min_x, min_y, abs(max_x - min_x), abs(max_y - min_y), Qt.KeepAspectRatio)
  131. self.fixScaleFactor()
  132. def zoom_in(self):
  133. view = self.m_view
  134. transform = view.transform()
  135. if transform.m11() < self.m_scale_max:
  136. transform.scale(1.2, 1.2)
  137. if transform.m11() > self.m_scale_max:
  138. transform.reset()
  139. transform.scale(self.m_scale_max, self.m_scale_max)
  140. view.setTransform(transform)
  141. self.scaleChanged.emit(transform.m11())
  142. def zoom_out(self):
  143. view = self.m_view
  144. transform = view.transform()
  145. if transform.m11() > self.m_scale_min:
  146. transform.scale(0.833333333333333, 0.833333333333333)
  147. if transform.m11() < self.m_scale_min:
  148. transform.reset()
  149. transform.scale(self.m_scale_min, self.m_scale_min)
  150. view.setTransform(transform)
  151. self.scaleChanged.emit(transform.m11())
  152. def zoom_reset(self):
  153. self.m_view.resetTransform()
  154. self.scaleChanged.emit(1.0)
  155. def handleMouseRelease(self):
  156. rubberband_active = self.m_rubberband_selection
  157. if self.m_scale_area and not self.m_rubberband_selection:
  158. self.m_scale_area = False
  159. self.m_view.viewport().unsetCursor()
  160. if self.m_rubberband_selection:
  161. if self.m_scale_area:
  162. self.m_scale_area = False
  163. self.m_view.viewport().unsetCursor()
  164. rect = self.m_rubberband.rect()
  165. self.m_view.fitInView(rect.x(), rect.y(), rect.width(), rect.height(), Qt.KeepAspectRatio)
  166. self.fixScaleFactor()
  167. else:
  168. items_list = self.items()
  169. for item in items_list:
  170. if item and item.isVisible() and item.type() == CanvasBoxType:
  171. item_rect = item.sceneBoundingRect()
  172. item_top_left = QPointF(item_rect.x(), item_rect.y())
  173. item_bottom_right = QPointF(item_rect.x() + item_rect.width(),
  174. item_rect.y() + item_rect.height())
  175. if self.m_rubberband.contains(item_top_left) and self.m_rubberband.contains(item_bottom_right):
  176. item.setSelected(True)
  177. self.m_rubberband.hide()
  178. self.m_rubberband.setRect(0, 0, 0, 0)
  179. self.m_rubberband_selection = False
  180. self.m_mouse_rubberband = False
  181. self.stopConnectionCut()
  182. return rubberband_active
  183. def startConnectionCut(self):
  184. if self.m_cursor_cut:
  185. self.m_connection_cut_mode = True
  186. self.m_view.viewport().setCursor(self.m_cursor_cut)
  187. def stopConnectionCut(self):
  188. if self.m_connection_cut_mode:
  189. self.m_connection_cut_mode = False
  190. self.m_view.viewport().unsetCursor()
  191. def triggerRubberbandScale(self):
  192. self.m_scale_area = True
  193. if self.m_cursor_zoom:
  194. self.m_view.viewport().setCursor(self.m_cursor_zoom)
  195. @pyqtSlot()
  196. def slot_selectionChanged(self):
  197. items_list = self.selectedItems()
  198. if len(items_list) == 0:
  199. self.pluginSelected.emit([])
  200. return
  201. plugin_list = []
  202. for item in items_list:
  203. if item and item.isVisible():
  204. group_item = None
  205. if item.type() == CanvasBoxType:
  206. group_item = item
  207. elif item.type() == CanvasPortType:
  208. group_item = item.parentItem()
  209. #elif item.type() in (CanvasLineType, CanvasBezierLineType, CanvasLineMovType, CanvasBezierLineMovType):
  210. #plugin_list = []
  211. #break
  212. if group_item is not None and group_item.m_plugin_id >= 0:
  213. plugin_id = group_item.m_plugin_id
  214. if plugin_id > MAX_PLUGIN_ID_ALLOWED:
  215. plugin_id = 0
  216. plugin_list.append(plugin_id)
  217. self.pluginSelected.emit(plugin_list)
  218. def keyPressEvent(self, event):
  219. if not self.m_view:
  220. event.ignore()
  221. return
  222. if event.key() == Qt.Key_Home:
  223. event.accept()
  224. self.zoom_fit()
  225. return
  226. if event.modifiers() & Qt.ControlModifier:
  227. if event.key() == Qt.Key_Plus:
  228. event.accept()
  229. self.zoom_in()
  230. return
  231. if event.key() == Qt.Key_Minus:
  232. event.accept()
  233. self.zoom_out()
  234. return
  235. if event.key() == Qt.Key_1:
  236. event.accept()
  237. self.zoom_reset()
  238. return
  239. QGraphicsScene.keyPressEvent(self, event)
  240. def keyReleaseEvent(self, event):
  241. self.stopConnectionCut()
  242. QGraphicsScene.keyReleaseEvent(self, event)
  243. def mousePressEvent(self, event):
  244. ctrlDown = bool(event.modifiers() & Qt.ControlModifier)
  245. self.m_mouse_down_init = (
  246. (event.button() == Qt.LeftButton) or ((event.button() == Qt.RightButton) and ctrlDown)
  247. )
  248. self.m_mouse_rubberband = False
  249. if event.button() == Qt.MidButton and ctrlDown:
  250. self.startConnectionCut()
  251. items = self.items(event.scenePos())
  252. for item in items:
  253. if item and item.type() in (CanvasLineType, CanvasBezierLineType, CanvasPortType):
  254. item.triggerDisconnect()
  255. QGraphicsScene.mousePressEvent(self, event)
  256. def mouseMoveEvent(self, event):
  257. if self.m_mouse_down_init:
  258. self.m_mouse_down_init = False
  259. items = self.items(event.scenePos())
  260. for item in items:
  261. if item and item.type() in (CanvasBoxType, CanvasIconType, CanvasPortType):
  262. self.m_mouse_rubberband = False
  263. break
  264. else:
  265. self.m_mouse_rubberband = True
  266. if self.m_mouse_rubberband:
  267. event.accept()
  268. pos = event.scenePos()
  269. pos_x = pos.x()
  270. pos_y = pos.y()
  271. if not self.m_rubberband_selection:
  272. self.m_rubberband.show()
  273. self.m_rubberband_selection = True
  274. self.m_rubberband_orig_point = pos
  275. rubberband_orig_point = self.m_rubberband_orig_point
  276. x = min(pos_x, rubberband_orig_point.x())
  277. y = min(pos_y, rubberband_orig_point.y())
  278. lineHinting = canvas.theme.rubberband_pen.widthF() / 2
  279. self.m_rubberband.setRect(x+lineHinting,
  280. y+lineHinting,
  281. abs(pos_x - rubberband_orig_point.x()),
  282. abs(pos_y - rubberband_orig_point.y()))
  283. return
  284. if self.m_connection_cut_mode:
  285. trail = QPolygonF([event.scenePos(), event.lastScenePos(), event.scenePos()])
  286. items = self.items(trail)
  287. for item in items:
  288. if item and item.type() in (CanvasLineType, CanvasBezierLineType):
  289. item.triggerDisconnect()
  290. QGraphicsScene.mouseMoveEvent(self, event)
  291. def mouseReleaseEvent(self, event):
  292. self.m_mouse_down_init = False
  293. if not self.handleMouseRelease():
  294. items_list = self.selectedItems()
  295. needs_update = False
  296. for item in items_list:
  297. if item and item.isVisible() and item.type() == CanvasBoxType:
  298. item.checkItemPos()
  299. needs_update = True
  300. if needs_update:
  301. canvas.scene.update()
  302. QGraphicsScene.mouseReleaseEvent(self, event)
  303. def zoom_wheel(self, delta):
  304. transform = self.m_view.transform()
  305. scale = transform.m11()
  306. if (delta > 0 and scale < self.m_scale_max) or (delta < 0 and scale > self.m_scale_min):
  307. factor = 1.41 ** (delta / 240.0)
  308. transform.scale(factor, factor)
  309. self.fixScaleFactor(transform)
  310. self.m_view.setTransform(transform)
  311. self.scaleChanged.emit(transform.m11())
  312. def wheelEvent(self, event):
  313. if not self.m_view:
  314. event.ignore()
  315. return
  316. if event.modifiers() & Qt.ControlModifier:
  317. event.accept()
  318. self.zoom_wheel(event.delta())
  319. return
  320. QGraphicsScene.wheelEvent(self, event)
  321. def contextMenuEvent(self, event):
  322. if self.handleMouseRelease():
  323. self.m_mouse_down_init = False
  324. QGraphicsScene.contextMenuEvent(self, event)
  325. return
  326. if event.modifiers() & Qt.ControlModifier:
  327. event.accept()
  328. self.triggerRubberbandScale()
  329. return
  330. if len(self.selectedItems()) == 0:
  331. self.m_mouse_down_init = False
  332. event.accept()
  333. canvas.callback(ACTION_BG_RIGHT_CLICK, 0, 0, "")
  334. return
  335. self.m_mouse_down_init = False
  336. QGraphicsScene.contextMenuEvent(self, event)
  337. # ------------------------------------------------------------------------------------------------------------