#!/usr/bin/env python3 # -*- coding: utf-8 -*- # PatchBay Canvas engine using QGraphicsView/Scene # Copyright (C) 2010-2021 Filipe Coelho # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a full copy of the GNU General Public License see the doc/GPL.txt file. # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, QT_VERSION, Qt, QPointF, QRectF, QTimer from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QImage, QLinearGradient, QPainter, QPen from PyQt5.QtWidgets import QGraphicsItem, QGraphicsObject, QMenu # ------------------------------------------------------------------------------------------------------------ # Backwards-compatible horizontalAdvance/width call, depending on Qt version def fontHorizontalAdvance(font, string): if QT_VERSION >= 0x50b00: return QFontMetrics(font).horizontalAdvance(string) return QFontMetrics(font).width(string) # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from . import ( canvas, features, options, port_dict_t, CanvasBoxType, ANTIALIASING_FULL, ACTION_PLUGIN_EDIT, ACTION_PLUGIN_SHOW_UI, ACTION_PLUGIN_CLONE, ACTION_PLUGIN_REMOVE, ACTION_PLUGIN_RENAME, ACTION_PLUGIN_REPLACE, ACTION_GROUP_INFO, ACTION_GROUP_JOIN, ACTION_GROUP_SPLIT, ACTION_GROUP_RENAME, ACTION_PORTS_DISCONNECT, ACTION_INLINE_DISPLAY, EYECANDY_FULL, PORT_MODE_NULL, PORT_MODE_INPUT, PORT_MODE_OUTPUT, PORT_TYPE_NULL, PORT_TYPE_AUDIO_JACK, PORT_TYPE_MIDI_ALSA, PORT_TYPE_MIDI_JACK, PORT_TYPE_PARAMETER, MAX_PLUGIN_ID_ALLOWED, ) from .canvasboxshadow import CanvasBoxShadow from .canvasicon import CanvasIcon from .canvasport import CanvasPort from .theme import Theme from .utils import CanvasItemFX, CanvasGetFullPortName, CanvasGetPortConnectionList # ------------------------------------------------------------------------------------------------------------ class cb_line_t(object): def __init__(self, line, connection_id): self.line = line self.connection_id = connection_id # ------------------------------------------------------------------------------------------------------------ class CanvasBox(QGraphicsObject): # signals positionChanged = pyqtSignal(int, bool, int, int) # enums INLINE_DISPLAY_DISABLED = 0 INLINE_DISPLAY_ENABLED = 1 INLINE_DISPLAY_CACHED = 2 def __init__(self, group_id, group_name, icon, parent=None): QGraphicsObject.__init__(self) self.setParentItem(parent) # Save Variables, useful for later self.m_group_id = group_id self.m_group_name = group_name # plugin Id, < 0 if invalid self.m_plugin_id = -1 self.m_plugin_ui = False self.m_plugin_inline = self.INLINE_DISPLAY_DISABLED # Base Variables self.p_width = 50 self.p_width_in = 0 self.p_width_out = 0 self.p_height = canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1 self.m_last_pos = QPointF() self.m_split = False self.m_split_mode = PORT_MODE_NULL self.m_cursor_moving = False self.m_forced_split = False self.m_mouse_down = False self.m_inline_image = None self.m_inline_scaling = 1.0 self.m_inline_first = True self.m_will_signal_pos_change = False self.m_port_list_ids = [] self.m_connection_lines = [] # Set Font self.m_font_name = QFont() self.m_font_name.setFamily(canvas.theme.box_font_name) self.m_font_name.setPixelSize(canvas.theme.box_font_size) self.m_font_name.setWeight(canvas.theme.box_font_state) self.m_font_port = QFont() self.m_font_port.setFamily(canvas.theme.port_font_name) self.m_font_port.setPixelSize(canvas.theme.port_font_size) self.m_font_port.setWeight(canvas.theme.port_font_state) # Icon if canvas.theme.box_use_icon: self.icon_svg = CanvasIcon(icon, self.m_group_name, self) else: self.icon_svg = None # Shadow if options.eyecandy and QT_VERSION >= 0x50c00: self.shadow = CanvasBoxShadow(self.toGraphicsObject()) self.shadow.setFakeParent(self) self.setGraphicsEffect(self.shadow) else: self.shadow = None # Final touches self.setFlags(QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) # Wait for at least 1 port if options.auto_hide_groups: self.setVisible(False) if options.auto_select_items: self.setAcceptHoverEvents(True) self.updatePositions() self.visibleChanged.connect(self.slot_signalPositionChangedLater) self.xChanged.connect(self.slot_signalPositionChangedLater) self.yChanged.connect(self.slot_signalPositionChangedLater) canvas.scene.addItem(self) QTimer.singleShot(0, self.fixPos) def getGroupId(self): return self.m_group_id def getGroupName(self): return self.m_group_name def isSplit(self): return self.m_split def getSplitMode(self): return self.m_split_mode def getPortCount(self): return len(self.m_port_list_ids) def getPortList(self): return self.m_port_list_ids def redrawInlineDisplay(self): if self.m_plugin_inline == self.INLINE_DISPLAY_CACHED: self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED self.update() def removeAsPlugin(self): #del self.m_inline_image #self.m_inline_image = None #self.m_inline_scaling = 1.0 self.m_plugin_id = -1 self.m_plugin_ui = False self.m_plugin_inline = self.INLINE_DISPLAY_DISABLED def setAsPlugin(self, plugin_id, hasUI, hasInlineDisplay): if hasInlineDisplay and not options.inline_displays: hasInlineDisplay = False if not hasInlineDisplay: self.m_inline_image = None self.m_inline_scaling = 1.0 self.m_plugin_id = plugin_id self.m_plugin_ui = hasUI self.m_plugin_inline = self.INLINE_DISPLAY_ENABLED if hasInlineDisplay else self.INLINE_DISPLAY_DISABLED self.update() def setIcon(self, icon): if self.icon_svg is not None: self.icon_svg.setIcon(icon, self.m_group_name) def setSplit(self, split, mode=PORT_MODE_NULL): self.m_split = split self.m_split_mode = mode def setGroupName(self, group_name): self.m_group_name = group_name self.updatePositions() def setShadowOpacity(self, opacity): if self.shadow is not None: self.shadow.setOpacity(opacity) def addPortFromGroup(self, port_id, port_mode, port_type, port_name, is_alternate): if len(self.m_port_list_ids) == 0: if options.auto_hide_groups: if options.eyecandy == EYECANDY_FULL: CanvasItemFX(self, True, False) self.blockSignals(True) self.setVisible(True) self.blockSignals(False) new_widget = CanvasPort(self.m_group_id, port_id, port_name, port_mode, port_type, is_alternate, self) port_dict = port_dict_t() port_dict.group_id = self.m_group_id port_dict.port_id = port_id port_dict.port_name = port_name port_dict.port_mode = port_mode port_dict.port_type = port_type port_dict.is_alternate = is_alternate port_dict.widget = new_widget self.m_port_list_ids.append(port_id) return new_widget def removePortFromGroup(self, port_id): if port_id in self.m_port_list_ids: self.m_port_list_ids.remove(port_id) else: qCritical("PatchCanvas::CanvasBox.removePort(%i) - unable to find port to remove" % port_id) return if len(self.m_port_list_ids) > 0: self.updatePositions() elif self.isVisible(): if options.auto_hide_groups: if options.eyecandy == EYECANDY_FULL: CanvasItemFX(self, False, False) else: self.blockSignals(True) self.setVisible(False) self.blockSignals(False) def addLineFromGroup(self, line, connection_id): new_cbline = cb_line_t(line, connection_id) self.m_connection_lines.append(new_cbline) def removeLineFromGroup(self, connection_id): for connection in self.m_connection_lines: if connection.connection_id == connection_id: self.m_connection_lines.remove(connection) return qCritical("PatchCanvas::CanvasBox.removeLineFromGroup(%i) - unable to find line to remove" % connection_id) def checkItemPos(self): if canvas.size_rect.isNull(): return pos = self.scenePos() if (canvas.size_rect.contains(pos) and canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))): return if pos.x() < canvas.size_rect.x(): self.setPos(canvas.size_rect.x(), pos.y()) elif pos.x() + self.p_width > canvas.size_rect.width(): self.setPos(canvas.size_rect.width() - self.p_width, pos.y()) pos = self.scenePos() if pos.y() < canvas.size_rect.y(): self.setPos(pos.x(), canvas.size_rect.y()) elif pos.y() + self.p_height > canvas.size_rect.height(): self.setPos(pos.x(), canvas.size_rect.height() - self.p_height) def removeIconFromScene(self): if self.icon_svg is None: return item = self.icon_svg self.icon_svg = None canvas.scene.removeItem(item) del item def updatePositions(self): self.prepareGeometryChange() # Check Text Name size app_name_size = fontHorizontalAdvance(self.m_font_name, self.m_group_name) + 30 self.p_width = max(50, app_name_size) # Get Port List port_list = [] for port in canvas.port_list: if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids: port_list.append(port) if len(port_list) == 0: self.p_height = canvas.theme.box_header_height self.p_width_in = 0 self.p_width_out = 0 else: max_in_width = max_out_width = 0 port_spacing = canvas.theme.port_height + canvas.theme.port_spacing # Get Max Box Width, vertical ports re-positioning port_types = (PORT_TYPE_AUDIO_JACK, PORT_TYPE_MIDI_JACK, PORT_TYPE_MIDI_ALSA, PORT_TYPE_PARAMETER) last_in_type = last_out_type = PORT_TYPE_NULL last_in_pos = last_out_pos = canvas.theme.box_header_height + canvas.theme.box_header_spacing for port_type in port_types: for port in port_list: if port.port_type != port_type: continue size = fontHorizontalAdvance(self.m_font_port, port.port_name) if port.port_mode == PORT_MODE_INPUT: max_in_width = max(max_in_width, size) if port.port_type != last_in_type: if last_in_type != PORT_TYPE_NULL: last_in_pos += canvas.theme.port_spacingT last_in_type = port.port_type port.widget.setY(last_in_pos) last_in_pos += port_spacing elif port.port_mode == PORT_MODE_OUTPUT: max_out_width = max(max_out_width, size) if port.port_type != last_out_type: if last_out_type != PORT_TYPE_NULL: last_out_pos += canvas.theme.port_spacingT last_out_type = port.port_type port.widget.setY(last_out_pos) last_out_pos += port_spacing self.p_width = max(self.p_width, 30 + max_in_width + max_out_width) self.p_width_in = max_in_width self.p_width_out = max_out_width self.p_height = max(last_in_pos, last_out_pos) self.p_height += max(canvas.theme.port_spacing, canvas.theme.port_spacingT) - canvas.theme.port_spacing self.p_height += canvas.theme.box_pen.width() self.repositionPorts(port_list) self.repaintLines(True) self.update() def repositionPorts(self, port_list = None): if port_list is None: port_list = [] for port in canvas.port_list: if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids: port_list.append(port) # Horizontal ports re-positioning inX = canvas.theme.port_offset outX = self.p_width - self.p_width_out - canvas.theme.port_offset - 12 for port in port_list: if port.port_mode == PORT_MODE_INPUT: port.widget.setX(inX) port.widget.setPortWidth(self.p_width_in) elif port.port_mode == PORT_MODE_OUTPUT: port.widget.setX(outX) port.widget.setPortWidth(self.p_width_out) def repaintLines(self, forced=False): if self.pos() != self.m_last_pos or forced: for connection in self.m_connection_lines: connection.line.updateLinePos() self.m_last_pos = self.pos() def resetLinesZValue(self): for connection in canvas.connection_list: if connection.port_out_id in self.m_port_list_ids and connection.port_in_id in self.m_port_list_ids: z_value = canvas.last_z_value else: z_value = canvas.last_z_value - 1 connection.widget.setZValue(z_value) def triggerSignalPositionChanged(self): self.positionChanged.emit(self.m_group_id, self.m_split, int(self.x()), int(self.y())) self.m_will_signal_pos_change = False @pyqtSlot() def slot_signalPositionChangedLater(self): if self.m_will_signal_pos_change: return self.m_will_signal_pos_change = True QTimer.singleShot(0, self.triggerSignalPositionChanged) def type(self): return CanvasBoxType def contextMenuEvent(self, event): event.accept() menu = QMenu() # Conenct menu stuff connMenu = QMenu("Connect", menu) our_port_types = [] our_port_outs = { PORT_TYPE_AUDIO_JACK: [], PORT_TYPE_MIDI_JACK: [], PORT_TYPE_MIDI_ALSA: [], PORT_TYPE_PARAMETER: [], } for port in canvas.port_list: if port.group_id != self.m_group_id: continue if port.port_mode != PORT_MODE_OUTPUT: continue if port.port_id not in self.m_port_list_ids: continue if port.port_type not in our_port_types: our_port_types.append(port.port_type) our_port_outs[port.port_type].append((port.group_id, port.port_id)) if len(our_port_types) != 0: act_x_conn = None for group in canvas.group_list: if self.m_group_id == group.group_id: continue has_ports = False target_ports = { PORT_TYPE_AUDIO_JACK: [], PORT_TYPE_MIDI_JACK: [], PORT_TYPE_MIDI_ALSA: [], PORT_TYPE_PARAMETER: [], } for port in canvas.port_list: if port.group_id != group.group_id: continue if port.port_mode != PORT_MODE_INPUT: continue if port.port_type not in our_port_types: continue has_ports = True target_ports[port.port_type].append((port.group_id, port.port_id)) if not has_ports: continue act_x_conn = connMenu.addAction(group.group_name) act_x_conn.setData((our_port_outs, target_ports)) act_x_conn.triggered.connect(canvas.qobject.PortContextMenuConnect) if act_x_conn is None: act_x_disc = connMenu.addAction("Nothing to connect to") act_x_disc.setEnabled(False) else: act_x_disc = connMenu.addAction("No output ports") act_x_disc.setEnabled(False) # Disconnect menu stuff discMenu = QMenu("Disconnect", menu) conn_list = [] conn_list_ids = [] for port_id in self.m_port_list_ids: tmp_conn_list = CanvasGetPortConnectionList(self.m_group_id, port_id) for tmp_conn_id, tmp_group_id, tmp_port_id in tmp_conn_list: if tmp_conn_id not in conn_list_ids: conn_list.append((tmp_conn_id, tmp_group_id, tmp_port_id)) conn_list_ids.append(tmp_conn_id) if len(conn_list) > 0: for conn_id, group_id, port_id in conn_list: act_x_disc = discMenu.addAction(CanvasGetFullPortName(group_id, port_id)) act_x_disc.setData(conn_id) act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect) else: act_x_disc = discMenu.addAction("No connections") act_x_disc.setEnabled(False) menu.addMenu(connMenu) menu.addMenu(discMenu) act_x_disc_all = menu.addAction("Disconnect &All") act_x_sep1 = menu.addSeparator() act_x_info = menu.addAction("Info") act_x_rename = menu.addAction("Rename") act_x_sep2 = menu.addSeparator() act_x_split_join = menu.addAction("Join" if self.m_split else "Split") if not features.group_info: act_x_info.setVisible(False) if not features.group_rename: act_x_rename.setVisible(False) if not (features.group_info and features.group_rename): act_x_sep1.setVisible(False) if self.m_plugin_id >= 0 and self.m_plugin_id <= MAX_PLUGIN_ID_ALLOWED: menu.addSeparator() act_p_edit = menu.addAction("Edit") act_p_ui = menu.addAction("Show Custom UI") menu.addSeparator() act_p_clone = menu.addAction("Clone") act_p_rename = menu.addAction("Rename...") act_p_replace = menu.addAction("Replace...") act_p_remove = menu.addAction("Remove") if not self.m_plugin_ui: act_p_ui.setVisible(False) else: act_p_edit = act_p_ui = None act_p_clone = act_p_rename = None act_p_replace = act_p_remove = None haveIns = haveOuts = False for port in canvas.port_list: if port.group_id == self.m_group_id and port.port_id in self.m_port_list_ids: if port.port_mode == PORT_MODE_INPUT: haveIns = True elif port.port_mode == PORT_MODE_OUTPUT: haveOuts = True if not (self.m_split or bool(haveIns and haveOuts)): act_x_sep2.setVisible(False) act_x_split_join.setVisible(False) act_selected = menu.exec_(event.screenPos()) if act_selected is None: pass elif act_selected == act_x_disc_all: for conn_id in conn_list_ids: canvas.callback(ACTION_PORTS_DISCONNECT, conn_id, 0, "") elif act_selected == act_x_info: canvas.callback(ACTION_GROUP_INFO, self.m_group_id, 0, "") elif act_selected == act_x_rename: canvas.callback(ACTION_GROUP_RENAME, self.m_group_id, 0, "") elif act_selected == act_x_split_join: if self.m_split: canvas.callback(ACTION_GROUP_JOIN, self.m_group_id, 0, "") else: canvas.callback(ACTION_GROUP_SPLIT, self.m_group_id, 0, "") elif act_selected == act_p_edit: canvas.callback(ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "") elif act_selected == act_p_ui: canvas.callback(ACTION_PLUGIN_SHOW_UI, self.m_plugin_id, 0, "") elif act_selected == act_p_clone: canvas.callback(ACTION_PLUGIN_CLONE, self.m_plugin_id, 0, "") elif act_selected == act_p_rename: canvas.callback(ACTION_PLUGIN_RENAME, self.m_plugin_id, 0, "") elif act_selected == act_p_replace: canvas.callback(ACTION_PLUGIN_REPLACE, self.m_plugin_id, 0, "") elif act_selected == act_p_remove: canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "") def keyPressEvent(self, event): if self.m_plugin_id >= 0 and event.key() == Qt.Key_Delete: event.accept() canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "") return QGraphicsObject.keyPressEvent(self, event) def hoverEnterEvent(self, event): if options.auto_select_items: if len(canvas.scene.selectedItems()) > 0: canvas.scene.clearSelection() self.setSelected(True) QGraphicsObject.hoverEnterEvent(self, event) def mouseDoubleClickEvent(self, event): if self.m_plugin_id >= 0: event.accept() canvas.callback(ACTION_PLUGIN_SHOW_UI if self.m_plugin_ui else ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "") return QGraphicsObject.mouseDoubleClickEvent(self, event) def mousePressEvent(self, event): if event.button() == Qt.MiddleButton or event.source() == Qt.MouseEventSynthesizedByApplication: event.ignore() return canvas.last_z_value += 1 self.setZValue(canvas.last_z_value) self.resetLinesZValue() self.m_cursor_moving = False if event.button() == Qt.RightButton: event.accept() canvas.scene.clearSelection() self.setSelected(True) self.m_mouse_down = False return elif event.button() == Qt.LeftButton: if self.sceneBoundingRect().contains(event.scenePos()): self.m_mouse_down = True else: # FIXME: Check if still valid: Fix a weird Qt behaviour with right-click mouseMove self.m_mouse_down = False event.ignore() return else: self.m_mouse_down = False QGraphicsObject.mousePressEvent(self, event) def mouseMoveEvent(self, event): if self.m_mouse_down: if not self.m_cursor_moving: self.setCursor(QCursor(Qt.SizeAllCursor)) self.m_cursor_moving = True self.repaintLines() QGraphicsObject.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event): if self.m_cursor_moving: self.unsetCursor() QTimer.singleShot(0, self.fixPos) self.m_mouse_down = False self.m_cursor_moving = False QGraphicsObject.mouseReleaseEvent(self, event) def fixPos(self): self.blockSignals(True) self.setX(round(self.x())) self.setY(round(self.y())) self.blockSignals(False) def boundingRect(self): return QRectF(0, 0, self.p_width, self.p_height) def paint(self, painter, option, widget): painter.save() painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL)) rect = QRectF(0, 0, self.p_width, self.p_height) # Draw rectangle pen = QPen(canvas.theme.box_pen_sel if self.isSelected() else canvas.theme.box_pen) pen.setWidthF(pen.widthF() + 0.00001) painter.setPen(pen) lineHinting = pen.widthF() / 2 if canvas.theme.box_bg_type == Theme.THEME_BG_GRADIENT: box_gradient = QLinearGradient(0, 0, 0, self.p_height) box_gradient.setColorAt(0, canvas.theme.box_bg_1) box_gradient.setColorAt(1, canvas.theme.box_bg_2) painter.setBrush(box_gradient) else: painter.setBrush(canvas.theme.box_bg_1) rect.adjust(lineHinting, lineHinting, -lineHinting, -lineHinting) painter.drawRect(rect) # Draw plugin inline display if supported self.paintInlineDisplay(painter) # Draw pixmap header rect.setHeight(canvas.theme.box_header_height) if canvas.theme.box_header_pixmap: painter.setPen(Qt.NoPen) painter.setBrush(canvas.theme.box_bg_2) # outline rect.adjust(lineHinting, lineHinting, -lineHinting, -lineHinting) painter.drawRect(rect) rect.adjust(1, 1, -1, 0) painter.drawTiledPixmap(rect, canvas.theme.box_header_pixmap, rect.topLeft()) # Draw text painter.setFont(self.m_font_name) if self.isSelected(): painter.setPen(canvas.theme.box_text_sel) else: painter.setPen(canvas.theme.box_text) if canvas.theme.box_use_icon: textPos = QPointF(25, canvas.theme.box_text_ypos) else: appNameSize = fontHorizontalAdvance(self.m_font_name, self.m_group_name) rem = self.p_width - appNameSize textPos = QPointF(rem/2, canvas.theme.box_text_ypos) painter.drawText(textPos, self.m_group_name) self.repaintLines() painter.restore() def paintInlineDisplay(self, painter): if self.m_plugin_inline == self.INLINE_DISPLAY_DISABLED: return if not options.inline_displays: return inwidth = self.p_width - 16 - self.p_width_in - self.p_width_out inheight = self.p_height - 3 - canvas.theme.box_header_height - canvas.theme.box_header_spacing - canvas.theme.port_spacing scaling = canvas.scene.getScaleFactor() * canvas.scene.getDevicePixelRatioF() if self.m_plugin_id >= 0 and self.m_plugin_id <= MAX_PLUGIN_ID_ALLOWED and ( self.m_plugin_inline == self.INLINE_DISPLAY_ENABLED or self.m_inline_scaling != scaling): if self.m_inline_first: size = "%i:%i" % (int(50*scaling), int(50*scaling)) else: size = "%i:%i" % (int(inwidth*scaling), int(inheight*scaling)) data = canvas.callback(ACTION_INLINE_DISPLAY, self.m_plugin_id, 0, size) if data is None: return self.m_inline_image = QImage(data['data'], data['width'], data['height'], data['stride'], QImage.Format_ARGB32) self.m_inline_scaling = scaling self.m_plugin_inline = self.INLINE_DISPLAY_CACHED # make room for inline display, in a square shape if self.m_inline_first: self.m_inline_first = False aspectRatio = data['width'] / data['height'] self.p_height = int(max(50*scaling, self.p_height)) self.p_width += int(max(0, min((80 - 14)*scaling, (inheight-inwidth) * aspectRatio * scaling))) self.repositionPorts() self.repaintLines(True) self.update() return if self.m_inline_image is None: print("ERROR: inline display image is None for", self.m_plugin_id, self.m_group_name) return swidth = self.m_inline_image.width() / scaling sheight = self.m_inline_image.height() / scaling srcx = int(self.p_width_in + (self.p_width - self.p_width_in - self.p_width_out) / 2 - swidth / 2) srcy = int(canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1 + (inheight - sheight) / 2) painter.drawImage(QRectF(srcx, srcy, swidth, sheight), self.m_inline_image) # ------------------------------------------------------------------------------------------------------------