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 29KB

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