|  | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# PatchBay Canvas engine using QGraphicsView/Scene
# Copyright (C) 2010-2020 Filipe Coelho <falktx@falktx.com>
#
# 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 sip import voidptr
from struct import pack
from PyQt5.QtCore import qCritical, Qt, QPointF, QRectF, QTimer
from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QImage, QLinearGradient, QPainter, QPen
from PyQt5.QtWidgets import QGraphicsItem, QMenu
# ------------------------------------------------------------------------------------------------------------
# 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(QGraphicsItem):
    INLINE_DISPLAY_DISABLED = 0
    INLINE_DISPLAY_ENABLED  = 1
    INLINE_DISPLAY_CACHED   = 2
    def __init__(self, group_id, group_name, icon, parent=None):
        QGraphicsItem.__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_splitted = False
        self.m_splitted_mode = PORT_MODE_NULL
        self.m_cursor_moving = False
        self.m_forced_split = False
        self.m_mouse_down = False
        self.m_inline_data = None
        self.m_inline_image = None
        self.m_inline_scaling = 1.0
        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
        # FIXME FX on top of graphic items make them lose high-dpi
        # See https://bugreports.qt.io/browse/QTBUG-65035
        if options.eyecandy and canvas.scene.getDevicePixelRatioF() == 1.0:
            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()
        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 isSplitted(self):
        return self.m_splitted
    def getSplittedMode(self):
        return self.m_splitted_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_data = None
        #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:
            del self.m_inline_image
            self.m_inline_data = None
            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_splitted = split
        self.m_splitted_mode = mode
    def setGroupName(self, group_name):
        self.m_group_name = group_name
        self.updatePositions()
    def setShadowOpacity(self, opacity):
        if self.shadow:
            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.setVisible(True)
        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.setVisible(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 not canvas.size_rect.isNull():
            pos = self.scenePos()
            if not (canvas.size_rect.contains(pos) and
                    canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))):
                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 = QFontMetrics(self.m_font_name).width(self.m_group_name) + 30
        self.p_width = max(200 if self.m_plugin_inline != self.INLINE_DISPLAY_DISABLED else 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 = QFontMetrics(self.m_font_port).width(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, (100 if self.m_plugin_inline != self.INLINE_DISPLAY_DISABLED else 30) + max_in_width + max_out_width)
            self.p_width_in = max_in_width
            self.p_width_out = max_out_width
            #if self.m_plugin_inline:
                #self.p_width += 10
            # Horizontal ports re-positioning
            inX = canvas.theme.port_offset
            outX = self.p_width - max_out_width - canvas.theme.port_offset - 12
            for port_type in port_types:
                for port in port_list:
                    if port.port_mode == PORT_MODE_INPUT:
                        port.widget.setX(inX)
                        port.widget.setPortWidth(max_in_width)
                    elif port.port_mode == PORT_MODE_OUTPUT:
                        port.widget.setX(outX)
                        port.widget.setPortWidth(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.widthF()
        self.repaintLines(True)
        self.update()
    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 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_splitted 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_splitted 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_splitted:
                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
        QGraphicsItem.keyPressEvent(self, event)
    def hoverEnterEvent(self, event):
        if options.auto_select_items:
            if len(canvas.scene.selectedItems()) > 0:
                canvas.scene.clearSelection()
            self.setSelected(True)
        QGraphicsItem.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
        QGraphicsItem.mouseDoubleClickEvent(self, event)
    def mousePressEvent(self, event):
        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
        QGraphicsItem.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()
        QGraphicsItem.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
        QGraphicsItem.mouseReleaseEvent(self, event)
    def fixPos(self):
        self.setX(round(self.x()))
        self.setY(round(self.y()))
    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 = QFontMetrics(self.m_font_name).width(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 - self.p_width_in - self.p_width_out - 16
        inheight = self.p_height - canvas.theme.box_header_height - canvas.theme.box_header_spacing - canvas.theme.port_spacing - 3
        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):
            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
            # invalidate old image first
            del self.m_inline_image
            self.m_inline_data = pack("%iB" % (data['height'] * data['stride']), *data['data'])
            self.m_inline_image = QImage(voidptr(self.m_inline_data), data['width'], data['height'], data['stride'], QImage.Format_ARGB32)
            self.m_inline_scaling = scaling
            self.m_plugin_inline = self.INLINE_DISPLAY_CACHED
        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)
# ------------------------------------------------------------------------------------------------------------
 |