#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# PatchBay Canvas engine using QGraphicsView/Scene
# Copyright (C) 2010-2018 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 GPL.txt file

# Imports (Global)
if True:
    from PyQt5.QtCore import pyqtSignal, pyqtSlot, qDebug, qCritical, qFatal, qWarning, Qt, QObject
    from PyQt5.QtCore import QAbstractAnimation, QLineF, QPointF, QRectF, QSizeF, QSettings, QTimer
    from PyQt5.QtGui import QColor, QLinearGradient, QPen, QPolygonF, QPainter, QPainterPath
    from PyQt5.QtGui import QCursor, QFont, QFontMetrics
    from PyQt5.QtWidgets import QGraphicsScene, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem
    from PyQt5.QtWidgets import QGraphicsColorizeEffect, QGraphicsDropShadowEffect
    from PyQt5.QtWidgets import QInputDialog, QLineEdit, QMenu
    from PyQt5.QtSvg import QGraphicsSvgItem, QSvgRenderer
else:
    from PyQt4.QtCore import pyqtSignal, pyqtSlot, qDebug, qCritical, qFatal, qWarning, Qt, QObject
    from PyQt4.QtCore import QAbstractAnimation, QLineF, QPointF, QRectF, QSizeF, QSettings, QTimer
    from PyQt4.QtGui import QColor, QLinearGradient, QPen, QPolygonF, QPainter, QPainterPath
    from PyQt4.QtGui import QCursor, QFont, QFontMetrics
    from PyQt4.QtGui import QGraphicsScene, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem
    from PyQt4.QtGui import QGraphicsColorizeEffect, QGraphicsDropShadowEffect
    from PyQt4.QtGui import QInputDialog, QLineEdit, QMenu
    from PyQt4.QtSvg import QGraphicsSvgItem, QSvgRenderer

# Imports (Theme)
from patchcanvas_theme import *

# ------------------------------------------------------------------------------
# patchcanvas-api.h

# Port Mode
PORT_MODE_NULL   = 0
PORT_MODE_INPUT  = 1
PORT_MODE_OUTPUT = 2

# Port Type
PORT_TYPE_NULL       = 0
PORT_TYPE_AUDIO_JACK = 1
PORT_TYPE_MIDI_JACK  = 2
PORT_TYPE_MIDI_A2J   = 3
PORT_TYPE_MIDI_ALSA  = 4

# Callback Action
ACTION_GROUP_INFO       = 0 # group_id, N, N
ACTION_GROUP_RENAME     = 1 # group_id, N, new_name
ACTION_GROUP_SPLIT      = 2 # group_id, N, N
ACTION_GROUP_JOIN       = 3 # group_id, N, N
ACTION_PORT_INFO        = 4 # port_id, N, N
ACTION_PORT_RENAME      = 5 # port_id, N, new_name
ACTION_PORTS_CONNECT    = 6 # out_id, in_id, N
ACTION_PORTS_DISCONNECT = 7 # conn_id, N, N

# Icon
ICON_APPLICATION = 0
ICON_HARDWARE    = 1
ICON_DISTRHO     = 2
ICON_FILE        = 3
ICON_PLUGIN      = 4
ICON_LADISH_ROOM = 5

# Split Option
SPLIT_UNDEF = 0
SPLIT_NO    = 1
SPLIT_YES   = 2

# Antialiasing Option
ANTIALIASING_NONE  = 0
ANTIALIASING_SMALL = 1
ANTIALIASING_FULL  = 2

# Eye-Candy Option
EYECANDY_NONE  = 0
EYECANDY_SMALL = 1
EYECANDY_FULL  = 2

# Canvas options
class options_t(object):
    __slots__ = [
        'theme_name',
        'auto_hide_groups',
        'use_bezier_lines',
        'antialiasing',
        'eyecandy'
    ]

# Canvas features
class features_t(object):
    __slots__ = [
        'group_info',
        'group_rename',
        'port_info',
        'port_rename',
        'handle_group_pos'
    ]

# ------------------------------------------------------------------------------
# patchcanvas.h

# object types
CanvasBoxType           = QGraphicsItem.UserType + 1
CanvasIconType          = QGraphicsItem.UserType + 2
CanvasPortType          = QGraphicsItem.UserType + 3
CanvasLineType          = QGraphicsItem.UserType + 4
CanvasBezierLineType    = QGraphicsItem.UserType + 5
CanvasLineMovType       = QGraphicsItem.UserType + 6
CanvasBezierLineMovType = QGraphicsItem.UserType + 7

# object lists
class group_dict_t(object):
    __slots__ = [
        'group_id',
        'group_name',
        'split',
        'icon',
        'widgets'
    ]

class port_dict_t(object):
    __slots__ = [
        'group_id',
        'port_id',
        'port_name',
        'port_mode',
        'port_type',
        'widget'
    ]

class connection_dict_t(object):
    __slots__ = [
        'connection_id',
        'port_in_id',
        'port_out_id',
        'widget'
    ]

class animation_dict_t(object):
    __slots__ = [
        'animation',
        'item'
    ]

# Main Canvas object
class Canvas(object):
    __slots__ = [
        'scene',
        'callback',
        'debug',
        'last_z_value',
        'last_connection_id',
        'initial_pos',
        'size_rect',
        'group_list',
        'port_list',
        'connection_list',
        'animation_list',
        'qobject',
        'settings',
        'theme',
        'initiated'
    ]

# ------------------------------------------------------------------------------
# patchcanvas.cpp

class CanvasObject(QObject):
    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    @pyqtSlot()
    def AnimationIdle(self):
        animation = self.sender()
        if animation:
            CanvasRemoveAnimation(animation)

    @pyqtSlot()
    def AnimationHide(self):
        animation = self.sender()
        if animation:
            item = animation.item()
            if item:
                item.hide()
            CanvasRemoveAnimation(animation)

    @pyqtSlot()
    def AnimationDestroy(self):
        animation = self.sender()
        if animation:
            item = animation.item()
            if item:
                CanvasRemoveItemFX(item)
            CanvasRemoveAnimation(animation)

    @pyqtSlot()
    def PortContextMenuDisconnect(self):
        try:
            connectionId = int(self.sender().data())
        except:
            return

        CanvasCallback(ACTION_PORTS_DISCONNECT, connectionId, 0, "")

# Global objects
canvas = Canvas()
canvas.qobject    = None
canvas.settings   = None
canvas.theme      = None
canvas.initiated  = False
canvas.group_list = []
canvas.port_list  = []
canvas.connection_list = []
canvas.animation_list  = []

options = options_t()
options.theme_name = getDefaultThemeName()
options.auto_hide_groups = False
options.use_bezier_lines = True
options.antialiasing = ANTIALIASING_SMALL
options.eyecandy     = EYECANDY_SMALL

features = features_t()
features.group_info   = False
features.group_rename = False
features.port_info    = False
features.port_rename  = False
features.handle_group_pos = False

# Internal functions
def bool2str(check):
    return "True" if check else "False"

def port_mode2str(port_mode):
    if port_mode == PORT_MODE_NULL:
        return "PORT_MODE_NULL"
    elif port_mode == PORT_MODE_INPUT:
        return "PORT_MODE_INPUT"
    elif port_mode == PORT_MODE_OUTPUT:
        return "PORT_MODE_OUTPUT"
    else:
        return "PORT_MODE_???"

def port_type2str(port_type):
    if port_type == PORT_TYPE_NULL:
        return "PORT_TYPE_NULL"
    elif port_type == PORT_TYPE_AUDIO_JACK:
        return "PORT_TYPE_AUDIO_JACK"
    elif port_type == PORT_TYPE_MIDI_JACK:
        return "PORT_TYPE_MIDI_JACK"
    elif port_type == PORT_TYPE_MIDI_A2J:
        return "PORT_TYPE_MIDI_A2J"
    elif port_type == PORT_TYPE_MIDI_ALSA:
        return "PORT_TYPE_MIDI_ALSA"
    else:
        return "PORT_TYPE_???"

def icon2str(icon):
    if icon == ICON_HARDWARE:
        return "ICON_HARDWARE"
    elif ICON_APPLICATION:
        return "ICON_APPLICATION"
    elif ICON_LADISH_ROOM:
        return "ICON_LADISH_ROOM"
    else:
        return "ICON_???"

def split2str(split):
    if split == SPLIT_UNDEF:
        return "SPLIT_UNDEF"
    elif split == SPLIT_NO:
        return "SPLIT_NO"
    elif split == SPLIT_YES:
        return "SPLIT_YES"
    else:
        return "SPLIT_???"

# PatchCanvas API
def setOptions(new_options):
    if canvas.initiated: return
    options.theme_name       = new_options.theme_name
    options.auto_hide_groups = new_options.auto_hide_groups
    options.use_bezier_lines = new_options.use_bezier_lines
    options.antialiasing = new_options.antialiasing
    options.eyecandy     = new_options.eyecandy

def setFeatures(new_features):
    if canvas.initiated: return
    features.group_info   = new_features.group_info
    features.group_rename = new_features.group_rename
    features.port_info    = new_features.port_info
    features.port_rename  = new_features.port_rename
    features.handle_group_pos = new_features.handle_group_pos

def init(appName, scene, callback, debug=False):
    if debug:
        qDebug("PatchCanvas::init(\"%s\", %s, %s, %s)" % (appName, scene, callback, bool2str(debug)))

    if canvas.initiated:
        qCritical("PatchCanvas::init() - already initiated")
        return

    if not callback:
        qFatal("PatchCanvas::init() - fatal error: callback not set")
        return

    canvas.scene    = scene
    canvas.callback = callback
    canvas.debug    = debug

    canvas.last_z_value = 0
    canvas.last_connection_id = 0
    canvas.initial_pos = QPointF(0, 0)
    canvas.size_rect = QRectF()

    if not canvas.qobject:  canvas.qobject = CanvasObject()
    if not canvas.settings: canvas.settings = QSettings("falkTX", appName)

    if canvas.theme:
        del canvas.theme
        canvas.theme = None

    for i in range(Theme.THEME_MAX):
        this_theme_name = getThemeName(i)
        if this_theme_name == options.theme_name:
            canvas.theme = Theme(i)
            break

    if not canvas.theme:
        canvas.theme = Theme(getDefaultTheme())

    canvas.scene.updateTheme()

    canvas.initiated = True

def clear():
    if canvas.debug:
        qDebug("PatchCanvas::clear()")

    group_list_ids = []
    port_list_ids  = []
    connection_list_ids = []

    for group in canvas.group_list:
        group_list_ids.append(group.group_id)

    for port in canvas.port_list:
        port_list_ids.append(port.port_id)

    for connection in canvas.connection_list:
        connection_list_ids.append(connection.connection_id)

    for idx in connection_list_ids:
        disconnectPorts(idx)

    for idx in port_list_ids:
        removePort(idx)

    for idx in group_list_ids:
        removeGroup(idx)

    canvas.last_z_value = 0
    canvas.last_connection_id = 0

    canvas.group_list = []
    canvas.port_list = []
    canvas.connection_list = []

    canvas.scene.clear()

    canvas.initiated = False

def setInitialPos(x, y):
    if canvas.debug:
        qDebug("PatchCanvas::setInitialPos(%i, %i)" % (x, y))

    canvas.initial_pos.setX(x)
    canvas.initial_pos.setY(y)

def setCanvasSize(x, y, width, height):
    if canvas.debug:
        qDebug("PatchCanvas::setCanvasSize(%i, %i, %i, %i)" % (x, y, width, height))

    canvas.size_rect.setX(x)
    canvas.size_rect.setY(y)
    canvas.size_rect.setWidth(width)
    canvas.size_rect.setHeight(height)

def addGroup(group_id, group_name, split=SPLIT_UNDEF, icon=ICON_APPLICATION):
    if canvas.debug:
        qDebug("PatchCanvas::addGroup(%i, %s, %s, %s)" % (group_id, group_name.encode(), split2str(split), icon2str(icon)))

    for group in canvas.group_list:
        if group.group_id == group_id:
            qWarning("PatchCanvas::addGroup(%i, %s, %s, %s) - group already exists" % (group_id, group_name.encode(), split2str(split), icon2str(icon)))
            return

    if split == SPLIT_UNDEF and features.handle_group_pos:
        split = canvas.settings.value("CanvasPositions/%s_SPLIT" % group_name, split, type=int)

    group_box = CanvasBox(group_id, group_name, icon)

    group_dict = group_dict_t()
    group_dict.group_id = group_id
    group_dict.group_name = group_name
    group_dict.split = bool(split == SPLIT_YES)
    group_dict.icon = icon
    group_dict.widgets = [group_box, None]

    if split == SPLIT_YES:
        group_box.setSplit(True, PORT_MODE_OUTPUT)

        if features.handle_group_pos:
            group_box.setPos(canvas.settings.value("CanvasPositions/%s_OUTPUT" % group_name, CanvasGetNewGroupPos(), type=QPointF))
        else:
            group_box.setPos(CanvasGetNewGroupPos())

        group_sbox = CanvasBox(group_id, group_name, icon)
        group_sbox.setSplit(True, PORT_MODE_INPUT)

        group_dict.widgets[1] = group_sbox

        if features.handle_group_pos:
            group_sbox.setPos(canvas.settings.value("CanvasPositions/%s_INPUT" % group_name, CanvasGetNewGroupPos(True), type=QPointF))
        else:
            group_sbox.setPos(CanvasGetNewGroupPos(True))

        canvas.last_z_value += 1
        group_sbox.setZValue(canvas.last_z_value)

        if options.eyecandy == EYECANDY_FULL and not options.auto_hide_groups:
            CanvasItemFX(group_sbox, True)

        group_sbox.checkItemPos()

    else:
        group_box.setSplit(False)

        if features.handle_group_pos:
            group_box.setPos(canvas.settings.value("CanvasPositions/%s" % group_name, CanvasGetNewGroupPos(), type=QPointF))
        else:
            # Special ladish fake-split groups
            horizontal = bool(icon == ICON_HARDWARE or icon == ICON_LADISH_ROOM)
            group_box.setPos(CanvasGetNewGroupPos(horizontal))

    group_box.checkItemPos()

    canvas.last_z_value += 1
    group_box.setZValue(canvas.last_z_value)

    canvas.group_list.append(group_dict)

    if options.eyecandy == EYECANDY_FULL and not options.auto_hide_groups:
        CanvasItemFX(group_box, True)

    QTimer.singleShot(0, canvas.scene.update)

def removeGroup(group_id):
    if canvas.debug:
        qDebug("PatchCanvas::removeGroup(%i)" % group_id)

    for group in canvas.group_list:
        if group.group_id == group_id:
            item = group.widgets[0]
            group_name = group.group_name

            if group.split:
                s_item = group.widgets[1]

                if features.handle_group_pos:
                    canvas.settings.setValue("CanvasPositions/%s_OUTPUT" % group_name, item.pos())
                    canvas.settings.setValue("CanvasPositions/%s_INPUT" % group_name, s_item.pos())
                    canvas.settings.setValue("CanvasPositions/%s_SPLIT" % group_name, SPLIT_YES)

                if options.eyecandy == EYECANDY_FULL:
                    CanvasItemFX(s_item, False, True)
                else:
                    s_item.removeIconFromScene()
                    canvas.scene.removeItem(s_item)
                    del s_item

            else:
                if features.handle_group_pos:
                    canvas.settings.setValue("CanvasPositions/%s" % group_name, item.pos())
                    canvas.settings.setValue("CanvasPositions/%s_SPLIT" % group_name, SPLIT_NO)

            if options.eyecandy == EYECANDY_FULL:
                CanvasItemFX(item, False, True)
            else:
                item.removeIconFromScene()
                canvas.scene.removeItem(item)
                del item

            canvas.group_list.remove(group)

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::removeGroup(%i) - unable to find group to remove" % group_id)

def renameGroup(group_id, new_group_name):
    if canvas.debug:
        qDebug("PatchCanvas::renameGroup(%i, %s)" % (group_id, new_group_name.encode()))

    for group in canvas.group_list:
        if group.group_id == group_id:
            group.group_name = new_group_name
            group.widgets[0].setGroupName(new_group_name)

            if group.split and group.widgets[1]:
                group.widgets[1].setGroupName(new_group_name)

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::renameGroup(%i, %s) - unable to find group to rename" % (group_id, new_group_name.encode()))

def splitGroup(group_id):
    if canvas.debug:
        qDebug("PatchCanvas::splitGroup(%i)" % group_id)

    item = None
    group_name = ""
    group_icon = ICON_APPLICATION
    ports_data = []
    conns_data = []

    # Step 1 - Store all Item data
    for group in canvas.group_list:
        if group.group_id == group_id:
            if group.split:
                qCritical("PatchCanvas::splitGroup(%i) - group is already splitted" % group_id)
                return

            item = group.widgets[0]
            group_name = group.group_name
            group_icon = group.icon
            break

    if not item:
        qCritical("PatchCanvas::splitGroup(%i) - unable to find group to split" % group_id)
        return

    port_list_ids = list(item.getPortList())

    for port in canvas.port_list:
        if port.port_id in port_list_ids:
            port_dict = port_dict_t()
            port_dict.group_id = port.group_id
            port_dict.port_id = port.port_id
            port_dict.port_name = port.port_name
            port_dict.port_mode = port.port_mode
            port_dict.port_type = port.port_type
            port_dict.widget = None
            ports_data.append(port_dict)

    for connection in canvas.connection_list:
        if connection.port_out_id in port_list_ids or connection.port_in_id in port_list_ids:
            connection_dict = connection_dict_t()
            connection_dict.connection_id = connection.connection_id
            connection_dict.port_in_id = connection.port_in_id
            connection_dict.port_out_id = connection.port_out_id
            connection_dict.widget = None
            conns_data.append(connection_dict)

    # Step 2 - Remove Item and Children
    for conn in conns_data:
        disconnectPorts(conn.connection_id)

    for port_id in port_list_ids:
        removePort(port_id)

    removeGroup(group_id)

    # Step 3 - Re-create Item, now splitted
    addGroup(group_id, group_name, SPLIT_YES, group_icon)

    for port in ports_data:
        addPort(group_id, port.port_id, port.port_name, port.port_mode, port.port_type)

    for conn in conns_data:
        connectPorts(conn.connection_id, conn.port_out_id, conn.port_in_id)

    QTimer.singleShot(0, canvas.scene.update)

def joinGroup(group_id):
    if canvas.debug:
        qDebug("PatchCanvas::joinGroup(%i)" % group_id)

    item   = None
    s_item = None
    group_name = ""
    group_icon = ICON_APPLICATION
    ports_data = []
    conns_data = []

    # Step 1 - Store all Item data
    for group in canvas.group_list:
        if group.group_id == group_id:
            if not group.split:
                qCritical("PatchCanvas::joinGroup(%i) - group is not splitted" % group_id)
                return

            item = group.widgets[0]
            s_item = group.widgets[1]
            group_name = group.group_name
            group_icon = group.icon
            break

    # FIXME
    if not (item and s_item):
        qCritical("PatchCanvas::joinGroup(%i) - unable to find groups to join" % group_id)
        return

    port_list_ids = list(item.getPortList())
    port_list_idss = s_item.getPortList()

    for port_id in port_list_idss:
        if port_id not in port_list_ids:
            port_list_ids.append(port_id)

    for port in canvas.port_list:
        if port.port_id in port_list_ids:
            port_dict = port_dict_t()
            port_dict.group_id = port.group_id
            port_dict.port_id = port.port_id
            port_dict.port_name = port.port_name
            port_dict.port_mode = port.port_mode
            port_dict.port_type = port.port_type
            port_dict.widget = None
            ports_data.append(port_dict)

    for connection in canvas.connection_list:
        if connection.port_out_id in port_list_ids or connection.port_in_id in port_list_ids:
            connection_dict = connection_dict_t()
            connection_dict.connection_id = connection.connection_id
            connection_dict.port_in_id = connection.port_in_id
            connection_dict.port_out_id = connection.port_out_id
            connection_dict.widget = None
            conns_data.append(connection_dict)

    # Step 2 - Remove Item and Children
    for conn in conns_data:
        disconnectPorts(conn.connection_id)

    for port_id in port_list_ids:
        removePort(port_id)

    removeGroup(group_id)

    # Step 3 - Re-create Item, now together
    addGroup(group_id, group_name, SPLIT_NO, group_icon)

    for port in ports_data:
        addPort(group_id, port.port_id, port.port_name, port.port_mode, port.port_type)

    for conn in conns_data:
        connectPorts(conn.connection_id, conn.port_out_id, conn.port_in_id)

    QTimer.singleShot(0, canvas.scene.update)

def getGroupPos(group_id, port_mode=PORT_MODE_OUTPUT):
    if canvas.debug:
        qDebug("PatchCanvas::getGroupPos(%i, %s)" % (group_id, port_mode2str(port_mode)))

    for group in canvas.group_list:
        if group.group_id == group_id:
            if group.split:
                if port_mode == PORT_MODE_OUTPUT:
                    return group.widgets[0].pos()
                elif port_mode == PORT_MODE_INPUT:
                    return group.widgets[1].pos()
                else:
                    return QPointF(0, 0)
            else:
                return group.widgets[0].pos()

    qCritical("PatchCanvas::getGroupPos(%i, %s) - unable to find group" % (group_id, port_mode2str(port_mode)))
    return QPointF(0, 0)

def setGroupPos(group_id, group_pos_x, group_pos_y):
    setGroupPosFull(group_id, group_pos_x, group_pos_y, group_pos_x, group_pos_y)

def setGroupPosFull(group_id, group_pos_x_o, group_pos_y_o, group_pos_x_i, group_pos_y_i):
    if canvas.debug:
        qDebug("PatchCanvas::setGroupPos(%i, %i, %i, %i, %i)" % (group_id, group_pos_x_o, group_pos_y_o, group_pos_x_i, group_pos_y_i))

    for group in canvas.group_list:
        if group.group_id == group_id:
            group.widgets[0].setPos(group_pos_x_o, group_pos_y_o)

            if group.split and group.widgets[1]:
                group.widgets[1].setPos(group_pos_x_i, group_pos_y_i)

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::setGroupPos(%i, %i, %i, %i, %i) - unable to find group to reposition" % (group_id, group_pos_x_o, group_pos_y_o, group_pos_x_i, group_pos_y_i))

def setGroupIcon(group_id, icon):
    if canvas.debug:
        qDebug("PatchCanvas::setGroupIcon(%i, %s)" % (group_id, icon2str(icon)))

    for group in canvas.group_list:
        if group.group_id == group_id:
            group.icon = icon
            group.widgets[0].setIcon(icon)

            if group.split and group.widgets[1]:
                group.widgets[1].setIcon(icon)

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::setGroupIcon(%i, %s) - unable to find group to change icon" % (group_id, icon2str(icon)))

def addPort(group_id, port_id, port_name, port_mode, port_type):
    if canvas.debug:
        qDebug("PatchCanvas::addPort(%i, %i, %s, %s, %s)" % (group_id, port_id, port_name.encode(), port_mode2str(port_mode), port_type2str(port_type)))

    for port in canvas.port_list:
        if port.group_id == group_id and port.port_id == port_id:
            qWarning("PatchCanvas::addPort(%i, %i, %s, %s, %s) - port already exists" % (group_id, port_id, port_name.encode(), port_mode2str(port_mode), port_type2str(port_type)))
            return

    box_widget  = None
    port_widget = None

    for group in canvas.group_list:
        if group.group_id == group_id:
            if group.split and group.widgets[0].getSplittedMode() != port_mode and group.widgets[1]:
                n = 1
            else:
                n = 0
            box_widget  = group.widgets[n]
            port_widget = box_widget.addPortFromGroup(port_id, port_mode, port_type, port_name)
            break

    if not (box_widget and port_widget):
        qCritical("PatchCanvas::addPort(%i, %i, %s, %s, %s) - Unable to find parent group" % (group_id, port_id, port_name.encode(), port_mode2str(port_mode), port_type2str(port_type)))
        return

    if options.eyecandy == EYECANDY_FULL:
        CanvasItemFX(port_widget, True)

    port_dict = port_dict_t()
    port_dict.group_id  = 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.widget = port_widget
    canvas.port_list.append(port_dict)

    box_widget.updatePositions()

    QTimer.singleShot(0, canvas.scene.update)

def removePort(port_id):
    if canvas.debug:
        qDebug("PatchCanvas::removePort(%i)" % port_id)

    for port in canvas.port_list:
        if port.port_id == port_id:
            item = port.widget
            item.parentItem().removePortFromGroup(port_id)
            canvas.scene.removeItem(item)
            del item

            canvas.port_list.remove(port)

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::removePort(%i) - Unable to find port to remove" % port_id)

def renamePort(port_id, new_port_name):
    if canvas.debug:
        qDebug("PatchCanvas::renamePort(%i, %s)" % (port_id, new_port_name.encode()))

    for port in canvas.port_list:
        if port.port_id == port_id:
            port.port_name = new_port_name
            port.widget.setPortName(new_port_name)
            port.widget.parentItem().updatePositions()

            QTimer.singleShot(0, canvas.scene.update)
            return

    qCritical("PatchCanvas::renamePort(%i, %s) - Unable to find port to rename" % (port_id, new_port_name.encode()))

def connectPorts(connection_id, port_out_id, port_in_id):
    if canvas.debug:
        qDebug("PatchCanvas::connectPorts(%i, %i, %i)" % (connection_id, port_out_id, port_in_id))

    port_out = None
    port_in  = None
    port_out_parent = None
    port_in_parent  = None

    for port in canvas.port_list:
        if port.port_id == port_out_id:
            port_out = port.widget
            port_out_parent = port_out.parentItem()
        elif port.port_id == port_in_id:
            port_in = port.widget
            port_in_parent = port_in.parentItem()

    # FIXME
    if not (port_out and port_in):
        qCritical("PatchCanvas::connectPorts(%i, %i, %i) - unable to find ports to connect" % (connection_id, port_out_id, port_in_id))
        return

    connection_dict = connection_dict_t()
    connection_dict.connection_id = connection_id
    connection_dict.port_out_id = port_out_id
    connection_dict.port_in_id = port_in_id

    if options.use_bezier_lines:
        connection_dict.widget = CanvasBezierLine(port_out, port_in, None)
    else:
        connection_dict.widget = CanvasLine(port_out, port_in, None)

    canvas.scene.addItem(connection_dict.widget)

    port_out_parent.addLineFromGroup(connection_dict.widget, connection_id)
    port_in_parent.addLineFromGroup(connection_dict.widget, connection_id)

    canvas.last_z_value += 1
    port_out_parent.setZValue(canvas.last_z_value)
    port_in_parent.setZValue(canvas.last_z_value)

    canvas.last_z_value += 1
    connection_dict.widget.setZValue(canvas.last_z_value)

    canvas.connection_list.append(connection_dict)

    if options.eyecandy == EYECANDY_FULL:
        item = connection_dict.widget
        CanvasItemFX(item, True)

    QTimer.singleShot(0, canvas.scene.update)

def disconnectPorts(connection_id):
    if canvas.debug:
        qDebug("PatchCanvas::disconnectPorts(%i)" % connection_id)

    port_1_id = port_2_id = 0
    line = None
    item1 = None
    item2 = None

    for connection in canvas.connection_list:
        if connection.connection_id == connection_id:
            port_1_id = connection.port_out_id
            port_2_id = connection.port_in_id
            line = connection.widget
            canvas.connection_list.remove(connection)
            break

    if not line:
        qCritical("PatchCanvas::disconnectPorts(%i) - unable to find connection ports" % connection_id)
        return

    for port in canvas.port_list:
        if port.port_id == port_1_id:
            item1 = port.widget
            break

    if not item1:
        qCritical("PatchCanvas::disconnectPorts(%i) - unable to find output port" % connection_id)
        return

    for port in canvas.port_list:
        if port.port_id == port_2_id:
            item2 = port.widget
            break

    if not item2:
        qCritical("PatchCanvas::disconnectPorts(%i) - unable to find input port" % connection_id)
        return

    item1.parentItem().removeLineFromGroup(connection_id)
    item2.parentItem().removeLineFromGroup(connection_id)

    if options.eyecandy == EYECANDY_FULL:
        CanvasItemFX(line, False, True)
    else:
        line.deleteFromScene()

    QTimer.singleShot(0, canvas.scene.update)

def arrange():
    if canvas.debug:
        qDebug("PatchCanvas::arrange()")

def updateZValues():
    if canvas.debug:
        qDebug("PatchCanvas::updateZValues()")

    for group in canvas.group_list:
        group.widgets[0].resetLinesZValue()

        if group.split and group.widgets[1]:
            group.widgets[1].resetLinesZValue()

# Extra Internal functions

def CanvasGetGroupName(group_id):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetGroupName(%i)" % group_id)

    for group in canvas.group_list:
        if group.group_id == group_id:
            return group.group_name

    qCritical("PatchCanvas::CanvasGetGroupName(%i) - unable to find group" % group_id)
    return ""

def CanvasGetGroupPortCount(group_id):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetGroupPortCount(%i)" % group_id)

    port_count = 0
    for port in canvas.port_list:
        if port.group_id == group_id:
            port_count += 1

    return port_count

def CanvasGetNewGroupPos(horizontal=False):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetNewGroupPos(%s)" % bool2str(horizontal))

    new_pos = QPointF(canvas.initial_pos.x(), canvas.initial_pos.y())
    items = canvas.scene.items()

    break_loop = False
    while not break_loop:
        break_for = False
        for i in range(len(items)):
            item = items[i]
            if item and item.type() == CanvasBoxType:
                if item.sceneBoundingRect().contains(new_pos):
                    if horizontal:
                        new_pos += QPointF(item.boundingRect().width() + 15, 0)
                    else:
                        new_pos += QPointF(0, item.boundingRect().height() + 15)
                    break_for = True
                    break

            if i >= len(items) - 1 and not break_for:
                break_loop = True

    return new_pos

def CanvasGetFullPortName(port_id):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetFullPortName(%i)" % port_id)

    for port in canvas.port_list:
        if port.port_id == port_id:
            group_id = port.group_id
            for group in canvas.group_list:
                if group.group_id == group_id:
                    return group.group_name + ":" + port.port_name
            break

    qCritical("PatchCanvas::CanvasGetFullPortName(%i) - unable to find port" % port_id)
    return ""

def CanvasGetPortConnectionList(port_id):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetPortConnectionList(%i)" % port_id)

    port_con_list = []

    for connection in canvas.connection_list:
        if connection.port_out_id == port_id or connection.port_in_id == port_id:
            port_con_list.append(connection.connection_id)

    return port_con_list

def CanvasGetConnectedPort(connection_id, port_id):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasGetConnectedPort(%i, %i)" % (connection_id, port_id))

    for connection in canvas.connection_list:
        if connection.connection_id == connection_id:
            if connection.port_out_id == port_id:
                return connection.port_in_id
            else:
                return connection.port_out_id

    qCritical("PatchCanvas::CanvasGetConnectedPort(%i, %i) - unable to find connection" % (connection_id, port_id))
    return 0

def CanvasRemoveAnimation(f_animation):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasRemoveAnimation(%s)" % f_animation)

    for animation in canvas.animation_list:
        if animation.animation == f_animation:
            canvas.animation_list.remove(animation)
            del animation.animation
            break

def CanvasCallback(action, value1, value2, value_str):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasCallback(%i, %i, %i, %s)" % (action, value1, value2, value_str.encode()))

    canvas.callback(action, value1, value2, value_str)

def CanvasItemFX(item, show, destroy=False):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasItemFX(%s, %s, %s)" % (item, bool2str(show), bool2str(destroy)))

    # Check if item already has an animationItemFX
    for animation in canvas.animation_list:
        if animation.item == item:
            animation.animation.stop()
            canvas.animation_list.remove(animation)
            del animation.animation
            break

    animation = CanvasFadeAnimation(item, show)
    animation.setDuration(750 if show else 500)

    animation_dict = animation_dict_t()
    animation_dict.animation = animation
    animation_dict.item = item
    canvas.animation_list.append(animation_dict)

    if show:
        animation.finished.connect(canvas.qobject.AnimationIdle)
    else:
        if destroy:
            animation.finished.connect(canvas.qobject.AnimationDestroy)
        else:
            animation.finished.connect(canvas.qobject.AnimationHide)

    animation.start()

def CanvasRemoveItemFX(item):
    if canvas.debug:
        qDebug("PatchCanvas::CanvasRemoveItemFX(%s)" % item)

    if item.type() == CanvasBoxType:
        item.removeIconFromScene()
        canvas.scene.removeItem(item)

    elif item.type() == CanvasPortType:
        canvas.scene.removeItem(item)

    elif item.type() in (CanvasLineType, CanvasBezierLineType):
        pass #item.deleteFromScene()

    # Force deletion of item if needed
    if item.type() in (CanvasBoxType, CanvasPortType):
        del item

# ------------------------------------------------------------------------------
# patchscene.cpp

class PatchScene(QGraphicsScene):
    scaleChanged    = pyqtSignal(float)
    sceneGroupMoved = pyqtSignal(int, int, QPointF)

    def __init__(self, parent, view):
        QGraphicsScene.__init__(self, parent)

        self.m_ctrl_down = False
        self.m_mouse_down_init = False
        self.m_mouse_rubberband = False

        self.addRubberBand()

        self.m_view = view
        if not self.m_view:
            qFatal("PatchCanvas::PatchScene() - invalid view")

    def addRubberBand(self):
        self.m_rubberband = self.addRect(QRectF(0, 0, 0, 0))
        self.m_rubberband.setZValue(-1)
        self.m_rubberband.hide()
        self.m_rubberband_selection = False
        self.m_rubberband_orig_point = QPointF(0, 0)

    def clear(self):
        QGraphicsScene.clear(self)

        # Re-add rubberband, that just got deleted
        self.addRubberBand()

    def fixScaleFactor(self):
        scale = self.m_view.transform().m11()
        if scale > 3.0:
            self.m_view.resetTransform()
            self.m_view.scale(3.0, 3.0)
        elif scale < 0.2:
            self.m_view.resetTransform()
            self.m_view.scale(0.2, 0.2)
        self.scaleChanged.emit(self.m_view.transform().m11())

    def updateTheme(self):
        self.setBackgroundBrush(canvas.theme.canvas_bg)
        self.m_rubberband.setPen(canvas.theme.rubberband_pen)
        self.m_rubberband.setBrush(canvas.theme.rubberband_brush)

    def zoom_fit(self):
        min_x = min_y = max_x = max_y = None
        first_value = True

        items_list = self.items()

        if len(items_list) > 0:
            for item in items_list:
                if item and item.isVisible() and item.type() == CanvasBoxType:
                    pos = item.scenePos()
                    rect = item.boundingRect()

                    if first_value:
                        min_x = pos.x()
                    elif pos.x() < min_x:
                        min_x = pos.x()

                    if first_value:
                        min_y = pos.y()
                    elif pos.y() < min_y:
                        min_y = pos.y()

                    if first_value:
                        max_x = pos.x() + rect.width()
                    elif pos.x() + rect.width() > max_x:
                        max_x = pos.x() + rect.width()

                    if first_value:
                        max_y = pos.y() + rect.height()
                    elif pos.y() + rect.height() > max_y:
                        max_y = pos.y() + rect.height()

                    first_value = False

            if not first_value:
                self.m_view.fitInView(min_x, min_y, abs(max_x - min_x), abs(max_y - min_y), Qt.KeepAspectRatio)
                self.fixScaleFactor()

    def zoom_in(self):
        if self.m_view.transform().m11() < 3.0:
            self.m_view.scale(1.2, 1.2)
        self.scaleChanged.emit(self.m_view.transform().m11())

    def zoom_out(self):
        if self.m_view.transform().m11() > 0.2:
            self.m_view.scale(0.8, 0.8)
        self.scaleChanged.emit(self.m_view.transform().m11())

    def zoom_reset(self):
        self.m_view.resetTransform()
        self.scaleChanged.emit(1.0)

    def keyPressEvent(self, event):
        if not self.m_view:
            return event.ignore()

        if event.key() == Qt.Key_Control:
            self.m_ctrl_down = True

        elif event.key() == Qt.Key_Home:
            self.zoom_fit()
            return event.accept()

        elif self.m_ctrl_down:
            if event.key() == Qt.Key_Plus:
                self.zoom_in()
                return event.accept()
            elif event.key() == Qt.Key_Minus:
                self.zoom_out()
                return event.accept()
            elif event.key() == Qt.Key_1:
                self.zoom_reset()
                return event.accept()

        QGraphicsScene.keyPressEvent(self, event)

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Control:
            self.m_ctrl_down = False
        QGraphicsScene.keyReleaseEvent(self, event)

    def mousePressEvent(self, event):
        self.m_mouse_down_init  = bool(event.button() == Qt.LeftButton)
        self.m_mouse_rubberband = False
        QGraphicsScene.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.m_mouse_down_init:
            self.m_mouse_down_init  = False
            self.m_mouse_rubberband = bool(len(self.selectedItems()) == 0)

        if self.m_mouse_rubberband:
            if not self.m_rubberband_selection:
                self.m_rubberband.show()
                self.m_rubberband_selection = True
                self.m_rubberband_orig_point = event.scenePos()

            pos = event.scenePos()

            if pos.x() > self.m_rubberband_orig_point.x():
                x = self.m_rubberband_orig_point.x()
            else:
                x = pos.x()

            if pos.y() > self.m_rubberband_orig_point.y():
                y = self.m_rubberband_orig_point.y()
            else:
                y = pos.y()

            self.m_rubberband.setRect(x, y, abs(pos.x() - self.m_rubberband_orig_point.x()), abs(pos.y() - self.m_rubberband_orig_point.y()))
            return event.accept()

        QGraphicsScene.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.m_rubberband_selection:
            items_list = self.items()
            if len(items_list) > 0:
                for item in items_list:
                    if item and item.isVisible() and item.type() == CanvasBoxType:
                        item_rect = item.sceneBoundingRect()
                        item_top_left = QPointF(item_rect.x(), item_rect.y())
                        item_bottom_right = QPointF(item_rect.x() + item_rect.width(), item_rect.y() + item_rect.height())

                        if self.m_rubberband.contains(item_top_left) and self.m_rubberband.contains(item_bottom_right):
                            item.setSelected(True)

                self.m_rubberband.hide()
                self.m_rubberband.setRect(0, 0, 0, 0)
                self.m_rubberband_selection = False

        else:
            items_list = self.selectedItems()
            for item in items_list:
                if item and item.isVisible() and item.type() == CanvasBoxType:
                    item.checkItemPos()
                    self.sceneGroupMoved.emit(item.getGroupId(), item.getSplittedMode(), item.scenePos())

            if len(items_list) > 1:
                canvas.scene.update()

        self.m_mouse_down_init  = False
        self.m_mouse_rubberband = False
        QGraphicsScene.mouseReleaseEvent(self, event)

    def wheelEvent(self, event):
        if not self.m_view:
            return event.ignore()

        if self.m_ctrl_down:
            factor = 1.41 ** (event.delta() / 240.0)
            self.m_view.scale(factor, factor)

            self.fixScaleFactor()
            return event.accept()

        QGraphicsScene.wheelEvent(self, event)

# ------------------------------------------------------------------------------
# canvasfadeanimation.cpp

class CanvasFadeAnimation(QAbstractAnimation):
    def __init__(self, item, show, parent=None):
        QAbstractAnimation.__init__(self, parent)

        self.m_show = show
        self.m_duration = 0
        self.m_item = item

    def item(self):
        return self.m_item

    def setDuration(self, time):
        if self.m_item.opacity() == 0 and not self.m_show:
            self._duration = 0
        else:
            self.m_item.show()
            self.m_duration = time

    def duration(self):
        return self.m_duration

    def updateCurrentTime(self, time):
        if self.m_duration == 0:
            return

        if self.m_show:
            value = float(time) / self.m_duration
        else:
            value = 1.0 - (float(time) / self.m_duration)

        self.m_item.setOpacity(value)

        if self.m_item.type() == CanvasBoxType:
            self.m_item.setShadowOpacity(value)

    def updateDirection(self, direction):
        pass

    def updateState(self, oldState, newState):
        pass

# ------------------------------------------------------------------------------
# canvasline.cpp

class CanvasLine(QGraphicsLineItem):
    def __init__(self, item1, item2, parent):
        QGraphicsLineItem.__init__(self, parent)

        self.item1 = item1
        self.item2 = item2

        self.m_locked = False
        self.m_lineSelected = False

        self.setGraphicsEffect(None)
        self.updateLinePos()

    def deleteFromScene(self):
        canvas.scene.removeItem(self)
        del self

    def isLocked(self):
        return self.m_locked

    def setLocked(self, yesno):
        self.m_locked = yesno

    def isLineSelected(self):
        return self.m_lineSelected

    def setLineSelected(self, yesno):
        if self.m_locked:
            return

        if options.eyecandy == EYECANDY_FULL:
            if yesno:
                self.setGraphicsEffect(CanvasPortGlow(self.item1.getPortType(), self.toGraphicsObject()))
            else:
                self.setGraphicsEffect(None)

        self.m_lineSelected = yesno
        self.updateLineGradient()

    def updateLinePos(self):
        if self.item1.getPortMode() == PORT_MODE_OUTPUT:
            line = QLineF(self.item1.scenePos().x() + self.item1.getPortWidth() + 12,
                          self.item1.scenePos().y() + float(canvas.theme.port_height)/2,
                          self.item2.scenePos().x(),
                          self.item2.scenePos().y() + float(canvas.theme.port_height)/2)
            self.setLine(line)

            self.m_lineSelected = False
            self.updateLineGradient()

    def type(self):
        return CanvasLineType

    def updateLineGradient(self):
        pos_top = self.boundingRect().top()
        pos_bot = self.boundingRect().bottom()
        if self.item2.scenePos().y() >= self.item1.scenePos().y():
            pos1 = 0
            pos2 = 1
        else:
            pos1 = 1
            pos2 = 0

        port_type1 = self.item1.getPortType()
        port_type2 = self.item2.getPortType()
        port_gradient = QLinearGradient(0, pos_top, 0, pos_bot)

        if port_type1 == PORT_TYPE_AUDIO_JACK:
            port_gradient.setColorAt(pos1, canvas.theme.line_audio_jack_sel if self.m_lineSelected else canvas.theme.line_audio_jack)
        elif port_type1 == PORT_TYPE_MIDI_JACK:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_jack_sel if self.m_lineSelected else canvas.theme.line_midi_jack)
        elif port_type1 == PORT_TYPE_MIDI_A2J:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_a2j_sel if self.m_lineSelected else canvas.theme.line_midi_a2j)
        elif port_type1 == PORT_TYPE_MIDI_ALSA:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_alsa_sel if self.m_lineSelected else canvas.theme.line_midi_alsa)

        if port_type2 == PORT_TYPE_AUDIO_JACK:
            port_gradient.setColorAt(pos2, canvas.theme.line_audio_jack_sel if self.m_lineSelected else canvas.theme.line_audio_jack)
        elif port_type2 == PORT_TYPE_MIDI_JACK:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_jack_sel if self.m_lineSelected else canvas.theme.line_midi_jack)
        elif port_type2 == PORT_TYPE_MIDI_A2J:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_a2j_sel if self.m_lineSelected else canvas.theme.line_midi_a2j)
        elif port_type2 == PORT_TYPE_MIDI_ALSA:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_alsa_sel if self.m_lineSelected else canvas.theme.line_midi_alsa)

        self.setPen(QPen(port_gradient, 2))

    def paint(self, painter, option, widget):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing))
        QGraphicsLineItem.paint(self, painter, option, widget)
        painter.restore()

# ------------------------------------------------------------------------------
# canvasbezierline.cpp

class CanvasBezierLine(QGraphicsPathItem):
    def __init__(self, item1, item2, parent):
        QGraphicsPathItem.__init__(self, parent)

        self.item1 = item1
        self.item2 = item2

        self.m_locked = False
        self.m_lineSelected = False

        self.setBrush(QColor(0, 0, 0, 0))
        self.setGraphicsEffect(None)
        self.updateLinePos()

    def deleteFromScene(self):
        canvas.scene.removeItem(self)
        del self

    def isLocked(self):
        return self.m_locked

    def setLocked(self, yesno):
        self.m_locked = yesno

    def isLineSelected(self):
        return self.m_lineSelected

    def setLineSelected(self, yesno):
        if self.m_locked:
            return

        if options.eyecandy == EYECANDY_FULL:
            if yesno:
                self.setGraphicsEffect(CanvasPortGlow(self.item1.getPortType(), self.toGraphicsObject()))
            else:
                self.setGraphicsEffect(None)

        self.m_lineSelected = yesno
        self.updateLineGradient()

    def updateLinePos(self):
        if self.item1.getPortMode() == PORT_MODE_OUTPUT:
            item1_x = self.item1.scenePos().x() + self.item1.getPortWidth() + 12
            item1_y = self.item1.scenePos().y() + float(canvas.theme.port_height)/2

            item2_x = self.item2.scenePos().x()
            item2_y = self.item2.scenePos().y() + float(canvas.theme.port_height)/2

            item1_mid_x = abs(item1_x - item2_x) / 2
            item1_new_x = item1_x + item1_mid_x

            item2_mid_x = abs(item1_x - item2_x) / 2
            item2_new_x = item2_x - item2_mid_x

            path = QPainterPath(QPointF(item1_x, item1_y))
            path.cubicTo(item1_new_x, item1_y, item2_new_x, item2_y, item2_x, item2_y)
            self.setPath(path)

            self.m_lineSelected = False
            self.updateLineGradient()

    def type(self):
        return CanvasBezierLineType

    def updateLineGradient(self):
        pos_top = self.boundingRect().top()
        pos_bot = self.boundingRect().bottom()
        if self.item2.scenePos().y() >= self.item1.scenePos().y():
            pos1 = 0
            pos2 = 1
        else:
            pos1 = 1
            pos2 = 0

        port_type1 = self.item1.getPortType()
        port_type2 = self.item2.getPortType()
        port_gradient = QLinearGradient(0, pos_top, 0, pos_bot)

        if port_type1 == PORT_TYPE_AUDIO_JACK:
            port_gradient.setColorAt(pos1, canvas.theme.line_audio_jack_sel if self.m_lineSelected else canvas.theme.line_audio_jack)
        elif port_type1 == PORT_TYPE_MIDI_JACK:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_jack_sel if self.m_lineSelected else canvas.theme.line_midi_jack)
        elif port_type1 == PORT_TYPE_MIDI_A2J:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_a2j_sel if self.m_lineSelected else canvas.theme.line_midi_a2j)
        elif port_type1 == PORT_TYPE_MIDI_ALSA:
            port_gradient.setColorAt(pos1, canvas.theme.line_midi_alsa_sel if self.m_lineSelected else canvas.theme.line_midi_alsa)

        if port_type2 == PORT_TYPE_AUDIO_JACK:
            port_gradient.setColorAt(pos2, canvas.theme.line_audio_jack_sel if self.m_lineSelected else canvas.theme.line_audio_jack)
        elif port_type2 == PORT_TYPE_MIDI_JACK:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_jack_sel if self.m_lineSelected else canvas.theme.line_midi_jack)
        elif port_type2 == PORT_TYPE_MIDI_A2J:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_a2j_sel if self.m_lineSelected else canvas.theme.line_midi_a2j)
        elif port_type2 == PORT_TYPE_MIDI_ALSA:
            port_gradient.setColorAt(pos2, canvas.theme.line_midi_alsa_sel if self.m_lineSelected else canvas.theme.line_midi_alsa)

        self.setPen(QPen(port_gradient, 2))

    def paint(self, painter, option, widget):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing))
        QGraphicsPathItem.paint(self, painter, option, widget)
        painter.restore()

# ------------------------------------------------------------------------------
# canvaslivemov.cpp

class CanvasLineMov(QGraphicsLineItem):
    def __init__(self, port_mode, port_type, parent):
        QGraphicsLineItem.__init__(self, parent)

        self.m_port_mode = port_mode
        self.m_port_type = port_type

        # Port position doesn't change while moving around line
        self.p_lineX = self.scenePos().x()
        self.p_lineY = self.scenePos().y()
        self.p_width = self.parentItem().getPortWidth()

        if port_type == PORT_TYPE_AUDIO_JACK:
            pen = QPen(canvas.theme.line_audio_jack, 2)
        elif port_type == PORT_TYPE_MIDI_JACK:
            pen = QPen(canvas.theme.line_midi_jack, 2)
        elif port_type == PORT_TYPE_MIDI_A2J:
            pen = QPen(canvas.theme.line_midi_a2j, 2)
        elif port_type == PORT_TYPE_MIDI_ALSA:
            pen = QPen(canvas.theme.line_midi_alsa, 2)
        else:
            qWarning("PatchCanvas::CanvasLineMov(%s, %s, %s) - invalid port type" % (port_mode2str(port_mode), port_type2str(port_type), parent))
            pen = QPen(Qt.black)

        self.setPen(pen)

    def deleteFromScene(self):
        canvas.scene.removeItem(self)
        del self

    def updateLinePos(self, scenePos):
        item_pos = [0, 0]

        if self.m_port_mode == PORT_MODE_INPUT:
            item_pos[0] = 0
            item_pos[1] = float(canvas.theme.port_height)/2
        elif self.m_port_mode == PORT_MODE_OUTPUT:
            item_pos[0] = self.p_width + 12
            item_pos[1] = float(canvas.theme.port_height)/2
        else:
            return

        line = QLineF(item_pos[0], item_pos[1], scenePos.x() - self.p_lineX, scenePos.y() - self.p_lineY)
        self.setLine(line)

    def type(self):
        return CanvasLineMovType

    def paint(self, painter, option, widget):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing))
        QGraphicsLineItem.paint(self, painter, option, widget)
        painter.restore()

# ------------------------------------------------------------------------------
# canvasbezierlinemov.cpp

class CanvasBezierLineMov(QGraphicsPathItem):
    def __init__(self, port_mode, port_type, parent):
        QGraphicsPathItem.__init__(self, parent)

        self.m_port_mode = port_mode
        self.m_port_type = port_type

        # Port position doesn't change while moving around line
        self.p_itemX = self.scenePos().x()
        self.p_itemY = self.scenePos().y()
        self.p_width = self.parentItem().getPortWidth()

        if port_type == PORT_TYPE_AUDIO_JACK:
            pen = QPen(canvas.theme.line_audio_jack, 2)
        elif port_type == PORT_TYPE_MIDI_JACK:
            pen = QPen(canvas.theme.line_midi_jack, 2)
        elif port_type == PORT_TYPE_MIDI_A2J:
            pen = QPen(canvas.theme.line_midi_a2j, 2)
        elif port_type == PORT_TYPE_MIDI_ALSA:
            pen = QPen(canvas.theme.line_midi_alsa, 2)
        else:
            qWarning("PatchCanvas::CanvasBezierLineMov(%s, %s, %s) - invalid port type" % (port_mode2str(port_mode), port_type2str(port_type), parent))
            pen = QPen(Qt.black)

        self.setBrush(QColor(0, 0, 0, 0))
        self.setPen(pen)

    def deleteFromScene(self):
        canvas.scene.removeItem(self)
        del self

    def updateLinePos(self, scenePos):
        if self.m_port_mode == PORT_MODE_INPUT:
            old_x = 0
            old_y = float(canvas.theme.port_height)/2
            mid_x = abs(scenePos.x() - self.p_itemX) / 2
            new_x = old_x - mid_x
        elif self.m_port_mode == PORT_MODE_OUTPUT:
            old_x = self.p_width + 12
            old_y = float(canvas.theme.port_height)/2
            mid_x = abs(scenePos.x() - (self.p_itemX + old_x)) / 2
            new_x = old_x + mid_x
        else:
            return

        final_x = scenePos.x() - self.p_itemX
        final_y = scenePos.y() - self.p_itemY

        path = QPainterPath(QPointF(old_x, old_y))
        path.cubicTo(new_x, old_y, new_x, final_y, final_x, final_y)
        self.setPath(path)

    def type(self):
        return CanvasBezierLineMovType

    def paint(self, painter, option, widget):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing))
        QGraphicsPathItem.paint(self, painter, option, widget)
        painter.restore()

# ------------------------------------------------------------------------------
# canvasport.cpp

class CanvasPort(QGraphicsItem):
    def __init__(self, port_id, port_name, port_mode, port_type, parent):
        QGraphicsItem.__init__(self, parent)

        # Save Variables, useful for later
        self.m_port_id = port_id
        self.m_port_mode = port_mode
        self.m_port_type = port_type
        self.m_port_name = port_name

        # Base Variables
        self.m_port_width  = 15
        self.m_port_height = canvas.theme.port_height
        self.m_port_font = QFont(canvas.theme.port_font_name, canvas.theme.port_font_size, canvas.theme.port_font_state)

        self.m_line_mov = None
        self.m_hover_item = None
        self.m_last_selected_state = False

        self.m_mouse_down = False
        self.m_cursor_moving = False

        self.setFlags(QGraphicsItem.ItemIsSelectable)

    def getPortId(self):
        return self.m_port_id

    def getPortMode(self):
        return self.m_port_mode

    def getPortType(self):
        return self.m_port_type

    def getPortName(self):
        return self.m_port_name

    def getFullPortName(self):
        return self.parentItem().getGroupName() + ":" + self.m_port_name

    def getPortWidth(self):
        return self.m_port_width

    def getPortHeight(self):
        return self.m_port_height

    def setPortMode(self, port_mode):
        self.m_port_mode = port_mode
        self.update()

    def setPortType(self, port_type):
        self.m_port_type = port_type
        self.update()

    def setPortName(self, port_name):
        if QFontMetrics(self.m_port_font).width(port_name) < QFontMetrics(self.m_port_font).width(self.m_port_name):
            QTimer.singleShot(0, canvas.scene.update)

        self.m_port_name = port_name
        self.update()

    def setPortWidth(self, port_width):
        if port_width < self.m_port_width:
            QTimer.singleShot(0, canvas.scene.update)

        self.m_port_width = port_width
        self.update()

    def type(self):
        return CanvasPortType

    def mousePressEvent(self, event):
        self.m_hover_item = None
        self.m_mouse_down = bool(event.button() == Qt.LeftButton)
        self.m_cursor_moving = False
        QGraphicsItem.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if self.m_mouse_down:
            if not self.m_cursor_moving:
                self.setCursor(QCursor(Qt.CrossCursor))
                self.m_cursor_moving = True

                for connection in canvas.connection_list:
                    if connection.port_out_id == self.m_port_id or connection.port_in_id == self.m_port_id:
                        connection.widget.setLocked(True)

            if not self.m_line_mov:
                if options.use_bezier_lines:
                    self.m_line_mov = CanvasBezierLineMov(self.m_port_mode, self.m_port_type, self)
                else:
                    self.m_line_mov = CanvasLineMov(self.m_port_mode, self.m_port_type, self)

                canvas.last_z_value += 1
                self.m_line_mov.setZValue(canvas.last_z_value)
                canvas.last_z_value += 1
                self.parentItem().setZValue(canvas.last_z_value)

            item = None
            items = canvas.scene.items(event.scenePos(), Qt.ContainsItemShape, Qt.AscendingOrder)
            for i in range(len(items)):
                if items[i].type() == CanvasPortType:
                    if items[i] != self:
                        if not item:
                            item = items[i]
                        elif items[i].parentItem().zValue() > item.parentItem().zValue():
                            item = items[i]

            if self.m_hover_item and self.m_hover_item != item:
                self.m_hover_item.setSelected(False)

            if item:
                a2j_connection = bool(item.getPortType() == PORT_TYPE_MIDI_JACK and self.m_port_type == PORT_TYPE_MIDI_A2J) or (item.getPortType() == PORT_TYPE_MIDI_A2J and self.m_port_type == PORT_TYPE_MIDI_JACK)

                if item.getPortMode() != self.m_port_mode and (item.getPortType() == self.m_port_type or a2j_connection):
                    item.setSelected(True)
                    self.m_hover_item = item
                else:
                    self.m_hover_item = None
            else:
                self.m_hover_item = None

            self.m_line_mov.updateLinePos(event.scenePos())
            return event.accept()

        QGraphicsItem.mouseMoveEvent(self, event)

    def mouseReleaseEvent(self, event):
        if self.m_mouse_down:
            if self.m_line_mov:
                self.m_line_mov.deleteFromScene()
                self.m_line_mov = None

            for connection in canvas.connection_list:
                if connection.port_out_id == self.m_port_id or connection.port_in_id == self.m_port_id:
                    connection.widget.setLocked(False)

            if self.m_hover_item:
                check = False
                for connection in canvas.connection_list:
                    if ( (connection.port_out_id == self.m_port_id and connection.port_in_id == self.m_hover_item.getPortId()) or
                         (connection.port_out_id == self.m_hover_item.getPortId() and connection.port_in_id == self.m_port_id) ):
                        canvas.callback(ACTION_PORTS_DISCONNECT, connection.connection_id, 0, "")
                        check = True
                        break

                if not check:
                    if self.m_port_mode == PORT_MODE_OUTPUT:
                        canvas.callback(ACTION_PORTS_CONNECT, self.m_port_id, self.m_hover_item.getPortId(), "")
                    else:
                        canvas.callback(ACTION_PORTS_CONNECT, self.m_hover_item.getPortId(), self.m_port_id, "")

                canvas.scene.clearSelection()

        if self.m_cursor_moving:
            self.setCursor(QCursor(Qt.ArrowCursor))

        self.m_hover_item = None
        self.m_mouse_down = False
        self.m_cursor_moving = False
        QGraphicsItem.mouseReleaseEvent(self, event)

    def contextMenuEvent(self, event):
        canvas.scene.clearSelection()
        self.setSelected(True)

        menu = QMenu()
        discMenu = QMenu("Disconnect", menu)

        port_con_list = CanvasGetPortConnectionList(self.m_port_id)

        if len(port_con_list) > 0:
            for port_id in port_con_list:
                port_con_id = CanvasGetConnectedPort(port_id, self.m_port_id)
                act_x_disc = discMenu.addAction(CanvasGetFullPortName(port_con_id))
                act_x_disc.setData(port_id)
                act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect)
        else:
            act_x_disc = discMenu.addAction("No connections")
            act_x_disc.setEnabled(False)

        menu.addMenu(discMenu)
        act_x_disc_all = menu.addAction("Disconnect &All")
        act_x_sep_1 = menu.addSeparator()
        act_x_info = menu.addAction("Get &Info")
        act_x_rename = menu.addAction("&Rename")

        if not features.port_info:
            act_x_info.setVisible(False)

        if not features.port_rename:
            act_x_rename.setVisible(False)

        if not (features.port_info and features.port_rename):
            act_x_sep_1.setVisible(False)

        act_selected = menu.exec_(event.screenPos())

        if act_selected == act_x_disc_all:
            for port_id in port_con_list:
                canvas.callback(ACTION_PORTS_DISCONNECT, port_id, 0, "")

        elif act_selected == act_x_info:
            canvas.callback(ACTION_PORT_INFO, self.m_port_id, 0, "")

        elif act_selected == act_x_rename:
            new_name_try = QInputDialog.getText(None, "Rename Port", "New name:", QLineEdit.Normal, self.m_port_name)
            if new_name_try[1] and new_name_try[0]: # 1 - bool ok, 0 - return text
                canvas.callback(ACTION_PORT_RENAME, self.m_port_id, 0, new_name_try[0])

        event.accept()

    def boundingRect(self):
        return QRectF(0, 0, self.m_port_width + 12, self.m_port_height)

    def paint(self, painter, option, widget):
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))

        poly_locx = [0, 0, 0, 0, 0]

        if self.m_port_mode == PORT_MODE_INPUT:
            text_pos = QPointF(3, canvas.theme.port_text_ypos)

            if canvas.theme.port_mode == Theme.THEME_PORT_POLYGON:
                poly_locx[0] = 0
                poly_locx[1] = self.m_port_width + 5
                poly_locx[2] = self.m_port_width + 12
                poly_locx[3] = self.m_port_width + 5
                poly_locx[4] = 0
            elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE:
                poly_locx[0] = 0
                poly_locx[1] = self.m_port_width + 5
                poly_locx[2] = self.m_port_width + 5
                poly_locx[3] = self.m_port_width + 5
                poly_locx[4] = 0
            else:
                qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas.theme.port_mode)
                return

        elif self.m_port_mode == PORT_MODE_OUTPUT:
            text_pos = QPointF(9, canvas.theme.port_text_ypos)

            if canvas.theme.port_mode == Theme.THEME_PORT_POLYGON:
                poly_locx[0] = self.m_port_width + 12
                poly_locx[1] = 7
                poly_locx[2] = 0
                poly_locx[3] = 7
                poly_locx[4] = self.m_port_width + 12
            elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE:
                poly_locx[0] = self.m_port_width + 12
                poly_locx[1] = 5
                poly_locx[2] = 5
                poly_locx[3] = 5
                poly_locx[4] = self.m_port_width + 12
            else:
                qCritical("PatchCanvas::CanvasPort.paint() - invalid theme port mode '%s'" % canvas.theme.port_mode)
                return

        else:
            qCritical("PatchCanvas::CanvasPort.paint() - invalid port mode '%s'" % port_mode2str(self.m_port_mode))
            return

        if self.m_port_type == PORT_TYPE_AUDIO_JACK:
            poly_color = canvas.theme.port_audio_jack_bg_sel if self.isSelected() else canvas.theme.port_audio_jack_bg
            poly_pen = canvas.theme.port_audio_jack_pen_sel  if self.isSelected() else canvas.theme.port_audio_jack_pen
            text_pen = canvas.theme.port_audio_jack_text_sel if self.isSelected() else canvas.theme.port_audio_jack_text
            conn_pen = canvas.theme.port_audio_jack_pen_sel
        elif self.m_port_type == PORT_TYPE_MIDI_JACK:
            poly_color = canvas.theme.port_midi_jack_bg_sel if self.isSelected() else canvas.theme.port_midi_jack_bg
            poly_pen = canvas.theme.port_midi_jack_pen_sel  if self.isSelected() else canvas.theme.port_midi_jack_pen
            text_pen = canvas.theme.port_midi_jack_text_sel if self.isSelected() else canvas.theme.port_midi_jack_text
            conn_pen = canvas.theme.port_midi_jack_pen_sel
        elif self.m_port_type == PORT_TYPE_MIDI_A2J:
            poly_color = canvas.theme.port_midi_a2j_bg_sel if self.isSelected() else canvas.theme.port_midi_a2j_bg
            poly_pen = canvas.theme.port_midi_a2j_pen_sel  if self.isSelected() else canvas.theme.port_midi_a2j_pen
            text_pen = canvas.theme.port_midi_a2j_text_sel if self.isSelected() else canvas.theme.port_midi_a2j_text
            conn_pen = canvas.theme.port_midi_a2j_pen_sel
        elif self.m_port_type == PORT_TYPE_MIDI_ALSA:
            poly_color = canvas.theme.port_midi_alsa_bg_sel if self.isSelected() else canvas.theme.port_midi_alsa_bg
            poly_pen = canvas.theme.port_midi_alsa_pen_sel  if self.isSelected() else canvas.theme.port_midi_alsa_pen
            text_pen = canvas.theme.port_midi_alsa_text_sel if self.isSelected() else canvas.theme.port_midi_alsa_text
            conn_pen = canvas.theme.port_midi_alsa_pen_sel
        else:
            qCritical("PatchCanvas::CanvasPort.paint() - invalid port type '%s'" % port_type2str(self.m_port_type))
            return

        polygon  = QPolygonF()
        polygon += QPointF(poly_locx[0], 0)
        polygon += QPointF(poly_locx[1], 0)
        polygon += QPointF(poly_locx[2], float(canvas.theme.port_height)/2)
        polygon += QPointF(poly_locx[3], canvas.theme.port_height)
        polygon += QPointF(poly_locx[4], canvas.theme.port_height)

        if canvas.theme.port_bg_pixmap:
            portRect = polygon.boundingRect()
            portPos  = portRect.topLeft()
            painter.drawTiledPixmap(portRect, canvas.theme.port_bg_pixmap, portPos)
        else:
            painter.setBrush(poly_color)

        painter.setPen(poly_pen)
        painter.drawPolygon(polygon)

        painter.setPen(text_pen)
        painter.setFont(self.m_port_font)
        painter.drawText(text_pos, self.m_port_name)

        if self.isSelected() != self.m_last_selected_state:
            for connection in canvas.connection_list:
                if connection.port_out_id == self.m_port_id or connection.port_in_id == self.m_port_id:
                    connection.widget.setLineSelected(self.isSelected())

        if canvas.theme.idx == Theme.THEME_OOSTUDIO and canvas.theme.port_bg_pixmap:
            painter.setPen(Qt.NoPen)
            painter.setBrush(conn_pen.brush())

            if self.m_port_mode == PORT_MODE_INPUT:
                connRect = QRectF(portRect.topLeft(), QSizeF(2, portRect.height()))
            else:
                connRect = QRectF(QPointF(portRect.right()-2, portRect.top()), QSizeF(2, portRect.height()))

            painter.drawRect(connRect)

        self.m_last_selected_state = self.isSelected()

        painter.restore()

# ------------------------------------------------------------------------------
# canvasbox.cpp

class cb_line_t(object):
    __slots__ = [
        'line',
        'connection_id'
    ]

class CanvasBox(QGraphicsItem):
    def __init__(self, group_id, group_name, icon, parent=None):
        QGraphicsItem.__init__(self, parent)

        # Save Variables, useful for later
        self.m_group_id   = group_id
        self.m_group_name = group_name

        # Base Variables
        self.p_width  = 50
        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_port_list_ids = []
        self.m_connection_lines = []

        # Set Font
        self.m_font_name = QFont(canvas.theme.box_font_name, canvas.theme.box_font_size, canvas.theme.box_font_state)
        self.m_font_port = QFont(canvas.theme.port_font_name, canvas.theme.port_font_size, 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:
            self.shadow = CanvasBoxShadow(self.toGraphicsObject())
            self.shadow.setFakeParent(self)
            self.setGraphicsEffect(self.shadow)
        else:
            self.shadow = None

        # Final touches
        self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)

        # Wait for at least 1 port
        if options.auto_hide_groups:
            self.setVisible(False)

        self.updatePositions()

        canvas.scene.addItem(self)

    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 setIcon(self, icon):
        if self.icon_svg:
            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):
        if len(self.m_port_list_ids) == 0:
            if options.auto_hide_groups:
                if options.eyecandy == EYECANDY_FULL:
                    CanvasItemFX(self, True)
                self.setVisible(True)

        new_widget = CanvasPort(port_id, port_name, port_mode, port_type, 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.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)
                else:
                    self.setVisible(False)

    def addLineFromGroup(self, line, connection_id):
        new_cbline = cb_line_t()
        new_cbline.line = line
        new_cbline.connection_id = 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:
            canvas.scene.removeItem(self.icon_svg)

    def updatePositions(self):
        self.prepareGeometryChange()

        max_in_width  = 0
        max_in_height = canvas.theme.box_header_height + canvas.theme.box_header_spacing
        max_out_width  = 0
        max_out_height = canvas.theme.box_header_height + canvas.theme.box_header_spacing
        port_spacing   = canvas.theme.port_height + canvas.theme.port_spacing

        have_audio_jack_in = have_midi_jack_in = have_midi_a2j_in = have_midi_alsa_in = False
        have_audio_jack_out = have_midi_jack_out = have_midi_a2j_out = have_midi_alsa_out = False

        # reset box size
        self.p_width  = 50
        self.p_height = canvas.theme.box_header_height + canvas.theme.box_header_spacing + 1

        # Check Text Name size
        app_name_size = QFontMetrics(self.m_font_name).width(self.m_group_name) + 30
        if app_name_size > self.p_width:
            self.p_width = app_name_size

        # Get Port List
        port_list = []
        for port in canvas.port_list:
            if port.port_id in self.m_port_list_ids:
                port_list.append(port)

        # Get Max Box Width/Height
        for port in port_list:
            if port.port_mode == PORT_MODE_INPUT:
                max_in_height += port_spacing

                size = QFontMetrics(self.m_font_port).width(port.port_name)
                if size > max_in_width:
                    max_in_width = size

                if port.port_type == PORT_TYPE_AUDIO_JACK and not have_audio_jack_in:
                    have_audio_jack_in = True
                    max_in_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_JACK and not have_midi_jack_in:
                    have_midi_jack_in = True
                    max_in_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_A2J and not have_midi_a2j_in:
                    have_midi_a2j_in = True
                    max_in_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_ALSA and not have_midi_alsa_in:
                    have_midi_alsa_in = True
                    max_in_height += canvas.theme.port_spacingT

            elif port.port_mode == PORT_MODE_OUTPUT:
                max_out_height += port_spacing

                size = QFontMetrics(self.m_font_port).width(port.port_name)
                if size > max_out_width:
                    max_out_width = size

                if port.port_type == PORT_TYPE_AUDIO_JACK and not have_audio_jack_out:
                    have_audio_jack_out = True
                    max_out_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_JACK and not have_midi_jack_out:
                    have_midi_jack_out = True
                    max_out_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_A2J and not have_midi_a2j_out:
                    have_midi_a2j_out = True
                    max_out_height += canvas.theme.port_spacingT
                elif port.port_type == PORT_TYPE_MIDI_ALSA and not have_midi_alsa_out:
                    have_midi_alsa_out = True
                    max_out_height += canvas.theme.port_spacingT

        if canvas.theme.port_spacingT == 0:
            max_in_height  += 2
            max_out_height += 2

        final_width = 30 + max_in_width + max_out_width
        if final_width > self.p_width:
            self.p_width = final_width

        if max_in_height > self.p_height:
            self.p_height = max_in_height

        if max_out_height > self.p_height:
            self.p_height = max_out_height

        # Remove bottom space
        self.p_height -= canvas.theme.port_spacingT

        if canvas.theme.box_header_spacing > 0:
            if len(port_list) == 0:
                self.p_height -= canvas.theme.box_header_spacing
            else:
                self.p_height -= canvas.theme.box_header_spacing/2

        last_in_pos  = canvas.theme.box_header_height + canvas.theme.box_header_spacing
        last_out_pos = canvas.theme.box_header_height + canvas.theme.box_header_spacing
        last_in_type  = PORT_TYPE_NULL
        last_out_type = PORT_TYPE_NULL

        # Re-position ports, AUDIO_JACK
        for port in port_list:
            if port.port_type == PORT_TYPE_AUDIO_JACK:
                if port.port_mode == PORT_MODE_INPUT:
                    port.widget.setPos(QPointF(1 + canvas.theme.port_offset, last_in_pos))
                    port.widget.setPortWidth(max_in_width)

                    last_in_pos += port_spacing
                    last_in_type = port.port_type

                elif port.port_mode == PORT_MODE_OUTPUT:
                    port.widget.setPos(QPointF(self.p_width - max_out_width - canvas.theme.port_offset - 13, last_out_pos))
                    port.widget.setPortWidth(max_out_width)

                    last_out_pos += port_spacing
                    last_out_type = port.port_type

        # Re-position ports, MIDI_JACK
        for port in port_list:
            if port.port_type == PORT_TYPE_MIDI_JACK:
                if port.port_mode == PORT_MODE_INPUT:
                    if last_in_type != PORT_TYPE_NULL and port.port_type != last_in_type:
                        last_in_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(1 + canvas.theme.port_offset, last_in_pos))
                    port.widget.setPortWidth(max_in_width)

                    last_in_pos += port_spacing
                    last_in_type = port.port_type

                elif port.port_mode == PORT_MODE_OUTPUT:
                    if last_out_type != PORT_TYPE_NULL and port.port_type != last_out_type:
                        last_out_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(self.p_width - max_out_width - canvas.theme.port_offset - 13, last_out_pos))
                    port.widget.setPortWidth(max_out_width)

                    last_out_pos += port_spacing
                    last_out_type = port.port_type

        # Re-position ports, MIDI_A2J
        for port in port_list:
            if port.port_type == PORT_TYPE_MIDI_A2J:
                if port.port_mode == PORT_MODE_INPUT:
                    if last_in_type != PORT_TYPE_NULL and port.port_type != last_in_type:
                        last_in_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(1 + canvas.theme.port_offset, last_in_pos))
                    port.widget.setPortWidth(max_in_width)

                    last_in_pos += port_spacing
                    last_in_type = port.port_type

                elif port.port_mode == PORT_MODE_OUTPUT:
                    if last_out_type != PORT_TYPE_NULL and port.port_type != last_out_type:
                        last_out_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(self.p_width - max_out_width - canvas.theme.port_offset - 13, last_out_pos))
                    port.widget.setPortWidth(max_out_width)

                    last_out_pos += port_spacing
                    last_out_type = port.port_type

        # Re-position ports, MIDI_ALSA
        for port in port_list:
            if port.port_type == PORT_TYPE_MIDI_ALSA:
                if port.port_mode == PORT_MODE_INPUT:
                    if last_in_type != PORT_TYPE_NULL and port.port_type != last_in_type:
                        last_in_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(1 + canvas.theme.port_offset, last_in_pos))
                    port.widget.setPortWidth(max_in_width)

                    last_in_pos += port_spacing
                    last_in_type = port.port_type

                elif port.port_mode == PORT_MODE_OUTPUT:
                    if last_out_type != PORT_TYPE_NULL and port.port_type != last_out_type:
                        last_out_pos += canvas.theme.port_spacingT

                    port.widget.setPos(QPointF(self.p_width - max_out_width - canvas.theme.port_offset - 13, last_out_pos))
                    port.widget.setPortWidth(max_out_width)

                    last_out_pos += port_spacing
                    last_out_type = port.port_type

        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):
        menu = QMenu()
        discMenu = QMenu("Disconnect", menu)

        port_con_list = []
        port_con_list_ids = []

        for port_id in self.m_port_list_ids:
            tmp_port_con_list = CanvasGetPortConnectionList(port_id)
            for port_con_id in tmp_port_con_list:
                if port_con_id not in port_con_list:
                    port_con_list.append(port_con_id)
                    port_con_list_ids.append(port_id)

        if len(port_con_list) > 0:
            for i in range(len(port_con_list)):
                port_con_id = CanvasGetConnectedPort(port_con_list[i], port_con_list_ids[i])
                act_x_disc = discMenu.addAction(CanvasGetFullPortName(port_con_id))
                act_x_disc.setData(port_con_list[i])
                act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect)
        else:
            act_x_disc = discMenu.addAction("No connections")
            act_x_disc.setEnabled(False)

        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)

        haveIns = haveOuts = False
        for port in canvas.port_list:
            if 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 == act_x_disc_all:
            for port_id in port_con_list:
                canvas.callback(ACTION_PORTS_DISCONNECT, port_id, 0, "")

        elif act_selected == act_x_info:
            canvas.callback(ACTION_GROUP_INFO, self.m_group_id, 0, "")

        elif act_selected == act_x_rename:
            new_name_try = QInputDialog.getText(None, "Rename Group", "New name:", QLineEdit.Normal, self.m_group_name)
            if new_name_try[1] and new_name_try[0]: # 1 - bool ok, 0 - return text
                canvas.callback(ACTION_GROUP_RENAME, self.m_group_id, 0, new_name_try[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, "")

        event.accept()

    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:
            canvas.scene.clearSelection()
            self.setSelected(True)
            self.m_mouse_down = False
            return event.accept()

        elif event.button() == Qt.LeftButton:
            if self.sceneBoundingRect().contains(event.scenePos()):
                self.m_mouse_down = True
            else:
                # Fix a weird Qt behaviour with right-click mouseMove
                self.m_mouse_down = False
                return event.ignore()

        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.setCursor(QCursor(Qt.ArrowCursor))
        self.m_mouse_down = False
        self.m_cursor_moving = False
        QGraphicsItem.mouseReleaseEvent(self, event)

    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, False)

        # Draw rectangle
        if self.isSelected():
            painter.setPen(canvas.theme.box_pen_sel)
        else:
            painter.setPen(canvas.theme.box_pen)

        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)

        painter.drawRect(QRectF(0, 0, self.p_width, self.p_height))

        # Draw pixmap header
        if canvas.theme.box_header_pixmap:
            painter.setPen(Qt.NoPen)
            painter.setBrush(canvas.theme.box_bg_2)
            painter.drawRect(QRectF(1, 1, self.p_width-2, canvas.theme.box_header_height))

            headerPos  = QPointF(1, 1)
            headerRect = QRectF(2, 2, int(self.p_width-4), canvas.theme.box_header_height-3)
            painter.drawTiledPixmap(headerRect, canvas.theme.box_header_pixmap, headerPos)

        # 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()

# ------------------------------------------------------------------------------
# canvasicon.cpp

class CanvasIcon(QGraphicsSvgItem):
    def __init__(self, icon, name, parent):
        QGraphicsSvgItem.__init__(self, parent)

        self.m_renderer = None
        self.p_size = QRectF(0, 0, 0, 0)

        self.m_colorFX = QGraphicsColorizeEffect(self)
        self.m_colorFX.setColor(canvas.theme.box_text.color())

        self.setGraphicsEffect(self.m_colorFX)
        self.setIcon(icon, name)

    def setIcon(self, icon, name):
        name = name.lower()
        icon_path = ""

        if icon == ICON_APPLICATION:
            self.p_size = QRectF(3, 2, 19, 18)

            if "audacious" in name:
                icon_path   = ":/scalable/pb_audacious.svg"
                self.p_size = QRectF(5, 4, 16, 16)
            elif "clementine" in name:
                icon_path = ":/scalable/pb_clementine.svg"
                self.p_size = QRectF(5, 4, 16, 16)
            elif "distrho" in name:
                icon_path = ":/scalable/pb_distrho.svg"
                self.p_size = QRectF(5, 4, 14, 14)
            elif "jamin" in name:
                icon_path = ":/scalable/pb_jamin.svg"
                self.p_size = QRectF(5, 3, 16, 16)
            elif "mplayer" in name:
                icon_path = ":/scalable/pb_mplayer.svg"
                self.p_size = QRectF(5, 4, 16, 16)
            elif "vlc" in name:
                icon_path = ":/scalable/pb_vlc.svg"
                self.p_size = QRectF(5, 3, 16, 16)

            else:
                icon_path = ":/scalable/pb_generic.svg"
                self.p_size = QRectF(4, 3, 16, 16)

        elif icon == ICON_HARDWARE:
            icon_path = ":/scalable/pb_hardware.svg"
            self.p_size = QRectF(5, 2, 16, 16)

        elif icon == ICON_DISTRHO:
            icon_path = ":/scalable/pb_distrho.svg"
            self.p_size = QRectF(5, 4, 14, 14)

        elif icon == ICON_FILE:
            icon_path = ":/scalable/pb_file.svg"
            self.p_size = QRectF(5, 4, 12, 14)

        elif icon == ICON_PLUGIN:
            icon_path = ":/scalable/pb_plugin.svg"
            self.p_size = QRectF(5, 4, 14, 14)

        elif icon == ICON_LADISH_ROOM:
            # TODO - make a unique ladish-room icon
            icon_path = ":/scalable/pb_hardware.svg"
            self.p_size = QRectF(5, 2, 16, 16)

        else:
            self.p_size = QRectF(0, 0, 0, 0)
            qCritical("PatchCanvas::CanvasIcon.setIcon(%s, %s) - unsupported icon requested" % (icon2str(icon), name.encode()))
            return

        self.m_renderer = QSvgRenderer(icon_path, canvas.scene)
        self.setSharedRenderer(self.m_renderer)
        self.update()

    def type(self):
        return CanvasIconType

    def boundingRect(self):
        return self.p_size

    def paint(self, painter, option, widget):
        if self.m_renderer:
            painter.save()
            painter.setRenderHint(QPainter.Antialiasing, False)
            painter.setRenderHint(QPainter.TextAntialiasing, False)
            self.m_renderer.render(painter, self.p_size)
            painter.restore()
        else:
            QGraphicsSvgItem.paint(self, painter, option, widget)

# ------------------------------------------------------------------------------
# canvasportglow.cpp

class CanvasPortGlow(QGraphicsDropShadowEffect):
    def __init__(self, port_type, parent):
        QGraphicsDropShadowEffect.__init__(self, parent)

        self.setBlurRadius(12)
        self.setOffset(0, 0)

        if port_type == PORT_TYPE_AUDIO_JACK:
            self.setColor(canvas.theme.line_audio_jack_glow)
        elif port_type == PORT_TYPE_MIDI_JACK:
            self.setColor(canvas.theme.line_midi_jack_glow)
        elif port_type == PORT_TYPE_MIDI_A2J:
            self.setColor(canvas.theme.line_midi_a2j_glow)
        elif port_type == PORT_TYPE_MIDI_ALSA:
            self.setColor(canvas.theme.line_midi_alsa_glow)

# ------------------------------------------------------------------------------
# canvasboxshadow.cpp

class CanvasBoxShadow(QGraphicsDropShadowEffect):
    def __init__(self, parent):
        QGraphicsDropShadowEffect.__init__(self, parent)

        self.m_fakeParent = None

        self.setBlurRadius(20)
        self.setColor(canvas.theme.box_shadow)
        self.setOffset(0, 0)

    def setFakeParent(self, fakeParent):
        self.m_fakeParent = fakeParent

    def setOpacity(self, opacity):
        color = QColor(canvas.theme.box_shadow)
        color.setAlphaF(opacity)
        self.setColor(color)

    def draw(self, painter):
        if self.m_fakeParent:
            self.m_fakeParent.repaintLines()
        QGraphicsDropShadowEffect.draw(self, painter)
