Added ports to icon
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user