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.

425 lines
14KB

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