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.

769 lines
28KB

  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_will_signal_pos_change = False
  99. self.m_port_list_ids = []
  100. self.m_connection_lines = []
  101. # Set Font
  102. self.m_font_name = QFont()
  103. self.m_font_name.setFamily(canvas.theme.box_font_name)
  104. self.m_font_name.setPixelSize(canvas.theme.box_font_size)
  105. self.m_font_name.setWeight(canvas.theme.box_font_state)
  106. self.m_font_port = QFont()
  107. self.m_font_port.setFamily(canvas.theme.port_font_name)
  108. self.m_font_port.setPixelSize(canvas.theme.port_font_size)
  109. self.m_font_port.setWeight(canvas.theme.port_font_state)
  110. # Icon
  111. if canvas.theme.box_use_icon:
  112. self.icon_svg = CanvasIcon(icon, self.m_group_name, self)
  113. else:
  114. self.icon_svg = None
  115. # Shadow
  116. # FIXME FX on top of graphic items make them lose high-dpi
  117. # See https://bugreports.qt.io/browse/QTBUG-65035
  118. # FIXME Random crashes while using Qt's drop shadow, let's just disable it
  119. if False and options.eyecandy and canvas.scene.getDevicePixelRatioF() == 1.0:
  120. self.shadow = CanvasBoxShadow(self.toGraphicsObject())
  121. self.shadow.setFakeParent(self)
  122. self.setGraphicsEffect(self.shadow)
  123. else:
  124. self.shadow = None
  125. # Final touches
  126. self.setFlags(QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
  127. # Wait for at least 1 port
  128. if options.auto_hide_groups:
  129. self.setVisible(False)
  130. if options.auto_select_items:
  131. self.setAcceptHoverEvents(True)
  132. self.updatePositions()
  133. self.visibleChanged.connect(self.slot_signalPositionChangedLater)
  134. self.xChanged.connect(self.slot_signalPositionChangedLater)
  135. self.yChanged.connect(self.slot_signalPositionChangedLater)
  136. canvas.scene.addItem(self)
  137. QTimer.singleShot(0, self.fixPos)
  138. def getGroupId(self):
  139. return self.m_group_id
  140. def getGroupName(self):
  141. return self.m_group_name
  142. def isSplitted(self):
  143. return self.m_splitted
  144. def getSplittedMode(self):
  145. return self.m_splitted_mode
  146. def getPortCount(self):
  147. return len(self.m_port_list_ids)
  148. def getPortList(self):
  149. return self.m_port_list_ids
  150. def redrawInlineDisplay(self):
  151. if self.m_plugin_inline == self.INLINE_DISPLAY_CACHED:
  152. self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED
  153. self.update()
  154. def removeAsPlugin(self):
  155. #del self.m_inline_image
  156. #self.m_inline_data = None
  157. #self.m_inline_image = None
  158. #self.m_inline_scaling = 1.0
  159. self.m_plugin_id = -1
  160. self.m_plugin_ui = False
  161. #self.m_plugin_inline = self.INLINE_DISPLAY_DISABLED
  162. def setAsPlugin(self, plugin_id, hasUI, hasInlineDisplay):
  163. if hasInlineDisplay and not options.inline_displays:
  164. hasInlineDisplay = False
  165. if not hasInlineDisplay:
  166. del self.m_inline_image
  167. self.m_inline_data = None
  168. self.m_inline_image = None
  169. self.m_inline_scaling = 1.0
  170. self.m_plugin_id = plugin_id
  171. self.m_plugin_ui = hasUI
  172. self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED if hasInlineDisplay else self.INLINE_DISPLAY_DISABLED
  173. self.update()
  174. def setIcon(self, icon):
  175. if self.icon_svg is not None:
  176. self.icon_svg.setIcon(icon, self.m_group_name)
  177. def setSplit(self, split, mode=PORT_MODE_NULL):
  178. self.m_splitted = split
  179. self.m_splitted_mode = mode
  180. def setGroupName(self, group_name):
  181. self.m_group_name = group_name
  182. self.updatePositions()
  183. def setShadowOpacity(self, opacity):
  184. if self.shadow:
  185. self.shadow.setOpacity(opacity)
  186. def addPortFromGroup(self, port_id, port_mode, port_type, port_name, is_alternate):
  187. if len(self.m_port_list_ids) == 0:
  188. if options.auto_hide_groups:
  189. if options.eyecandy == EYECANDY_FULL:
  190. CanvasItemFX(self, True, False)
  191. self.setVisible(True)
  192. new_widget = CanvasPort(self.m_group_id, port_id, port_name, port_mode, port_type, is_alternate, self)
  193. port_dict = port_dict_t()
  194. port_dict.group_id = self.m_group_id
  195. port_dict.port_id = port_id
  196. port_dict.port_name = port_name
  197. port_dict.port_mode = port_mode
  198. port_dict.port_type = port_type
  199. port_dict.is_alternate = is_alternate
  200. port_dict.widget = new_widget
  201. self.m_port_list_ids.append(port_id)
  202. return new_widget
  203. def removePortFromGroup(self, port_id):
  204. if port_id in self.m_port_list_ids:
  205. self.m_port_list_ids.remove(port_id)
  206. else:
  207. qCritical("PatchCanvas::CanvasBox.removePort(%i) - unable to find port to remove" % port_id)
  208. return
  209. if len(self.m_port_list_ids) > 0:
  210. self.updatePositions()
  211. elif self.isVisible():
  212. if options.auto_hide_groups:
  213. if options.eyecandy == EYECANDY_FULL:
  214. CanvasItemFX(self, False, False)
  215. else:
  216. self.setVisible(False)
  217. def addLineFromGroup(self, line, connection_id):
  218. new_cbline = cb_line_t(line, connection_id)
  219. self.m_connection_lines.append(new_cbline)
  220. def removeLineFromGroup(self, connection_id):
  221. for connection in self.m_connection_lines:
  222. if connection.connection_id == connection_id:
  223. self.m_connection_lines.remove(connection)
  224. return
  225. qCritical("PatchCanvas::CanvasBox.removeLineFromGroup(%i) - unable to find line to remove" % connection_id)
  226. def checkItemPos(self, blockSignals):
  227. if canvas.size_rect.isNull():
  228. return
  229. pos = self.scenePos()
  230. if (canvas.size_rect.contains(pos) and
  231. canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))):
  232. return
  233. if blockSignals:
  234. self.blockSignals(True)
  235. if pos.x() < canvas.size_rect.x():
  236. self.setPos(canvas.size_rect.x(), pos.y())
  237. elif pos.x() + self.p_width > canvas.size_rect.width():
  238. self.setPos(canvas.size_rect.width() - self.p_width, pos.y())
  239. pos = self.scenePos()
  240. if pos.y() < canvas.size_rect.y():
  241. self.setPos(pos.x(), canvas.size_rect.y())
  242. elif pos.y() + self.p_height > canvas.size_rect.height():
  243. self.setPos(pos.x(), canvas.size_rect.height() - self.p_height)
  244. if blockSignals:
  245. self.blockSignals(False)
  246. def removeIconFromScene(self):
  247. if self.icon_svg is None:
  248. return
  249. item = self.icon_svg
  250. self.icon_svg = None
  251. canvas.scene.removeItem(item)
  252. del item
  253. def updatePositions(self):
  254. self.prepareGeometryChange()
  255. # Check Text Name size
  256. app_name_size = QFontMetrics(self.m_font_name).width(self.m_group_name) + 30
  257. self.p_width = max(200 if self.m_plugin_inline != self.INLINE_DISPLAY_DISABLED else 50, app_name_size)
  258. # Get Port List
  259. port_list = []
  260. for port in canvas.port_list:
  261. if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids:
  262. port_list.append(port)
  263. if len(port_list) == 0:
  264. self.p_height = canvas.theme.box_header_height
  265. self.p_width_in = 0
  266. self.p_width_out = 0
  267. else:
  268. max_in_width = max_out_width = 0
  269. port_spacing = canvas.theme.port_height + canvas.theme.port_spacing
  270. # Get Max Box Width, vertical ports re-positioning
  271. port_types = (PORT_TYPE_AUDIO_JACK, PORT_TYPE_MIDI_JACK, PORT_TYPE_MIDI_ALSA, PORT_TYPE_PARAMETER)
  272. last_in_type = last_out_type = PORT_TYPE_NULL
  273. last_in_pos = last_out_pos = canvas.theme.box_header_height + canvas.theme.box_header_spacing
  274. for port_type in port_types:
  275. for port in port_list:
  276. if port.port_type != port_type:
  277. continue
  278. size = QFontMetrics(self.m_font_port).width(port.port_name)
  279. if port.port_mode == PORT_MODE_INPUT:
  280. max_in_width = max(max_in_width, size)
  281. if port.port_type != last_in_type:
  282. if last_in_type != PORT_TYPE_NULL:
  283. last_in_pos += canvas.theme.port_spacingT
  284. last_in_type = port.port_type
  285. port.widget.setY(last_in_pos)
  286. last_in_pos += port_spacing
  287. elif port.port_mode == PORT_MODE_OUTPUT:
  288. max_out_width = max(max_out_width, size)
  289. if port.port_type != last_out_type:
  290. if last_out_type != PORT_TYPE_NULL:
  291. last_out_pos += canvas.theme.port_spacingT
  292. last_out_type = port.port_type
  293. port.widget.setY(last_out_pos)
  294. last_out_pos += port_spacing
  295. self.p_width = max(self.p_width, (100 if self.m_plugin_inline != self.INLINE_DISPLAY_DISABLED else 30) + max_in_width + max_out_width)
  296. self.p_width_in = max_in_width
  297. self.p_width_out = max_out_width
  298. #if self.m_plugin_inline:
  299. #self.p_width += 10
  300. # Horizontal ports re-positioning
  301. inX = canvas.theme.port_offset
  302. outX = self.p_width - max_out_width - canvas.theme.port_offset - 12
  303. for port_type in port_types:
  304. for port in port_list:
  305. if port.port_mode == PORT_MODE_INPUT:
  306. port.widget.setX(inX)
  307. port.widget.setPortWidth(max_in_width)
  308. elif port.port_mode == PORT_MODE_OUTPUT:
  309. port.widget.setX(outX)
  310. port.widget.setPortWidth(max_out_width)
  311. self.p_height = max(last_in_pos, last_out_pos)
  312. self.p_height += max(canvas.theme.port_spacing, canvas.theme.port_spacingT) - canvas.theme.port_spacing
  313. self.p_height += canvas.theme.box_pen.widthF()
  314. self.repaintLines(True)
  315. self.update()
  316. def repaintLines(self, forced=False):
  317. if self.pos() != self.m_last_pos or forced:
  318. for connection in self.m_connection_lines:
  319. connection.line.updateLinePos()
  320. self.m_last_pos = self.pos()
  321. def resetLinesZValue(self):
  322. for connection in canvas.connection_list:
  323. if connection.port_out_id in self.m_port_list_ids and connection.port_in_id in self.m_port_list_ids:
  324. z_value = canvas.last_z_value
  325. else:
  326. z_value = canvas.last_z_value - 1
  327. connection.widget.setZValue(z_value)
  328. def triggerSignalPositionChanged(self):
  329. self.positionChanged.emit(self.m_group_id, self.m_splitted, self.x(), self.y())
  330. self.m_will_signal_pos_change = False
  331. @pyqtSlot()
  332. def slot_signalPositionChangedLater(self):
  333. if self.m_will_signal_pos_change:
  334. return
  335. self.m_will_signal_pos_change = True
  336. QTimer.singleShot(0, self.triggerSignalPositionChanged)
  337. def type(self):
  338. return CanvasBoxType
  339. def contextMenuEvent(self, event):
  340. event.accept()
  341. menu = QMenu()
  342. # Conenct menu stuff
  343. connMenu = QMenu("Connect", menu)
  344. our_port_types = []
  345. our_port_outs = {
  346. PORT_TYPE_AUDIO_JACK: [],
  347. PORT_TYPE_MIDI_JACK: [],
  348. PORT_TYPE_MIDI_ALSA: [],
  349. PORT_TYPE_PARAMETER: [],
  350. }
  351. for port in canvas.port_list:
  352. if port.group_id != self.m_group_id:
  353. continue
  354. if port.port_mode != PORT_MODE_OUTPUT:
  355. continue
  356. if port.port_id not in self.m_port_list_ids:
  357. continue
  358. if port.port_type not in our_port_types:
  359. our_port_types.append(port.port_type)
  360. our_port_outs[port.port_type].append((port.group_id, port.port_id))
  361. if len(our_port_types) != 0:
  362. act_x_conn = None
  363. for group in canvas.group_list:
  364. if self.m_group_id == group.group_id:
  365. continue
  366. has_ports = False
  367. target_ports = {
  368. PORT_TYPE_AUDIO_JACK: [],
  369. PORT_TYPE_MIDI_JACK: [],
  370. PORT_TYPE_MIDI_ALSA: [],
  371. PORT_TYPE_PARAMETER: [],
  372. }
  373. for port in canvas.port_list:
  374. if port.group_id != group.group_id:
  375. continue
  376. if port.port_mode != PORT_MODE_INPUT:
  377. continue
  378. if port.port_type not in our_port_types:
  379. continue
  380. has_ports = True
  381. target_ports[port.port_type].append((port.group_id, port.port_id))
  382. if not has_ports:
  383. continue
  384. act_x_conn = connMenu.addAction(group.group_name)
  385. act_x_conn.setData((our_port_outs, target_ports))
  386. act_x_conn.triggered.connect(canvas.qobject.PortContextMenuConnect)
  387. if act_x_conn is None:
  388. act_x_disc = connMenu.addAction("Nothing to connect to")
  389. act_x_disc.setEnabled(False)
  390. else:
  391. act_x_disc = connMenu.addAction("No output ports")
  392. act_x_disc.setEnabled(False)
  393. # Disconnect menu stuff
  394. discMenu = QMenu("Disconnect", menu)
  395. conn_list = []
  396. conn_list_ids = []
  397. for port_id in self.m_port_list_ids:
  398. tmp_conn_list = CanvasGetPortConnectionList(self.m_group_id, port_id)
  399. for tmp_conn_id, tmp_group_id, tmp_port_id in tmp_conn_list:
  400. if tmp_conn_id not in conn_list_ids:
  401. conn_list.append((tmp_conn_id, tmp_group_id, tmp_port_id))
  402. conn_list_ids.append(tmp_conn_id)
  403. if len(conn_list) > 0:
  404. for conn_id, group_id, port_id in conn_list:
  405. act_x_disc = discMenu.addAction(CanvasGetFullPortName(group_id, port_id))
  406. act_x_disc.setData(conn_id)
  407. act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect)
  408. else:
  409. act_x_disc = discMenu.addAction("No connections")
  410. act_x_disc.setEnabled(False)
  411. menu.addMenu(connMenu)
  412. menu.addMenu(discMenu)
  413. act_x_disc_all = menu.addAction("Disconnect &All")
  414. act_x_sep1 = menu.addSeparator()
  415. act_x_info = menu.addAction("Info")
  416. act_x_rename = menu.addAction("Rename")
  417. act_x_sep2 = menu.addSeparator()
  418. act_x_split_join = menu.addAction("Join" if self.m_splitted else "Split")
  419. if not features.group_info:
  420. act_x_info.setVisible(False)
  421. if not features.group_rename:
  422. act_x_rename.setVisible(False)
  423. if not (features.group_info and features.group_rename):
  424. act_x_sep1.setVisible(False)
  425. if self.m_plugin_id >= 0 and self.m_plugin_id <= MAX_PLUGIN_ID_ALLOWED:
  426. menu.addSeparator()
  427. act_p_edit = menu.addAction("Edit")
  428. act_p_ui = menu.addAction("Show Custom UI")
  429. menu.addSeparator()
  430. act_p_clone = menu.addAction("Clone")
  431. act_p_rename = menu.addAction("Rename...")
  432. act_p_replace = menu.addAction("Replace...")
  433. act_p_remove = menu.addAction("Remove")
  434. if not self.m_plugin_ui:
  435. act_p_ui.setVisible(False)
  436. else:
  437. act_p_edit = act_p_ui = None
  438. act_p_clone = act_p_rename = None
  439. act_p_replace = act_p_remove = None
  440. haveIns = haveOuts = False
  441. for port in canvas.port_list:
  442. if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids:
  443. if port.port_mode == PORT_MODE_INPUT:
  444. haveIns = True
  445. elif port.port_mode == PORT_MODE_OUTPUT:
  446. haveOuts = True
  447. if not (self.m_splitted or bool(haveIns and haveOuts)):
  448. act_x_sep2.setVisible(False)
  449. act_x_split_join.setVisible(False)
  450. act_selected = menu.exec_(event.screenPos())
  451. if act_selected is None:
  452. pass
  453. elif act_selected == act_x_disc_all:
  454. for conn_id in conn_list_ids:
  455. canvas.callback(ACTION_PORTS_DISCONNECT, conn_id, 0, "")
  456. elif act_selected == act_x_info:
  457. canvas.callback(ACTION_GROUP_INFO, self.m_group_id, 0, "")
  458. elif act_selected == act_x_rename:
  459. canvas.callback(ACTION_GROUP_RENAME, self.m_group_id, 0, "")
  460. elif act_selected == act_x_split_join:
  461. if self.m_splitted:
  462. canvas.callback(ACTION_GROUP_JOIN, self.m_group_id, 0, "")
  463. else:
  464. canvas.callback(ACTION_GROUP_SPLIT, self.m_group_id, 0, "")
  465. elif act_selected == act_p_edit:
  466. canvas.callback(ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "")
  467. elif act_selected == act_p_ui:
  468. canvas.callback(ACTION_PLUGIN_SHOW_UI, self.m_plugin_id, 0, "")
  469. elif act_selected == act_p_clone:
  470. canvas.callback(ACTION_PLUGIN_CLONE, self.m_plugin_id, 0, "")
  471. elif act_selected == act_p_rename:
  472. canvas.callback(ACTION_PLUGIN_RENAME, self.m_plugin_id, 0, "")
  473. elif act_selected == act_p_replace:
  474. canvas.callback(ACTION_PLUGIN_REPLACE, self.m_plugin_id, 0, "")
  475. elif act_selected == act_p_remove:
  476. canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "")
  477. def keyPressEvent(self, event):
  478. if self.m_plugin_id >= 0 and event.key() == Qt.Key_Delete:
  479. event.accept()
  480. canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "")
  481. return
  482. QGraphicsObject.keyPressEvent(self, event)
  483. def hoverEnterEvent(self, event):
  484. if options.auto_select_items:
  485. if len(canvas.scene.selectedItems()) > 0:
  486. canvas.scene.clearSelection()
  487. self.setSelected(True)
  488. QGraphicsObject.hoverEnterEvent(self, event)
  489. def mouseDoubleClickEvent(self, event):
  490. if self.m_plugin_id >= 0:
  491. event.accept()
  492. canvas.callback(ACTION_PLUGIN_SHOW_UI if self.m_plugin_ui else ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "")
  493. return
  494. QGraphicsObject.mouseDoubleClickEvent(self, event)
  495. def mousePressEvent(self, event):
  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 = QFontMetrics(self.m_font_name).width(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 - self.p_width_in - self.p_width_out - 16
  589. inheight = self.p_height - canvas.theme.box_header_height - canvas.theme.box_header_spacing - canvas.theme.port_spacing - 3
  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. size = "%i:%i" % (int(inwidth*scaling), int(inheight*scaling))
  594. data = canvas.callback(ACTION_INLINE_DISPLAY, self.m_plugin_id, 0, size)
  595. if data is None:
  596. return
  597. # invalidate old image first
  598. del self.m_inline_image
  599. self.m_inline_data = pack("%iB" % (data['height'] * data['stride']), *data['data'])
  600. self.m_inline_image = QImage(voidptr(self.m_inline_data), data['width'], data['height'], data['stride'], QImage.Format_ARGB32)
  601. self.m_inline_scaling = scaling
  602. self.m_plugin_inline = self.INLINE_DISPLAY_CACHED
  603. if self.m_inline_image is None:
  604. print("ERROR: inline display image is None for", self.m_plugin_id, self.m_group_name)
  605. return
  606. swidth = self.m_inline_image.width() / scaling
  607. sheight = self.m_inline_image.height() / scaling
  608. srcx = int(self.p_width_in + (self.p_width - self.p_width_in - self.p_width_out) / 2 - swidth / 2)
  609. srcy = int(canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1 + (inheight - sheight) / 2)
  610. painter.drawImage(QRectF(srcx, srcy, swidth, sheight), self.m_inline_image)
  611. # ------------------------------------------------------------------------------------------------------------