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.

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