#!/usr/bin/env python3 # -*- coding: utf-8 -*- # PatchBay Canvas engine using QGraphicsView/Scene # Copyright (C) 2010-2019 Filipe Coelho # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # For a full copy of the GNU General Public License see the doc/GPL.txt file. # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from math import floor from PyQt5.QtCore import qCritical, Qt, QLineF, QPointF, QRectF, QTimer from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QPainter, QPainterPath, QPen, QPolygonF from PyQt5.QtWidgets import QGraphicsItem, QMenu # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) from . import ( canvas, features, options, port_mode2str, port_type2str, CanvasPortType, ANTIALIASING_FULL, ACTION_PORT_INFO, ACTION_PORT_RENAME, ACTION_PORTS_CONNECT, ACTION_PORTS_DISCONNECT, PORT_MODE_INPUT, PORT_MODE_OUTPUT, PORT_TYPE_AUDIO_JACK, PORT_TYPE_MIDI_ALSA, PORT_TYPE_MIDI_JACK, PORT_TYPE_PARAMETER, ) from .canvasbezierlinemov import CanvasBezierLineMov from .canvaslinemov import CanvasLineMov from .theme import Theme from .utils import CanvasGetFullPortName, CanvasGetPortConnectionList # ------------------------------------------------------------------------------------------------------------ class CanvasPort(QGraphicsItem): def __init__(self, group_id, port_id, port_name, port_mode, port_type, is_alternate, parent): QGraphicsItem.__init__(self) self.setParentItem(parent) # Save Variables, useful for later self.m_group_id = group_id self.m_port_id = port_id self.m_port_mode = port_mode self.m_port_type = port_type self.m_port_name = port_name self.m_is_alternate = is_alternate # Base Variables self.m_port_width = 15 self.m_port_height = canvas.theme.port_height self.m_port_font = QFont() self.m_port_font.setFamily(canvas.theme.port_font_name) self.m_port_font.setPixelSize(canvas.theme.port_font_size) self.m_port_font.setWeight(canvas.theme.port_font_state) self.m_line_mov = None self.m_hover_item = None self.m_mouse_down = False self.m_cursor_moving = False self.setFlags(QGraphicsItem.ItemIsSelectable) if options.auto_select_items: self.setAcceptHoverEvents(True) def getGroupId(self): return self.m_group_id 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): metrics = QFontMetrics(self.m_port_font) if QT_VERSION >= 0x50b00: width1 = metrics.horizontalAdvance(port_name) width2 = metrics.horizontalAdvance(self.m_port_name) else: width1 = metrics.width(port_name) width2 = metrics.width(self.m_port_name) if width1 < width2: 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 hoverEnterEvent(self, event): if options.auto_select_items: self.setSelected(True) QGraphicsItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): if options.auto_select_items: self.setSelected(False) QGraphicsItem.hoverLeaveEvent(self, event) def mousePressEvent(self, event): if event.button() == Qt.MiddleButton or event.source() == Qt.MouseEventSynthesizedByApplication: event.ignore() return if self.m_mouse_down: self.handleMouseRelease() 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 not self.m_mouse_down: QGraphicsItem.mouseMoveEvent(self, event) return event.accept() if not self.m_cursor_moving: self.setCursor(QCursor(Qt.CrossCursor)) self.m_cursor_moving = True for connection in canvas.connection_list: if ( (connection.group_out_id == self.m_group_id and connection.port_out_id == self.m_port_id) or (connection.group_in_id == self.m_group_id and 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)): for _, itemx in enumerate(items): if itemx.type() != CanvasPortType: continue if itemx == self: continue if item is None or itemx.parentItem().zValue() > item.parentItem().zValue(): item = itemx if self.m_hover_item and self.m_hover_item != item: self.m_hover_item.setSelected(False) if item is not None: if item.getPortMode() != self.m_port_mode and item.getPortType() == self.m_port_type: 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()) def handleMouseRelease(self): if self.m_mouse_down: if self.m_line_mov is not None: item = self.m_line_mov self.m_line_mov = None canvas.scene.removeItem(item) del item for connection in canvas.connection_list: if ( (connection.group_out_id == self.m_group_id and connection.port_out_id == self.m_port_id) or (connection.group_in_id == self.m_group_id and connection.port_in_id == self.m_port_id) ): connection.widget.setLocked(False) if self.m_hover_item: # TODO: a better way to check already existing connection for connection in canvas.connection_list: hover_group_id = self.m_hover_item.getGroupId() hover_port_id = self.m_hover_item.getPortId() # FIXME clean this big if stuff if ( (connection.group_out_id == self.m_group_id and connection.port_out_id == self.m_port_id and connection.group_in_id == hover_group_id and connection.port_in_id == hover_port_id) or (connection.group_out_id == hover_group_id and connection.port_out_id == hover_port_id and connection.group_in_id == self.m_group_id and connection.port_in_id == self.m_port_id) ): canvas.callback(ACTION_PORTS_DISCONNECT, connection.connection_id, 0, "") break else: if self.m_port_mode == PORT_MODE_OUTPUT: conn = "%i:%i:%i:%i" % (self.m_group_id, self.m_port_id, self.m_hover_item.getGroupId(), self.m_hover_item.getPortId()) canvas.callback(ACTION_PORTS_CONNECT, 0, 0, conn) else: conn = "%i:%i:%i:%i" % (self.m_hover_item.getGroupId(), self.m_hover_item.getPortId(), self.m_group_id, self.m_port_id) canvas.callback(ACTION_PORTS_CONNECT, 0, 0, conn) canvas.scene.clearSelection() if self.m_cursor_moving: self.unsetCursor() self.m_hover_item = None self.m_mouse_down = False self.m_cursor_moving = False def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.handleMouseRelease() QGraphicsItem.mouseReleaseEvent(self, event) def contextMenuEvent(self, event): event.accept() canvas.scene.clearSelection() self.setSelected(True) menu = QMenu() discMenu = QMenu("Disconnect", menu) conn_list = CanvasGetPortConnectionList(self.m_group_id, self.m_port_id) if len(conn_list) > 0: for conn_id, group_id, port_id in conn_list: act_x_disc = discMenu.addAction(CanvasGetFullPortName(group_id, port_id)) act_x_disc.setData(conn_id) act_x_disc.triggered.connect(canvas.qobject.PortContextMenuDisconnect) else: act_x_disc = discMenu.addAction("No connections") act_x_disc.setEnabled(False) menu.addMenu(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: self.triggerDisconnect(conn_list) elif act_selected == act_x_info: canvas.callback(ACTION_PORT_INFO, self.m_group_id, self.m_port_id, "") elif act_selected == act_x_rename: canvas.callback(ACTION_PORT_RENAME, self.m_group_id, self.m_port_id, "") def setPortSelected(self, yesno): for connection in canvas.connection_list: if ( (connection.group_out_id == self.m_group_id and connection.port_out_id == self.m_port_id) or (connection.group_in_id == self.m_group_id and connection.port_in_id == self.m_port_id) ): connection.widget.updateLineSelected() def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedHasChanged: self.setPortSelected(value) return QGraphicsItem.itemChange(self, change, value) def triggerDisconnect(self, conn_list=None): if not conn_list: conn_list = CanvasGetPortConnectionList(self.m_group_id, self.m_port_id) for conn_id, group_id, port_id in conn_list: canvas.callback(ACTION_PORTS_DISCONNECT, conn_id, 0, "") 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)) selected = self.isSelected() theme = canvas.theme if self.m_port_type == PORT_TYPE_AUDIO_JACK: poly_color = theme.port_audio_jack_bg_sel if selected else theme.port_audio_jack_bg poly_pen = theme.port_audio_jack_pen_sel if selected else theme.port_audio_jack_pen text_pen = theme.port_audio_jack_text_sel if selected else theme.port_audio_jack_text conn_pen = QPen(theme.port_audio_jack_pen_sel) elif self.m_port_type == PORT_TYPE_MIDI_JACK: poly_color = theme.port_midi_jack_bg_sel if selected else theme.port_midi_jack_bg poly_pen = theme.port_midi_jack_pen_sel if selected else theme.port_midi_jack_pen text_pen = theme.port_midi_jack_text_sel if selected else theme.port_midi_jack_text conn_pen = QPen(theme.port_midi_jack_pen_sel) elif self.m_port_type == PORT_TYPE_MIDI_ALSA: poly_color = theme.port_midi_alsa_bg_sel if selected else theme.port_midi_alsa_bg poly_pen = theme.port_midi_alsa_pen_sel if selected else theme.port_midi_alsa_pen text_pen = theme.port_midi_alsa_text_sel if selected else theme.port_midi_alsa_text conn_pen = QPen(theme.port_midi_alsa_pen_sel) elif self.m_port_type == PORT_TYPE_PARAMETER: poly_color = theme.port_parameter_bg_sel if selected else theme.port_parameter_bg poly_pen = theme.port_parameter_pen_sel if selected else theme.port_parameter_pen text_pen = theme.port_parameter_text_sel if selected else theme.port_parameter_text conn_pen = QPen(theme.port_parameter_pen_sel) else: qCritical("PatchCanvas::CanvasPort.paint() - invalid port type '%s'" % port_type2str(self.m_port_type)) return # To prevent quality worsening poly_pen = QPen(poly_pen) poly_pen.setWidthF(poly_pen.widthF() + 0.00001) if self.m_is_alternate: poly_color = poly_color.darker(180) #poly_pen.setColor(poly_pen.color().darker(110)) #text_pen.setColor(text_pen.color()) #.darker(150)) #conn_pen.setColor(conn_pen.color()) #.darker(150)) lineHinting = poly_pen.widthF() / 2 poly_locx = [0, 0, 0, 0, 0] poly_corner_xhinting = (float(canvas.theme.port_height)/2) % floor(float(canvas.theme.port_height)/2) if poly_corner_xhinting == 0: poly_corner_xhinting = 0.5 * (1 - 7 / (float(canvas.theme.port_height)/2)) 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] = lineHinting poly_locx[1] = self.m_port_width + 5 - lineHinting poly_locx[2] = self.m_port_width + 12 - poly_corner_xhinting poly_locx[3] = self.m_port_width + 5 - lineHinting poly_locx[4] = lineHinting elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE: poly_locx[0] = lineHinting poly_locx[1] = self.m_port_width + 5 - lineHinting poly_locx[2] = self.m_port_width + 5 - lineHinting poly_locx[3] = self.m_port_width + 5 - lineHinting poly_locx[4] = lineHinting 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 - lineHinting poly_locx[1] = 7 + lineHinting poly_locx[2] = 0 + poly_corner_xhinting poly_locx[3] = 7 + lineHinting poly_locx[4] = self.m_port_width + 12 - lineHinting elif canvas.theme.port_mode == Theme.THEME_PORT_SQUARE: poly_locx[0] = self.m_port_width + 12 - lineHinting poly_locx[1] = 5 + lineHinting poly_locx[2] = 5 + lineHinting poly_locx[3] = 5 + lineHinting poly_locx[4] = self.m_port_width + 12 - lineHinting 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 polygon = QPolygonF() polygon += QPointF(poly_locx[0], lineHinting) polygon += QPointF(poly_locx[1], lineHinting) polygon += QPointF(poly_locx[2], float(canvas.theme.port_height)/2) polygon += QPointF(poly_locx[3], canvas.theme.port_height - lineHinting) polygon += QPointF(poly_locx[4], canvas.theme.port_height - lineHinting) polygon += QPointF(poly_locx[0], lineHinting) if canvas.theme.port_bg_pixmap: portRect = polygon.boundingRect().adjusted(-lineHinting+1, -lineHinting+1, lineHinting-1, lineHinting-1) portPos = portRect.topLeft() painter.drawTiledPixmap(portRect, canvas.theme.port_bg_pixmap, portPos) else: painter.setBrush(poly_color) #.lighter(200)) 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 canvas.theme.idx == Theme.THEME_OOSTUDIO and canvas.theme.port_bg_pixmap: conn_pen.setCosmetic(True) conn_pen.setWidthF(0.4) painter.setPen(conn_pen) if self.m_port_mode == PORT_MODE_INPUT: connLineX = portRect.left()+1 else: connLineX = portRect.right()-1 conn_path = QPainterPath() conn_path.addRect(QRectF(connLineX-1, portRect.top(), 2, portRect.height())) painter.fillPath(conn_path, conn_pen.brush()) painter.drawLine(QLineF(connLineX, portRect.top(), connLineX, portRect.bottom())) painter.restore() # ------------------------------------------------------------------------------------------------------------