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.

737 lines
27KB

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