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.

800 lines
29KB

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