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.

782 lines
28KB

  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 qt_compat import qt_config
  7. if qt_config == 5:
  8. from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, QT_VERSION, Qt, QPointF, QRectF, QTimer
  9. from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QImage, QLinearGradient, QPainter, QPen
  10. from PyQt5.QtSvg import QGraphicsSvgItem
  11. from PyQt5.QtWidgets import QGraphicsItem, QGraphicsObject, QMenu
  12. elif qt_config == 6:
  13. from PyQt6.QtCore import pyqtSignal, pyqtSlot, qCritical, QT_VERSION, Qt, QPointF, QRectF, QTimer
  14. from PyQt6.QtGui import QCursor, QFont, QFontMetrics, QImage, QLinearGradient, QPainter, QPen
  15. from PyQt6.QtSvgWidgets import QGraphicsSvgItem
  16. from PyQt6.QtWidgets import QGraphicsItem, QGraphicsObject, QMenu
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Backwards-compatible horizontalAdvance/width call, depending on Qt version
  19. def fontHorizontalAdvance(font, string):
  20. if QT_VERSION >= 0x50b00:
  21. return QFontMetrics(font).horizontalAdvance(string)
  22. return QFontMetrics(font).width(string)
  23. # ------------------------------------------------------------------------------------------------------------
  24. # Imports (Custom)
  25. from . import (
  26. canvas,
  27. features,
  28. options,
  29. port_dict_t,
  30. CanvasBoxType,
  31. ANTIALIASING_FULL,
  32. ACTION_PLUGIN_EDIT,
  33. ACTION_PLUGIN_SHOW_UI,
  34. ACTION_PLUGIN_CLONE,
  35. ACTION_PLUGIN_REMOVE,
  36. ACTION_PLUGIN_RENAME,
  37. ACTION_PLUGIN_REPLACE,
  38. ACTION_GROUP_INFO,
  39. ACTION_GROUP_JOIN,
  40. ACTION_GROUP_SPLIT,
  41. ACTION_GROUP_RENAME,
  42. ACTION_PORTS_DISCONNECT,
  43. ACTION_INLINE_DISPLAY,
  44. EYECANDY_FULL,
  45. PORT_MODE_NULL,
  46. PORT_MODE_INPUT,
  47. PORT_MODE_OUTPUT,
  48. PORT_TYPE_NULL,
  49. PORT_TYPE_AUDIO_JACK,
  50. PORT_TYPE_MIDI_ALSA,
  51. PORT_TYPE_MIDI_JACK,
  52. PORT_TYPE_PARAMETER,
  53. MAX_PLUGIN_ID_ALLOWED,
  54. )
  55. from .canvasboxshadow import CanvasBoxShadow
  56. from .canvasicon import CanvasIcon
  57. from .canvasport import CanvasPort
  58. from .theme import Theme
  59. from .utils import CanvasItemFX, CanvasGetFullPortName, CanvasGetPortConnectionList
  60. # ------------------------------------------------------------------------------------------------------------
  61. class cb_line_t(object):
  62. def __init__(self, line, connection_id):
  63. self.line = line
  64. self.connection_id = connection_id
  65. # ------------------------------------------------------------------------------------------------------------
  66. class CanvasBox(QGraphicsObject):
  67. # signals
  68. positionChanged = pyqtSignal(int, bool, int, int)
  69. # enums
  70. INLINE_DISPLAY_DISABLED = 0
  71. INLINE_DISPLAY_ENABLED = 1
  72. INLINE_DISPLAY_CACHED = 2
  73. def __init__(self, group_id, group_name, icon, parent=None):
  74. QGraphicsObject.__init__(self)
  75. self.setParentItem(parent)
  76. # Save Variables, useful for later
  77. self.m_group_id = group_id
  78. self.m_group_name = group_name
  79. # plugin Id, < 0 if invalid
  80. self.m_plugin_id = -1
  81. self.m_plugin_ui = False
  82. self.m_plugin_inline = self.INLINE_DISPLAY_DISABLED
  83. # Base Variables
  84. self.p_width = 50
  85. self.p_width_in = 0
  86. self.p_width_out = 0
  87. self.p_height = canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1
  88. self.m_last_pos = QPointF()
  89. self.m_split = False
  90. self.m_split_mode = PORT_MODE_NULL
  91. self.m_cursor_moving = False
  92. self.m_forced_split = False
  93. self.m_mouse_down = False
  94. self.m_inline_image = None
  95. self.m_inline_scaling = 1.0
  96. self.m_inline_first = True
  97. self.m_will_signal_pos_change = False
  98. self.m_port_list_ids = []
  99. self.m_connection_lines = []
  100. # Set Font
  101. self.m_font_name = QFont()
  102. self.m_font_name.setFamily(canvas.theme.box_font_name)
  103. self.m_font_name.setPixelSize(canvas.theme.box_font_size)
  104. self.m_font_name.setWeight(canvas.theme.box_font_state)
  105. self.m_font_port = QFont()
  106. self.m_font_port.setFamily(canvas.theme.port_font_name)
  107. self.m_font_port.setPixelSize(canvas.theme.port_font_size)
  108. self.m_font_port.setWeight(canvas.theme.port_font_state)
  109. # Icon
  110. if canvas.theme.box_use_icon:
  111. self.icon_svg = CanvasIcon(icon, self.m_group_name, self)
  112. else:
  113. self.icon_svg = None
  114. # Shadow
  115. if options.eyecandy and QT_VERSION >= 0x50c00:
  116. self.shadow = CanvasBoxShadow(self.toGraphicsObject())
  117. self.shadow.setFakeParent(self)
  118. self.setGraphicsEffect(self.shadow)
  119. else:
  120. self.shadow = None
  121. # Final touches
  122. self.setFlags(QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
  123. # Wait for at least 1 port
  124. if options.auto_hide_groups:
  125. self.setVisible(False)
  126. if options.auto_select_items:
  127. self.setAcceptHoverEvents(True)
  128. self.updatePositions()
  129. self.visibleChanged.connect(self.slot_signalPositionChangedLater)
  130. self.xChanged.connect(self.slot_signalPositionChangedLater)
  131. self.yChanged.connect(self.slot_signalPositionChangedLater)
  132. canvas.scene.addItem(self)
  133. QTimer.singleShot(0, self.fixPos)
  134. def getGroupId(self):
  135. return self.m_group_id
  136. def getGroupName(self):
  137. return self.m_group_name
  138. def isSplit(self):
  139. return self.m_split
  140. def getSplitMode(self):
  141. return self.m_split_mode
  142. def getPortCount(self):
  143. return len(self.m_port_list_ids)
  144. def getPortList(self):
  145. return self.m_port_list_ids
  146. def redrawInlineDisplay(self):
  147. if self.m_plugin_inline == self.INLINE_DISPLAY_CACHED:
  148. self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED
  149. self.update()
  150. def removeAsPlugin(self):
  151. #del self.m_inline_image
  152. #self.m_inline_image = None
  153. #self.m_inline_scaling = 1.0
  154. self.m_plugin_id = -1
  155. self.m_plugin_ui = False
  156. self.m_plugin_inline = self.INLINE_DISPLAY_DISABLED
  157. def setAsPlugin(self, plugin_id, hasUI, hasInlineDisplay):
  158. if hasInlineDisplay and not options.inline_displays:
  159. hasInlineDisplay = False
  160. if not hasInlineDisplay:
  161. self.m_inline_image = None
  162. self.m_inline_scaling = 1.0
  163. self.m_plugin_id = plugin_id
  164. self.m_plugin_ui = hasUI
  165. self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED if hasInlineDisplay else self.INLINE_DISPLAY_DISABLED
  166. self.update()
  167. def setIcon(self, icon):
  168. if self.icon_svg is not None:
  169. self.icon_svg.setIcon(icon, self.m_group_name)
  170. def setSplit(self, split, mode=PORT_MODE_NULL):
  171. self.m_split = split
  172. self.m_split_mode = mode
  173. def setGroupName(self, group_name):
  174. self.m_group_name = group_name
  175. self.updatePositions()
  176. def setShadowOpacity(self, opacity):
  177. if self.shadow is not None:
  178. self.shadow.setOpacity(opacity)
  179. def addPortFromGroup(self, port_id, port_mode, port_type, port_name, is_alternate):
  180. if len(self.m_port_list_ids) == 0:
  181. if options.auto_hide_groups:
  182. if options.eyecandy == EYECANDY_FULL:
  183. CanvasItemFX(self, True, False)
  184. self.blockSignals(True)
  185. self.setVisible(True)
  186. self.blockSignals(False)
  187. new_widget = CanvasPort(self.m_group_id, port_id, port_name, port_mode, port_type, is_alternate, self)
  188. port_dict = port_dict_t()
  189. port_dict.group_id = self.m_group_id
  190. port_dict.port_id = port_id
  191. port_dict.port_name = port_name
  192. port_dict.port_mode = port_mode
  193. port_dict.port_type = port_type
  194. port_dict.is_alternate = is_alternate
  195. port_dict.widget = new_widget
  196. self.m_port_list_ids.append(port_id)
  197. return new_widget
  198. def removePortFromGroup(self, port_id):
  199. if port_id in self.m_port_list_ids:
  200. self.m_port_list_ids.remove(port_id)
  201. else:
  202. qCritical("PatchCanvas::CanvasBox.removePort(%i) - unable to find port to remove" % port_id)
  203. return
  204. if len(self.m_port_list_ids) > 0:
  205. self.updatePositions()
  206. elif self.isVisible():
  207. if options.auto_hide_groups:
  208. if options.eyecandy == EYECANDY_FULL:
  209. CanvasItemFX(self, False, False)
  210. else:
  211. self.blockSignals(True)
  212. self.setVisible(False)
  213. self.blockSignals(False)
  214. def addLineFromGroup(self, line, connection_id):
  215. new_cbline = cb_line_t(line, connection_id)
  216. self.m_connection_lines.append(new_cbline)
  217. def removeLineFromGroup(self, connection_id):
  218. for connection in self.m_connection_lines:
  219. if connection.connection_id == connection_id:
  220. self.m_connection_lines.remove(connection)
  221. return
  222. qCritical("PatchCanvas::CanvasBox.removeLineFromGroup(%i) - unable to find line to remove" % connection_id)
  223. def checkItemPos(self):
  224. if canvas.size_rect.isNull():
  225. return
  226. pos = self.scenePos()
  227. if (canvas.size_rect.contains(pos) and
  228. canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))):
  229. return
  230. if pos.x() < canvas.size_rect.x():
  231. self.setPos(canvas.size_rect.x(), pos.y())
  232. elif pos.x() + self.p_width > canvas.size_rect.width():
  233. self.setPos(canvas.size_rect.width() - self.p_width, pos.y())
  234. pos = self.scenePos()
  235. if pos.y() < canvas.size_rect.y():
  236. self.setPos(pos.x(), canvas.size_rect.y())
  237. elif pos.y() + self.p_height > canvas.size_rect.height():
  238. self.setPos(pos.x(), canvas.size_rect.height() - self.p_height)
  239. def removeIconFromScene(self):
  240. if self.icon_svg is None:
  241. return
  242. item = self.icon_svg
  243. self.icon_svg = None
  244. canvas.scene.removeItem(item)
  245. del item
  246. def updatePositions(self):
  247. self.prepareGeometryChange()
  248. # Check Text Name size
  249. app_name_size = fontHorizontalAdvance(self.m_font_name, self.m_group_name) + 30
  250. self.p_width = max(50, app_name_size)
  251. # Get Port List
  252. port_list = []
  253. for port in canvas.port_list:
  254. if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids:
  255. port_list.append(port)
  256. if len(port_list) == 0:
  257. self.p_height = canvas.theme.box_header_height
  258. self.p_width_in = 0
  259. self.p_width_out = 0
  260. else:
  261. max_in_width = max_out_width = 0
  262. port_spacing = canvas.theme.port_height + canvas.theme.port_spacing
  263. # Get Max Box Width, vertical ports re-positioning
  264. port_types = (PORT_TYPE_AUDIO_JACK, PORT_TYPE_MIDI_JACK, PORT_TYPE_MIDI_ALSA, PORT_TYPE_PARAMETER)
  265. last_in_type = last_out_type = PORT_TYPE_NULL
  266. last_in_pos = last_out_pos = canvas.theme.box_header_height + canvas.theme.box_header_spacing
  267. for port_type in port_types:
  268. for port in port_list:
  269. if port.port_type != port_type:
  270. continue
  271. size = fontHorizontalAdvance(self.m_font_port, port.port_name)
  272. if port.port_mode == PORT_MODE_INPUT:
  273. max_in_width = max(max_in_width, size)
  274. if port.port_type != last_in_type:
  275. if last_in_type != PORT_TYPE_NULL:
  276. last_in_pos += canvas.theme.port_spacingT
  277. last_in_type = port.port_type
  278. port.widget.setY(last_in_pos)
  279. last_in_pos += port_spacing
  280. elif port.port_mode == PORT_MODE_OUTPUT:
  281. max_out_width = max(max_out_width, size)
  282. if port.port_type != last_out_type:
  283. if last_out_type != PORT_TYPE_NULL:
  284. last_out_pos += canvas.theme.port_spacingT
  285. last_out_type = port.port_type
  286. port.widget.setY(last_out_pos)
  287. last_out_pos += port_spacing
  288. self.p_width = max(self.p_width, 30 + max_in_width + max_out_width)
  289. self.p_width_in = max_in_width
  290. self.p_width_out = max_out_width
  291. self.p_height = max(last_in_pos, last_out_pos)
  292. self.p_height += max(canvas.theme.port_spacing, canvas.theme.port_spacingT) - canvas.theme.port_spacing
  293. self.p_height += canvas.theme.box_pen.width()
  294. self.repositionPorts(port_list)
  295. self.repaintLines(True)
  296. self.update()
  297. def repositionPorts(self, port_list = None):
  298. if port_list is None:
  299. port_list = []
  300. for port in canvas.port_list:
  301. if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids:
  302. port_list.append(port)
  303. # Horizontal ports re-positioning
  304. inX = canvas.theme.port_offset
  305. outX = self.p_width - self.p_width_out - canvas.theme.port_offset - 12
  306. for port in port_list:
  307. if port.port_mode == PORT_MODE_INPUT:
  308. port.widget.setX(inX)
  309. port.widget.setPortWidth(self.p_width_in)
  310. elif port.port_mode == PORT_MODE_OUTPUT:
  311. port.widget.setX(outX)
  312. port.widget.setPortWidth(self.p_width_out)
  313. def repaintLines(self, forced=False):
  314. if self.pos() != self.m_last_pos or forced:
  315. for connection in self.m_connection_lines:
  316. connection.line.updateLinePos()
  317. self.m_last_pos = self.pos()
  318. def resetLinesZValue(self):
  319. for connection in canvas.connection_list:
  320. if connection.port_out_id in self.m_port_list_ids and connection.port_in_id in self.m_port_list_ids:
  321. z_value = canvas.last_z_value
  322. else:
  323. z_value = canvas.last_z_value - 1
  324. connection.widget.setZValue(z_value)
  325. def triggerSignalPositionChanged(self):
  326. self.positionChanged.emit(self.m_group_id, self.m_split, int(self.x()), int(self.y()))
  327. self.m_will_signal_pos_change = False
  328. @pyqtSlot()
  329. def slot_signalPositionChangedLater(self):
  330. if self.m_will_signal_pos_change:
  331. return
  332. self.m_will_signal_pos_change = True
  333. QTimer.singleShot(0, self.triggerSignalPositionChanged)
  334. def type(self):
  335. return CanvasBoxType
  336. def contextMenuEvent(self, event):
  337. event.accept()
  338. menu = QMenu()
  339. # Conenct menu stuff
  340. connMenu = QMenu("Connect", menu)
  341. our_port_types = []
  342. our_port_outs = {
  343. PORT_TYPE_AUDIO_JACK: [],
  344. PORT_TYPE_MIDI_JACK: [],
  345. PORT_TYPE_MIDI_ALSA: [],
  346. PORT_TYPE_PARAMETER: [],
  347. }
  348. for port in canvas.port_list:
  349. if port.group_id != self.m_group_id:
  350. continue
  351. if port.port_mode != PORT_MODE_OUTPUT:
  352. continue
  353. if port.port_id not in self.m_port_list_ids:
  354. continue
  355. if port.port_type not in our_port_types:
  356. our_port_types.append(port.port_type)
  357. our_port_outs[port.port_type].append((port.group_id, port.port_id))
  358. if len(our_port_types) != 0:
  359. act_x_conn = None
  360. for group in canvas.group_list:
  361. if self.m_group_id == group.group_id:
  362. continue
  363. has_ports = False
  364. target_ports = {
  365. PORT_TYPE_AUDIO_JACK: [],
  366. PORT_TYPE_MIDI_JACK: [],
  367. PORT_TYPE_MIDI_ALSA: [],
  368. PORT_TYPE_PARAMETER: [],
  369. }
  370. for port in canvas.port_list:
  371. if port.group_id != group.group_id:
  372. continue
  373. if port.port_mode != PORT_MODE_INPUT:
  374. continue
  375. if port.port_type not in our_port_types:
  376. continue
  377. has_ports = True
  378. target_ports[port.port_type].append((port.group_id, port.port_id))
  379. if not has_ports:
  380. continue
  381. act_x_conn = connMenu.addAction(group.group_name)
  382. act_x_conn.setData((our_port_outs, target_ports))
  383. act_x_conn.triggered.connect(canvas.qobject.PortContextMenuConnect)
  384. if act_x_conn is None:
  385. act_x_disc = connMenu.addAction("Nothing to connect to")
  386. act_x_disc.setEnabled(False)
  387. else:
  388. act_x_disc = connMenu.addAction("No output ports")
  389. act_x_disc.setEnabled(False)
  390. # Disconnect menu stuff
  391. discMenu = QMenu("Disconnect", menu)
  392. conn_list = []
  393. conn_list_ids = []
  394. for port_id in self.m_port_list_ids:
  395. tmp_conn_list = CanvasGetPortConnectionList(self.m_group_id, port_id)
  396. for tmp_conn_id, tmp_group_id, tmp_port_id in tmp_conn_list:
  397. if tmp_conn_id not in conn_list_ids:
  398. conn_list.append((tmp_conn_id, tmp_group_id, tmp_port_id))
  399. conn_list_ids.append(tmp_conn_id)
  400. if len(conn_list) > 0:
  401. for conn_id, group_id, port_id in conn_list:
  402. act_x_disc = discMenu.addAction(CanvasGetFullPortName(group_id, port_id))
  403. act_x_disc.setData(conn_id)
  404. act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect)
  405. else:
  406. act_x_disc = discMenu.addAction("No connections")
  407. act_x_disc.setEnabled(False)
  408. menu.addMenu(connMenu)
  409. menu.addMenu(discMenu)
  410. act_x_disc_all = menu.addAction("Disconnect &All")
  411. act_x_sep1 = menu.addSeparator()
  412. act_x_info = menu.addAction("Info")
  413. act_x_rename = menu.addAction("Rename")
  414. act_x_sep2 = menu.addSeparator()
  415. act_x_split_join = menu.addAction("Join" if self.m_split else "Split")
  416. if not features.group_info:
  417. act_x_info.setVisible(False)
  418. if not features.group_rename:
  419. act_x_rename.setVisible(False)
  420. if not (features.group_info and features.group_rename):
  421. act_x_sep1.setVisible(False)
  422. if self.m_plugin_id >= 0 and self.m_plugin_id <= MAX_PLUGIN_ID_ALLOWED:
  423. menu.addSeparator()
  424. act_p_edit = menu.addAction("Edit")
  425. act_p_ui = menu.addAction("Show Custom UI")
  426. menu.addSeparator()
  427. act_p_clone = menu.addAction("Clone")
  428. act_p_rename = menu.addAction("Rename...")
  429. act_p_replace = menu.addAction("Replace...")
  430. act_p_remove = menu.addAction("Remove")
  431. if not self.m_plugin_ui:
  432. act_p_ui.setVisible(False)
  433. else:
  434. act_p_edit = act_p_ui = None
  435. act_p_clone = act_p_rename = None
  436. act_p_replace = act_p_remove = None
  437. haveIns = haveOuts = False
  438. for port in canvas.port_list:
  439. if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids:
  440. if port.port_mode == PORT_MODE_INPUT:
  441. haveIns = True
  442. elif port.port_mode == PORT_MODE_OUTPUT:
  443. haveOuts = True
  444. if not (self.m_split or bool(haveIns and haveOuts)):
  445. act_x_sep2.setVisible(False)
  446. act_x_split_join.setVisible(False)
  447. act_selected = menu.exec_(event.screenPos())
  448. if act_selected is None:
  449. pass
  450. elif act_selected == act_x_disc_all:
  451. for conn_id in conn_list_ids:
  452. canvas.callback(ACTION_PORTS_DISCONNECT, conn_id, 0, "")
  453. elif act_selected == act_x_info:
  454. canvas.callback(ACTION_GROUP_INFO, self.m_group_id, 0, "")
  455. elif act_selected == act_x_rename:
  456. canvas.callback(ACTION_GROUP_RENAME, self.m_group_id, 0, "")
  457. elif act_selected == act_x_split_join:
  458. if self.m_split:
  459. canvas.callback(ACTION_GROUP_JOIN, self.m_group_id, 0, "")
  460. else:
  461. canvas.callback(ACTION_GROUP_SPLIT, self.m_group_id, 0, "")
  462. elif act_selected == act_p_edit:
  463. canvas.callback(ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "")
  464. elif act_selected == act_p_ui:
  465. canvas.callback(ACTION_PLUGIN_SHOW_UI, self.m_plugin_id, 0, "")
  466. elif act_selected == act_p_clone:
  467. canvas.callback(ACTION_PLUGIN_CLONE, self.m_plugin_id, 0, "")
  468. elif act_selected == act_p_rename:
  469. canvas.callback(ACTION_PLUGIN_RENAME, self.m_plugin_id, 0, "")
  470. elif act_selected == act_p_replace:
  471. canvas.callback(ACTION_PLUGIN_REPLACE, self.m_plugin_id, 0, "")
  472. elif act_selected == act_p_remove:
  473. canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "")
  474. def keyPressEvent(self, event):
  475. if self.m_plugin_id >= 0 and event.key() == Qt.Key_Delete:
  476. event.accept()
  477. canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "")
  478. return
  479. QGraphicsObject.keyPressEvent(self, event)
  480. def hoverEnterEvent(self, event):
  481. if options.auto_select_items:
  482. if len(canvas.scene.selectedItems()) > 0:
  483. canvas.scene.clearSelection()
  484. self.setSelected(True)
  485. QGraphicsObject.hoverEnterEvent(self, event)
  486. def mouseDoubleClickEvent(self, event):
  487. if self.m_plugin_id >= 0:
  488. event.accept()
  489. canvas.callback(ACTION_PLUGIN_SHOW_UI if self.m_plugin_ui else ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "")
  490. return
  491. QGraphicsObject.mouseDoubleClickEvent(self, event)
  492. def mousePressEvent(self, event):
  493. if event.button() == Qt.MiddleButton or event.source() == Qt.MouseEventSynthesizedByApplication:
  494. event.ignore()
  495. return
  496. canvas.last_z_value += 1
  497. self.setZValue(canvas.last_z_value)
  498. self.resetLinesZValue()
  499. self.m_cursor_moving = False
  500. if event.button() == Qt.RightButton:
  501. event.accept()
  502. canvas.scene.clearSelection()
  503. self.setSelected(True)
  504. self.m_mouse_down = False
  505. return
  506. elif event.button() == Qt.LeftButton:
  507. if self.sceneBoundingRect().contains(event.scenePos()):
  508. self.m_mouse_down = True
  509. else:
  510. # FIXME: Check if still valid: Fix a weird Qt behaviour with right-click mouseMove
  511. self.m_mouse_down = False
  512. event.ignore()
  513. return
  514. else:
  515. self.m_mouse_down = False
  516. QGraphicsObject.mousePressEvent(self, event)
  517. def mouseMoveEvent(self, event):
  518. if self.m_mouse_down:
  519. if not self.m_cursor_moving:
  520. self.setCursor(QCursor(Qt.SizeAllCursor))
  521. self.m_cursor_moving = True
  522. self.repaintLines()
  523. QGraphicsObject.mouseMoveEvent(self, event)
  524. def mouseReleaseEvent(self, event):
  525. if self.m_cursor_moving:
  526. self.unsetCursor()
  527. QTimer.singleShot(0, self.fixPos)
  528. self.m_mouse_down = False
  529. self.m_cursor_moving = False
  530. QGraphicsObject.mouseReleaseEvent(self, event)
  531. def fixPos(self):
  532. self.blockSignals(True)
  533. self.setX(round(self.x()))
  534. self.setY(round(self.y()))
  535. self.blockSignals(False)
  536. def boundingRect(self):
  537. return QRectF(0, 0, self.p_width, self.p_height)
  538. def paint(self, painter, option, widget):
  539. painter.save()
  540. painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))
  541. rect = QRectF(0, 0, self.p_width, self.p_height)
  542. # Draw rectangle
  543. pen = QPen(canvas.theme.box_pen_sel if self.isSelected() else canvas.theme.box_pen)
  544. pen.setWidthF(pen.widthF() + 0.00001)
  545. painter.setPen(pen)
  546. lineHinting = pen.widthF() / 2
  547. if canvas.theme.box_bg_type == Theme.THEME_BG_GRADIENT:
  548. box_gradient = QLinearGradient(0, 0, 0, self.p_height)
  549. box_gradient.setColorAt(0, canvas.theme.box_bg_1)
  550. box_gradient.setColorAt(1, canvas.theme.box_bg_2)
  551. painter.setBrush(box_gradient)
  552. else:
  553. painter.setBrush(canvas.theme.box_bg_1)
  554. rect.adjust(lineHinting, lineHinting, -lineHinting, -lineHinting)
  555. painter.drawRect(rect)
  556. # Draw plugin inline display if supported
  557. self.paintInlineDisplay(painter)
  558. # Draw pixmap header
  559. rect.setHeight(canvas.theme.box_header_height)
  560. if canvas.theme.box_header_pixmap:
  561. painter.setPen(Qt.NoPen)
  562. painter.setBrush(canvas.theme.box_bg_2)
  563. # outline
  564. rect.adjust(lineHinting, lineHinting, -lineHinting, -lineHinting)
  565. painter.drawRect(rect)
  566. rect.adjust(1, 1, -1, 0)
  567. painter.drawTiledPixmap(rect, canvas.theme.box_header_pixmap, rect.topLeft())
  568. # Draw text
  569. painter.setFont(self.m_font_name)
  570. if self.isSelected():
  571. painter.setPen(canvas.theme.box_text_sel)
  572. else:
  573. painter.setPen(canvas.theme.box_text)
  574. if canvas.theme.box_use_icon:
  575. textPos = QPointF(25, canvas.theme.box_text_ypos)
  576. else:
  577. appNameSize = fontHorizontalAdvance(self.m_font_name, self.m_group_name)
  578. rem = self.p_width - appNameSize
  579. textPos = QPointF(rem/2, canvas.theme.box_text_ypos)
  580. painter.drawText(textPos, self.m_group_name)
  581. self.repaintLines()
  582. painter.restore()
  583. def paintInlineDisplay(self, painter):
  584. if self.m_plugin_inline == self.INLINE_DISPLAY_DISABLED:
  585. return
  586. if not options.inline_displays:
  587. return
  588. inwidth = self.p_width - 16 - self.p_width_in - self.p_width_out
  589. inheight = self.p_height - 3 - canvas.theme.box_header_height - canvas.theme.box_header_spacing - canvas.theme.port_spacing
  590. scaling = canvas.scene.getScaleFactor() * canvas.scene.getDevicePixelRatioF()
  591. if self.m_plugin_id >= 0 and self.m_plugin_id <= MAX_PLUGIN_ID_ALLOWED and (
  592. self.m_plugin_inline == self.INLINE_DISPLAY_ENABLED or self.m_inline_scaling != scaling):
  593. if self.m_inline_first:
  594. size = "%i:%i" % (int(50*scaling), int(50*scaling))
  595. else:
  596. size = "%i:%i" % (int(inwidth*scaling), int(inheight*scaling))
  597. data = canvas.callback(ACTION_INLINE_DISPLAY, self.m_plugin_id, 0, size)
  598. if data is None:
  599. return
  600. self.m_inline_image = QImage(data['data'], data['width'], data['height'], data['stride'],
  601. QImage.Format_ARGB32)
  602. self.m_inline_scaling = scaling
  603. self.m_plugin_inline = self.INLINE_DISPLAY_CACHED
  604. # make room for inline display, in a square shape
  605. if self.m_inline_first:
  606. self.m_inline_first = False
  607. aspectRatio = data['width'] / data['height']
  608. self.p_height = int(max(50*scaling, self.p_height))
  609. self.p_width += int(max(0, min((80 - 14)*scaling, (inheight-inwidth) * aspectRatio * scaling)))
  610. self.repositionPorts()
  611. self.repaintLines(True)
  612. self.update()
  613. return
  614. if self.m_inline_image is None:
  615. print("ERROR: inline display image is None for", self.m_plugin_id, self.m_group_name)
  616. return
  617. swidth = self.m_inline_image.width() / scaling
  618. sheight = self.m_inline_image.height() / scaling
  619. srcx = int(self.p_width_in + (self.p_width - self.p_width_in - self.p_width_out) / 2 - swidth / 2)
  620. srcy = int(canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1 + (inheight - sheight) / 2)
  621. painter.drawImage(QRectF(srcx, srcy, swidth, sheight), self.m_inline_image)
  622. # ------------------------------------------------------------------------------------------------------------