diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py
index 6d8fe9c..3a2c940 100644
--- a/src/bedit_gui/controllers/document_tree_controller.py
+++ b/src/bedit_gui/controllers/document_tree_controller.py
@@ -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)
diff --git a/src/bedit_gui/models.py b/src/bedit_gui/models.py
index 7ff1ad8..ba517a7 100644
--- a/src/bedit_gui/models.py
+++ b/src/bedit_gui/models.py
@@ -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
diff --git a/src/bedit_gui/ui/forms/icon_editor_window.ui b/src/bedit_gui/ui/forms/icon_editor_window.ui
index b77f8a7..a4b1a30 100644
--- a/src/bedit_gui/ui/forms/icon_editor_window.ui
+++ b/src/bedit_gui/ui/forms/icon_editor_window.ui
@@ -72,6 +72,7 @@
false
+
@@ -160,6 +161,21 @@
QAction::MenuRole::NoRole
+
+
+
+ :/icons/icons/draw-text.png:/icons/icons/draw-text.png
+
+
+ Add Text
+
+
+ Add a text field
+
+
+ QAction::MenuRole::NoRole
+
+
diff --git a/src/bedit_gui/views/icon_editor_window.py b/src/bedit_gui/views/icon_editor_window.py
index 3987998..044dc65 100644
--- a/src/bedit_gui/views/icon_editor_window.py
+++ b/src/bedit_gui/views/icon_editor_window.py
@@ -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))
diff --git a/src/bedit_gui/views/icon_graphics_scene.py b/src/bedit_gui/views/icon_graphics_scene.py
index f6d02ba..54cf96a 100644
--- a/src/bedit_gui/views/icon_graphics_scene.py
+++ b/src/bedit_gui/views/icon_graphics_scene.py
@@ -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()