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.

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