Source code for pySimBlocks.gui.graphics.port_item
# ******************************************************************************
# pySimBlocks
# Copyright (c) 2026 Université de Lille & INRIA
# ******************************************************************************
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) 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 Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# ******************************************************************************
# Authors: see Authors.txt
# ******************************************************************************
from typing import TYPE_CHECKING
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QBrush, QFont, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem
if TYPE_CHECKING:
from pySimBlocks.gui.graphics.block_item import BlockItem
from pySimBlocks.gui.models.port_instance import PortInstance
[docs]
class PortItem(QGraphicsItem):
"""Render and interact with a block port on the diagram.
Attributes:
instance: Port model represented by the item.
parent_block: Block item owning the port.
label: Text label displayed next to the port.
"""
MARGIN = 4
R = 6 # radius input port
L = 15 # length output port
H = 10 # height output port
RECT = QRectF(-8, -8, 15, 15) # bounding rect for both port types
def __init__(self, instance: 'PortInstance', parent_block: 'BlockItem'):
"""Initialize a port item.
Args:
instance: Port model represented by the item.
parent_block: Block item owning the port.
Raises:
None.
"""
super().__init__(parent_block)
self.instance = instance
self.parent_block = parent_block
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges)
self.setAcceptedMouseButtons(Qt.LeftButton)
# Label
self.t = parent_block.view.theme
self.label = QGraphicsTextItem(self.instance.display_as, parent_block)
self.label.setDefaultTextColor(self.t.text)
self.label.setFont(QFont("Sans Serif", 8))
# --------------------------------------------------------------------------
# Public Methods
# --------------------------------------------------------------------------
@property
def is_input(self):
"""Return whether this port is an input port."""
return self.instance.direction == "input"
@property
def is_on_left_side(self) -> bool:
"""Return whether the port is currently placed on the left block side."""
return self.pos().x() < (self.parent_block.rect().width() * 0.5)
[docs]
def update_label_position(self):
"""Position the port label according to the current side."""
label_rect = self.label.boundingRect()
if self.is_on_left_side:
self.label.setPos(
self.x() + self.R + self.MARGIN,
self.y() - label_rect.height() / 2,
)
else:
self.label.setPos(
self.x() - label_rect.width() - self.R - self.MARGIN,
self.y() - label_rect.height() / 2,
)
[docs]
def update_display_as(self):
"""Refresh the displayed port label text."""
self.label.setPlainText(self.instance.display_as)
[docs]
def connection_anchor(self) -> QPointF:
"""Return the scene anchor point used to attach a connection.
Returns:
Scene coordinate used as the wire anchor for this port.
"""
if self.is_input:
x = -self.R if self.is_on_left_side else self.R
local = QPointF(x, 0)
else:
x = self.L if not self.is_on_left_side else -self.L
local = QPointF(x, 0)
return self.mapToScene(local)
[docs]
def is_compatible(self, other: 'PortItem'):
"""Return whether this port can connect to another port.
Args:
other: Other port item to compare against.
Returns:
True if the ports have opposite directions.
"""
return self.instance.direction != other.instance.direction
[docs]
def boundingRect(self) -> QRectF:
"""Return the fixed bounding rectangle of the port.
Returns:
Bounding rectangle used for painting and hit testing.
"""
return self.RECT
[docs]
def paint(self, painter, option, widget=None):
"""Paint the port as a circle or triangle depending on its direction.
Args:
painter: Painter used to render the item.
option: Style option describing the current paint state.
widget: Optional target widget.
"""
painter.setRenderHint(QPainter.Antialiasing)
fill = self.t.port_in if self.is_input else self.t.port_out
painter.setBrush(QBrush(fill))
painter.setPen(QPen(self.t.block_border, 1))
if self.is_input:
painter.drawEllipse(-self.R, -self.R, 2 * self.R, 2 * self.R)
else:
path = QPainterPath()
path.moveTo(0, -self.H)
path.lineTo(0, self.H)
tip_x = self.L if not self.is_on_left_side else - self.L
path.lineTo(tip_x, 0)
path.closeSubpath()
painter.drawPath(path)
[docs]
def shape(self):
"""Return the hit-test shape of the port.
Returns:
Painter path matching the painted port shape.
"""
path = QPainterPath()
if self.is_input:
path.addEllipse(-self.R, -self.R, 2*self.R, 2*self.R)
else:
tip_x = self.L if not self.is_on_left_side else -self.L
path.moveTo(0, -self.H)
path.lineTo(0, self.H)
path.lineTo(tip_x, 0)
path.closeSubpath()
return path
[docs]
def mousePressEvent(self, event):
"""Start a connection drag from this port.
Args:
event: Qt mouse-press event.
"""
self.parent_block.view.create_connection_event(self)
event.accept()
[docs]
def itemChange(self, change, value):
"""Update the label position when the port scene position changes.
Args:
change: Item change identifier.
value: Proposed new value for the change.
Returns:
Base implementation result.
"""
if change == QGraphicsItem.ItemScenePositionHasChanged:
self.update_label_position()
return super().itemChange(change, value)