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.

485 lines
19KB

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