Added ports to icon

This commit is contained in:
2026-07-27 14:12:13 +02:00
parent 035cbdf53f
commit 13d3825f9b
5 changed files with 132 additions and 7 deletions

View File

@@ -113,7 +113,7 @@ class DocumentTreeController(QObject):
def _edit_icon(self, component: Component) -> None:
component_id = self.document.component_id(component)
editor = IconEditorWindow(self.document.component_icon(component_id), self.window)
editor = IconEditorWindow(self.document.component_icon(component_id), component.interface.ports, self.window)
editor.saved.connect(partial(self.document.change_icon, component_id))
editor.destroyed.connect(partial(self._icon_editor_closed, editor))
self._icon_editors.append(editor)

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from bedit_core.models import ComponentID, ID
from bedit_core.models import ComponentID, ID, PortID
class ShapeID(ID):
@@ -40,8 +40,8 @@ class LineType(Enum):
@dataclass
class Rectangle(Shape):
type: str = field(init=False, default="rectangle")
width: float = 100.0
height: float = 100.0
width: float = 32.0
height: float = 32.0
line_type: LineType = LineType.SOLID
line_thickness: float = 1.0
corner_radius: float = 0.0
@@ -56,18 +56,30 @@ class Rectangle(Shape):
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass
class Text(Shape):
type: str = field(init=False, default="text")
width: float = 100.0
height: float = 100.0
color: str = "#000000ff"
bold: bool = False
italic: bool = False
size: float = 16.0
text: str = ""
@dataclass
class Icon:
shapes: dict[ShapeID, Shape] = field(default_factory=dict)
port_positions: dict[PortID, tuple[int, int]] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Icon:
shapes = {ShapeID(key): Shape.from_data(value) for key, value in data.get("shapes", {}).items()}
return cls(shapes=shapes)
port_positions = {PortID(key): (int(value[0]), int(value[1])) for key, value in data.get("port_positions", {}).items()}
return cls(shapes=shapes, port_positions=port_positions)
def to_data(self) -> dict[str, Any]:
return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}}
return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}, "port_positions": {str(key): list(position) for key, position in self.port_positions.items()}}
@dataclass

View File

@@ -72,6 +72,7 @@
<bool>false</bool>
</attribute>
<addaction name="actionAdd_Rectangle"/>
<addaction name="actionAdd_Text"/>
</widget>
<action name="actionUndo">
<property name="icon">
@@ -160,6 +161,21 @@
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionAdd_Text">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-text.png</normaloff>:/icons/icons/draw-text.png</iconset>
</property>
<property name="text">
<string>Add Text</string>
</property>
<property name="toolTip">
<string>Add a text field</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<resources>
<include location="../../resources/resources.qrc"/>

View File

@@ -6,6 +6,7 @@ from PySide6.QtCore import QEvent, QObject, Qt, Signal
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent, QShowEvent
from PySide6.QtWidgets import QDialog, QGraphicsView, QMainWindow, QWidget
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, Shape, ShapeID
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool
@@ -69,6 +70,21 @@ class DeleteShapesCommand(QUndoCommand):
self.editor._restore_shapes(self.shapes)
class MovePortCommand(QUndoCommand):
def __init__(self, editor: IconEditorWindow, port_id: PortID, old_position: tuple[int, int], new_position: tuple[int, int]) -> None:
super().__init__("Move port")
self.editor = editor
self.port_id = port_id
self.old_position = old_position
self.new_position = new_position
def redo(self) -> None:
self.editor._move_port(self.port_id, self.new_position)
def undo(self) -> None:
self.editor._move_port(self.port_id, self.old_position)
class IconEditorWindow(QMainWindow):
"""Independent icon editing session with its own undo stack."""
@@ -78,7 +94,7 @@ class IconEditorWindow(QMainWindow):
minimum_zoom = 0.1
maximum_zoom = 10.0
def __init__(self, icon: Icon, parent: QWidget | None = None) -> None:
def __init__(self, icon: Icon, ports: dict[PortID, Port], parent: QWidget | None = None) -> None:
super().__init__(parent)
self.ui = Ui_iconEditor()
@@ -88,7 +104,10 @@ class IconEditorWindow(QMainWindow):
self._icon = deepcopy(icon)
self.scene = IconGraphicsScene(self)
self.scene.set_ports(ports)
self._ensure_port_positions(ports)
self.scene.set_icon(self._icon)
self.scene.port_moved.connect(self._port_moved)
self.scene.shape_created.connect(self._shape_created)
self.scene.shape_changed.connect(self._shape_changed)
self.scene.shape_options_requested.connect(self._show_shape_options)
@@ -197,12 +216,20 @@ class IconEditorWindow(QMainWindow):
self.scene.set_icon(self._icon)
self.icon_changed.emit(self.icon())
def _move_port(self, port_id: PortID, position: tuple[int, int]) -> None:
self._icon.port_positions[port_id] = position
self.scene.set_icon(self._icon)
self.icon_changed.emit(self.icon())
def _shape_created(self, shape: Shape) -> None:
self.undo_stack.push(AddShapeCommand(self, ShapeID(), shape))
def _shape_changed(self, shape_id: ShapeID, old_shape: Shape, new_shape: Shape) -> None:
self.undo_stack.push(ChangeShapeCommand(self, shape_id, old_shape, new_shape))
def _port_moved(self, port_id: PortID, old_position: tuple[int, int], new_position: tuple[int, int]) -> None:
self.undo_stack.push(MovePortCommand(self, port_id, old_position, new_position))
def _delete_selected_shapes(self) -> None:
shape_ids = self.scene.selected_shape_ids()
shapes = {shape_id: self._icon.shapes[shape_id] for shape_id in shape_ids}
@@ -219,6 +246,26 @@ class IconEditorWindow(QMainWindow):
if new_shape != old_shape:
self.undo_stack.push(ChangeShapeCommand(self, shape_id, old_shape, new_shape))
def _ensure_port_positions(self, ports: dict[PortID, Port]) -> None:
bounds = self.scene.sceneRect()
left = round(bounds.left())
top = round(bounds.top())
right = round(bounds.right() - 16)
bottom = round(bounds.bottom() - 16)
positions: dict[PortID, tuple[int, int]] = {}
input_index = 0
output_index = 0
for port_id, port in ports.items():
if port.direction is SignalDirection.INPUT:
default = (left, top + input_index * 16)
input_index += 1
else:
default = (right, top + output_index * 16)
output_index += 1
position = self._icon.port_positions.get(port_id, default)
positions[port_id] = (max(left, min(right, position[0])), max(top, min(bottom, position[1])))
self._icon.port_positions = positions
def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))

View File

@@ -8,6 +8,7 @@ from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID
@@ -172,7 +173,48 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
self.setPath(path)
class PortGraphicsItem(QGraphicsRectItem):
size = 16.0
def __init__(self, port_id: PortID, port: Port, position: tuple[int, int], scene: IconGraphicsScene) -> None:
super().__init__(0, 0, self.size, self.size)
self.port_id = port_id
self.icon_scene = scene
self._original_position: tuple[int, int] | None = None
self.setPos(position[0], position[1])
self.setPen(QPen(QColor("#000000")))
self.setBrush(QBrush(QColor("#000000") if port.direction is SignalDirection.INPUT else QColor("#ffffff")))
self.setZValue(1000000)
self.setToolTip(port.name)
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_position = self.position()
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
super().mouseReleaseEvent(event)
position = self.position()
if self._original_position is not None and position != self._original_position:
self.icon_scene.port_moved.emit(self.port_id, self._original_position, position)
self._original_position = None
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
if change is QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.scene() is not None:
position = value
if isinstance(position, QPointF):
bounds = self.icon_scene.sceneRect()
x = max(bounds.left(), min(bounds.right() - self.size, round(position.x())))
y = max(bounds.top(), min(bounds.bottom() - self.size, round(position.y())))
return QPointF(x, y)
return super().itemChange(change, value)
def position(self) -> tuple[int, int]:
return (round(self.pos().x()), round(self.pos().y()))
class IconGraphicsScene(QGraphicsScene):
port_moved = Signal(object, object, object)
shape_created = Signal(object)
shape_changed = Signal(object, object, object)
shape_options_requested = Signal(object)
@@ -181,13 +223,21 @@ class IconGraphicsScene(QGraphicsScene):
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._tool: ShapeCreationTool | None = None
self._ports: dict[PortID, Port] = {}
self.setSceneRect(-64, -64, 128, 128)
def set_ports(self, ports: dict[PortID, Port]) -> None:
self._ports = deepcopy(ports)
def set_icon(self, icon: Icon) -> None:
self.cancel_creation_tool()
self.clear()
for shape_id, shape in sorted(icon.shapes.items(), key=lambda item: item[1].layer):
self._add_shape_item(shape_id, shape)
for port_id, port in self._ports.items():
position = icon.port_positions.get(port_id)
if position is not None:
self.addItem(PortGraphicsItem(port_id, port, position, self))
def set_creation_tool(self, tool: ShapeCreationTool) -> None:
self.cancel_creation_tool()