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.

492 lines
19KB

  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 qCritical, QT_VERSION, Qt, QLineF, QPointF, QRectF, QTimer
  21. from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QPainter, QPainterPath, QPen, QPolygonF
  22. from PyQt5.QtWidgets import QGraphicsItem, QMenu
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Imports (Custom)
  25. from . import (
  26. canvas,
  27. features,
  28. options,
  29. port_mode2str,
  30. port_type2str,
  31. CanvasPortType,
  32. ANTIALIASING_FULL,
  33. ACTION_PORT_INFO,
  34. ACTION_PORT_RENAME,
  35. ACTION_PORTS_CONNECT,
  36. ACTION_PORTS_DISCONNECT,
  37. PORT_MODE_INPUT,
  38. PORT_MODE_OUTPUT,
  39. PORT_TYPE_AUDIO_JACK,
  40. PORT_TYPE_MIDI_ALSA,
  41. PORT_TYPE_MIDI_JACK,
  42. PORT_TYPE_PARAMETER,
  43. )
  44. from .canvasbezierlinemov import CanvasBezierLineMov
  45. from .canvaslinemov import CanvasLineMov
  46. from .theme import Theme
  47. from .utils import CanvasGetFullPortName, CanvasGetPortConnectionList
  48. # ------------------------------------------------------------------------------------------------------------
  49. class CanvasPort(QGraphicsItem):
  50. def __init__(self, group_id, port_id, port_name, port_mode, port_type, is_alternate, parent):
  51. QGraphicsItem.__init__(self)
  52. self.setParentItem(parent)
  53. # Save Variables, useful for later
  54. self.m_group_id = group_id
  55. self.m_port_id = port_id
  56. self.m_port_mode = port_mode
  57. self.m_port_type = port_type
  58. self.m_port_name = port_name
  59. self.m_is_alternate = is_alternate
  60. # Base Variables
  61. self.m_port_width = 15
  62. self.m_port_height = canvas.theme.port_height
  63. self.m_port_font = QFont()
  64. self.m_port_font.setFamily(canvas.theme.port_font_name)
  65. self.m_port_font.setPixelSize(canvas.theme.port_font_size)
  66. self.m_port_font.setWeight(canvas.theme.port_font_state)
  67. self.m_line_mov = None
  68. self.m_hover_item = None
  69. self.m_mouse_down = False
  70. self.m_cursor_moving = False
  71. self.setFlags(QGraphicsItem.ItemIsSelectable)
  72. if options.auto_select_items:
  73. self.setAcceptHoverEvents(True)
  74. def getGroupId(self):
  75. return self.m_group_id
  76. def getPortId(self):
  77. return self.m_port_id
  78. def getPortMode(self):
  79. return self.m_port_mode
  80. def getPortType(self):
  81. return self.m_port_type
  82. def getPortName(self):
  83. return self.m_port_name
  84. def getFullPortName(self):
  85. return self.parentItem().getGroupName() + ":" + self.m_port_name
  86. def getPortWidth(self):
  87. return self.m_port_width
  88. def getPortHeight(self):
  89. return self.m_port_height
  90. def setPortMode(self, port_mode):
  91. self.m_port_mode = port_mode
  92. self.update()
  93. def setPortType(self, port_type):
  94. self.m_port_type = port_type
  95. self.update()
  96. def setPortName(self, port_name):
  97. metrics = QFontMetrics(self.m_port_font)
  98. if QT_VERSION >= 0x50b00:
  99. width1 = metrics.horizontalAdvance(port_name)
  100. width2 = metrics.horizontalAdvance(self.m_port_name)
  101. else:
  102. width1 = metrics.width(port_name)
  103. width2 = metrics.width(self.m_port_name)
  104. if width1 < width2:
  105. QTimer.singleShot(0, canvas.scene.update)
  106. self.m_port_name = port_name
  107. self.update()
  108. def setPortWidth(self, port_width):
  109. if port_width < self.m_port_width:
  110. QTimer.singleShot(0, canvas.scene.update)
  111. self.m_port_width = port_width
  112. self.update()
  113. def type(self):
  114. return CanvasPortType
  115. def hoverEnterEvent(self, event):
  116. if options.auto_select_items:
  117. self.setSelected(True)
  118. QGraphicsItem.hoverEnterEvent(self, event)
  119. def hoverLeaveEvent(self, event):
  120. if options.auto_select_items:
  121. self.setSelected(False)
  122. QGraphicsItem.hoverLeaveEvent(self, event)
  123. def mousePressEvent(self, event):
  124. if event.button() == Qt.MiddleButton or event.source() == Qt.MouseEventSynthesizedByApplication:
  125. event.ignore()
  126. return
  127. if self.m_mouse_down:
  128. self.handleMouseRelease()
  129. self.m_hover_item = None
  130. self.m_mouse_down = bool(event.button() == Qt.LeftButton)
  131. self.m_cursor_moving = False
  132. QGraphicsItem.mousePressEvent(self, event)
  133. def mouseMoveEvent(self, event):
  134. if not self.m_mouse_down:
  135. QGraphicsItem.mouseMoveEvent(self, event)
  136. return
  137. event.accept()
  138. if not self.m_cursor_moving:
  139. self.setCursor(QCursor(Qt.CrossCursor))
  140. self.m_cursor_moving = True
  141. for connection in canvas.connection_list:
  142. if (
  143. (connection.group_out_id == self.m_group_id and
  144. connection.port_out_id == self.m_port_id)
  145. or
  146. (connection.group_in_id == self.m_group_id and
  147. connection.port_in_id == self.m_port_id)
  148. ):
  149. connection.widget.setLocked(True)
  150. if not self.m_line_mov:
  151. if options.use_bezier_lines:
  152. self.m_line_mov = CanvasBezierLineMov(self.m_port_mode, self.m_port_type, self)
  153. else:
  154. self.m_line_mov = CanvasLineMov(self.m_port_mode, self.m_port_type, self)
  155. canvas.last_z_value += 1
  156. self.m_line_mov.setZValue(canvas.last_z_value)
  157. canvas.last_z_value += 1
  158. self.parentItem().setZValue(canvas.last_z_value)
  159. item = None
  160. items = canvas.scene.items(event.scenePos(), Qt.ContainsItemShape, Qt.AscendingOrder)
  161. #for i in range(len(items)):
  162. for _, itemx in enumerate(items):
  163. if itemx.type() != CanvasPortType:
  164. continue
  165. if itemx == self:
  166. continue
  167. if item is None or itemx.parentItem().zValue() > item.parentItem().zValue():
  168. item = itemx
  169. if self.m_hover_item and self.m_hover_item != item:
  170. self.m_hover_item.setSelected(False)
  171. if item is not None:
  172. if item.getPortMode() != self.m_port_mode and item.getPortType() == self.m_port_type:
  173. item.setSelected(True)
  174. self.m_hover_item = item
  175. else:
  176. self.m_hover_item = None
  177. else:
  178. self.m_hover_item = None
  179. self.m_line_mov.updateLinePos(event.scenePos())
  180. def handleMouseRelease(self):
  181. if self.m_mouse_down:
  182. if self.m_line_mov is not None:
  183. item = self.m_line_mov
  184. self.m_line_mov = None
  185. canvas.scene.removeItem(item)
  186. del item
  187. for connection in canvas.connection_list:
  188. if (
  189. (connection.group_out_id == self.m_group_id and
  190. connection.port_out_id == self.m_port_id)
  191. or
  192. (connection.group_in_id == self.m_group_id and
  193. connection.port_in_id == self.m_port_id)
  194. ):
  195. connection.widget.setLocked(False)
  196. if self.m_hover_item:
  197. # TODO: a better way to check already existing connection
  198. for connection in canvas.connection_list:
  199. hover_group_id = self.m_hover_item.getGroupId()
  200. hover_port_id = self.m_hover_item.getPortId()
  201. # FIXME clean this big if stuff
  202. if (
  203. (connection.group_out_id == self.m_group_id and
  204. connection.port_out_id == self.m_port_id and
  205. connection.group_in_id == hover_group_id and
  206. connection.port_in_id == hover_port_id)
  207. or
  208. (connection.group_out_id == hover_group_id and
  209. connection.port_out_id == hover_port_id and
  210. connection.group_in_id == self.m_group_id and
  211. connection.port_in_id == self.m_port_id)
  212. ):
  213. canvas.callback(ACTION_PORTS_DISCONNECT, connection.connection_id, 0, "")
  214. break
  215. else:
  216. if self.m_port_mode == PORT_MODE_OUTPUT:
  217. conn = "%i:%i:%i:%i" % (self.m_group_id, self.m_port_id,
  218. self.m_hover_item.getGroupId(), self.m_hover_item.getPortId())
  219. canvas.callback(ACTION_PORTS_CONNECT, 0, 0, conn)
  220. else:
  221. conn = "%i:%i:%i:%i" % (self.m_hover_item.getGroupId(),
  222. self.m_hover_item.getPortId(), self.m_group_id, self.m_port_id)
  223. canvas.callback(ACTION_PORTS_CONNECT, 0, 0, conn)
  224. canvas.scene.clearSelection()
  225. if self.m_cursor_moving:
  226. self.unsetCursor()
  227. self.m_hover_item = None
  228. self.m_mouse_down = False
  229. self.m_cursor_moving = False
  230. def mouseReleaseEvent(self, event):
  231. if event.button() == Qt.LeftButton:
  232. self.handleMouseRelease()
  233. QGraphicsItem.mouseReleaseEvent(self, event)
  234. def contextMenuEvent(self, event):
  235. event.accept()
  236. canvas.scene.clearSelection()
  237. self.setSelected(True)
  238. menu = QMenu()
  239. discMenu = QMenu("Disconnect", menu)
  240. conn_list = CanvasGetPortConnectionList(self.m_group_id, self.m_port_id)
  241. if len(conn_list) > 0:
  242. for conn_id, group_id, port_id in conn_list:
  243. act_x_disc = discMenu.addAction(CanvasGetFullPortName(group_id, port_id))
  244. act_x_disc.setData(conn_id)
  245. act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect)
  246. else:
  247. act_x_disc = discMenu.addAction("No connections")
  248. act_x_disc.setEnabled(False)
  249. menu.addMenu(discMenu)
  250. act_x_disc_all = menu.addAction("Disconnect &All")
  251. act_x_sep_1 = menu.addSeparator()
  252. act_x_info = menu.addAction("Get &Info")
  253. act_x_rename = menu.addAction("&Rename")
  254. if not features.port_info:
  255. act_x_info.setVisible(False)
  256. if not features.port_rename:
  257. act_x_rename.setVisible(False)
  258. if not (features.port_info and features.port_rename):
  259. act_x_sep_1.setVisible(False)
  260. act_selected = menu.exec_(event.screenPos())
  261. if act_selected == act_x_disc_all:
  262. self.triggerDisconnect(conn_list)
  263. elif act_selected == act_x_info:
  264. canvas.callback(ACTION_PORT_INFO, self.m_group_id, self.m_port_id, "")
  265. elif act_selected == act_x_rename:
  266. canvas.callback(ACTION_PORT_RENAME, self.m_group_id, self.m_port_id, "")
  267. def setPortSelected(self, yesno):
  268. for connection in canvas.connection_list:
  269. if (
  270. (connection.group_out_id == self.m_group_id and
  271. connection.port_out_id == self.m_port_id)
  272. or
  273. (connection.group_in_id == self.m_group_id and
  274. connection.port_in_id == self.m_port_id)
  275. ):
  276. connection.widget.updateLineSelected()
  277. def itemChange(self, change, value):
  278. if change == QGraphicsItem.ItemSelectedHasChanged:
  279. self.setPortSelected(value)
  280. return QGraphicsItem.itemChange(self, change, value)
  281. def triggerDisconnect(self, conn_list=None):
  282. if not conn_list:
  283. conn_list = CanvasGetPortConnectionList(self.m_group_id, self.m_port_id)
  284. for conn_id, group_id, port_id in conn_list:
  285. canvas.callback(ACTION_PORTS_DISCONNECT, conn_id, 0, "")
  286. def boundingRect(self):
  287. return QRectF(0, 0, self.m_port_width + 12, self.m_port_height)
  288. def paint(self, painter, option, widget):
  289. painter.save()
  290. painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))
  291. selected = self.isSelected()
  292. theme = canvas.theme
  293. if self.m_port_type == PORT_TYPE_AUDIO_JACK:
  294. poly_color = theme.port_audio_jack_bg_sel if selected else theme.port_audio_jack_bg
  295. poly_pen = theme.port_audio_jack_pen_sel if selected else theme.port_audio_jack_pen
  296. text_pen = theme.port_audio_jack_text_sel if selected else theme.port_audio_jack_text
  297. conn_pen = QPen(theme.port_audio_jack_pen_sel)
  298. elif self.m_port_type == PORT_TYPE_MIDI_JACK:
  299. poly_color = theme.port_midi_jack_bg_sel if selected else theme.port_midi_jack_bg
  300. poly_pen = theme.port_midi_jack_pen_sel if selected else theme.port_midi_jack_pen
  301. text_pen = theme.port_midi_jack_text_sel if selected else theme.port_midi_jack_text
  302. conn_pen = QPen(theme.port_midi_jack_pen_sel)
  303. elif self.m_port_type == PORT_TYPE_MIDI_ALSA:
  304. poly_color = theme.port_midi_alsa_bg_sel if selected else theme.port_midi_alsa_bg
  305. poly_pen = theme.port_midi_alsa_pen_sel if selected else theme.port_midi_alsa_pen
  306. text_pen = theme.port_midi_alsa_text_sel if selected else theme.port_midi_alsa_text
  307. conn_pen = QPen(theme.port_midi_alsa_pen_sel)
  308. elif self.m_port_type == PORT_TYPE_PARAMETER:
  309. poly_color = theme.port_parameter_bg_sel if selected else theme.port_parameter_bg
  310. poly_pen = theme.port_parameter_pen_sel if selected else theme.port_parameter_pen
  311. text_pen = theme.port_parameter_text_sel if selected else theme.port_parameter_text
  312. conn_pen = QPen(theme.port_parameter_pen_sel)
  313. else:
  314. qCritical("PatchCanvas::CanvasPort.paint() - invalid port type '%s'" % port_type2str(self.m_port_type))
  315. painter.restore()
  316. return
  317. # To prevent quality worsening
  318. poly_pen = QPen(poly_pen)
  319. poly_pen.setWidthF(poly_pen.widthF() + 0.00001)
  320. if self.m_is_alternate:
  321. poly_color = poly_color.darker(180)
  322. #poly_pen.setColor(poly_pen.color().darker(110))
  323. #text_pen.setColor(text_pen.color()) #.darker(150))
  324. #conn_pen.setColor(conn_pen.color()) #.darker(150))
  325. lineHinting = poly_pen.widthF() / 2
  326. poly_locx = [0, 0, 0, 0, 0]
  327. poly_corner_xhinting = (float(canvas.theme.port_height)/2) % floor(float(canvas.theme.port_height)/2)
  328. if poly_corner_xhinting == 0:
  329. poly_corner_xhinting = 0.5 * (1 - 7 / (float(canvas.theme.port_height)/2))
  330. if self.m_port_mode == PORT_MODE_INPUT:
  331. text_pos = QPointF(3, canvas.theme.port_text_ypos)
  332. if canvas.theme.port_mode == Theme.THEME_PORT_POLYGON:
  333. poly_locx[0] = lineHinting
  334. poly_locx[1] = self.m_port_width + 5 - lineHinting
  335. poly_locx[2] = self.m_port_width + 12 - poly_corner_xhinting
  336. poly_locx[3] = self.m_port_width + 5 - lineHinting
  337. poly_locx[4] = lineHinting
  338. elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE:
  339. poly_locx[0] = lineHinting
  340. poly_locx[1] = self.m_port_width + 5 - lineHinting
  341. poly_locx[2] = self.m_port_width + 5 - lineHinting
  342. poly_locx[3] = self.m_port_width + 5 - lineHinting
  343. poly_locx[4] = lineHinting
  344. else:
  345. qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas.theme.port_mode)
  346. painter.restore()
  347. return
  348. elif self.m_port_mode == PORT_MODE_OUTPUT:
  349. text_pos = QPointF(9, canvas.theme.port_text_ypos)
  350. if canvas.theme.port_mode == Theme.THEME_PORT_POLYGON:
  351. poly_locx[0] = self.m_port_width + 12 - lineHinting
  352. poly_locx[1] = 7 + lineHinting
  353. poly_locx[2] = 0 + poly_corner_xhinting
  354. poly_locx[3] = 7 + lineHinting
  355. poly_locx[4] = self.m_port_width + 12 - lineHinting
  356. elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE:
  357. poly_locx[0] = self.m_port_width + 12 - lineHinting
  358. poly_locx[1] = 5 + lineHinting
  359. poly_locx[2] = 5 + lineHinting
  360. poly_locx[3] = 5 + lineHinting
  361. poly_locx[4] = self.m_port_width + 12 - lineHinting
  362. else:
  363. qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas.theme.port_mode)
  364. painter.restore()
  365. return
  366. else:
  367. qCritical("PatchCanvas::CanvasPort.paint() - invalid port mode '%s'" % port_mode2str(self.m_port_mode))
  368. painter.restore()
  369. return
  370. polygon = QPolygonF()
  371. polygon += QPointF(poly_locx[0], lineHinting)
  372. polygon += QPointF(poly_locx[1], lineHinting)
  373. polygon += QPointF(poly_locx[2], float(canvas.theme.port_height)/2)
  374. polygon += QPointF(poly_locx[3], canvas.theme.port_height - lineHinting)
  375. polygon += QPointF(poly_locx[4], canvas.theme.port_height - lineHinting)
  376. polygon += QPointF(poly_locx[0], lineHinting)
  377. if canvas.theme.port_bg_pixmap:
  378. portRect = polygon.boundingRect().adjusted(-lineHinting+1, -lineHinting+1, lineHinting-1, lineHinting-1)
  379. portPos = portRect.topLeft()
  380. painter.drawTiledPixmap(portRect, canvas.theme.port_bg_pixmap, portPos)
  381. else:
  382. painter.setBrush(poly_color) #.lighter(200))
  383. painter.setPen(poly_pen)
  384. painter.drawPolygon(polygon)
  385. painter.setPen(text_pen)
  386. painter.setFont(self.m_port_font)
  387. painter.drawText(text_pos, self.m_port_name)
  388. if canvas.theme.idx == Theme.THEME_OOSTUDIO and canvas.theme.port_bg_pixmap:
  389. conn_pen.setCosmetic(True)
  390. conn_pen.setWidthF(0.4)
  391. painter.setPen(conn_pen)
  392. if self.m_port_mode == PORT_MODE_INPUT:
  393. connLineX = portRect.left()+1
  394. else:
  395. connLineX = portRect.right()-1
  396. conn_path = QPainterPath()
  397. conn_path.addRect(QRectF(connLineX-1, portRect.top(), 2, portRect.height()))
  398. painter.fillPath(conn_path, conn_pen.brush())
  399. painter.drawLine(QLineF(connLineX, portRect.top(), connLineX, portRect.bottom()))
  400. painter.restore()
  401. # ------------------------------------------------------------------------------------------------------------