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.

canvasbox.py 27KB

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