Better drawing and added more icons

This commit is contained in:
2026-07-20 13:18:16 +02:00
parent ee859c4311
commit 4ab74f64e2
27 changed files with 4087 additions and 307 deletions

View File

@@ -24,15 +24,18 @@ from PySide6.QtWidgets import (
QGraphicsView,
QApplication,
QMenu,
QInputDialog,
QStyleOptionGraphicsItem,
QToolTip,
QWidget,
)
from bedit.core.model import Component, Connection, Endpoint, Port
from bedit.core.model import Annotation, Component, Connection, Endpoint, Port
from bedit.gui.controllers.document import DocumentController
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon
from bedit.gui.graphics.icon_editor import ShapeOptionsDialog
from bedit.gui.graphics.icon_renderer import shape_pen
from bedit.gui.graphics.connection_styles import ConnectionStyle, connection_style
from bedit.gui.preferences import application_settings
@@ -94,7 +97,9 @@ class ComponentGraphicsItem(QGraphicsObject):
position = port.properties.get("iconPosition", {})
item.setPos(
float(position.get("x", x)) * self.WIDTH / self.component.icon.width,
float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height,
float(position.get("y", spacing * index))
* self.HEIGHT
/ self.component.icon.height,
)
result[port.id] = item
return result
@@ -212,6 +217,9 @@ class InterfaceTerminalItem(QGraphicsObject):
super().mousePressEvent(event)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if self.index < 0:
event.accept()
return
menu = QMenu()
options_action = menu.addAction(f"{self.direction.title()} Options…")
if menu.exec(event.screenPos()) is options_action:
@@ -253,13 +261,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.end_direction = QPointF(1, 0)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen()
self.setZValue(-1)
self.setZValue(0)
self.setToolTip(name or "Connection")
self.handles: list[WaypointHandle] = []
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
menu = QMenu()
add_node_action = menu.addAction("Add Node")
menu.addSeparator()
options_action = menu.addAction("Connection Options…")
if menu.exec(event.screenPos()) is options_action:
selected = menu.exec(event.screenPos())
if selected is add_node_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_route_node("connection", self.connection_id, event.scenePos())
elif selected is options_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.connectionOptionsRequested.emit(self.connection_id)
@@ -269,6 +288,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
result = super().itemChange(change, value)
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
self._update_pen()
self._update_handles()
return result
def _update_pen(self) -> None:
@@ -291,6 +311,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2]
def set_waypoints(self, points: list[QPointF]) -> None:
for handle in self.handles:
if handle.scene() is not None:
handle.scene().removeItem(handle)
self.handles = [
WaypointHandle("connection", self.connection_id, index, point)
for index, point in enumerate(points)
]
scene = self.scene()
if scene is not None:
for handle in self.handles:
scene.addItem(handle)
self._update_handles()
def _update_handles(self) -> None:
for handle in self.handles:
handle.setVisible(self.isSelected())
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
@@ -306,7 +344,211 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
if self.style.arrow_at_target:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
if self.style.arrow_at_source:
painter.drawPolygon(self._arrow(self.start, -self.start_direction, self.style.arrow_size))
painter.drawPolygon(
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
)
class WaypointHandle(QGraphicsEllipseItem):
def __init__(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
super().__init__(-5, -5, 10, 10)
self.item_kind, self.item_id, self.index = item_kind, item_id, index
self.setPos(position)
self.setBrush(QColor("#ffffff"))
self.setPen(QPen(QColor("#2563eb"), 1.5))
self.setZValue(1000)
self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
self.setCursor(Qt.CursorShape.SizeAllCursor)
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
snapped = _snapped(value)
scene = self.scene()
if isinstance(scene, GraphScene):
scene.preview_route_node(self.item_kind, self.item_id, self.index, snapped)
return snapped
return super().itemChange(change, value)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
super().mouseReleaseEvent(event)
scene = self.scene()
if isinstance(scene, GraphScene):
scene.move_route_node(self.item_kind, self.item_id, self.index, self.pos())
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
menu = QMenu()
delete_action = menu.addAction("Delete Node")
if menu.exec(event.screenPos()) is delete_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.delete_route_node(self.item_kind, self.item_id, self.index)
event.accept()
class AnnotationGraphicsItem(QGraphicsObject):
def __init__(self, annotation: Annotation, controller: DocumentController) -> None:
super().__init__()
self.annotation = annotation
self.annotation_id = annotation.id
self.controller = controller
self.drag_start = QPointF(annotation.x, annotation.y)
self.setPos(annotation.x, annotation.y)
self.setZValue(annotation.layer)
self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
def boundingRect(self) -> QRectF: # noqa: N802
width, height = self.annotation.width, self.annotation.height
return QRectF(min(0, width), min(0, height), abs(width), abs(height)).adjusted(-4, -4, 4, 4)
def paint(self, painter: QPainter, option, widget=None) -> None:
del option, widget
rect = QRectF(0, 0, self.annotation.width, self.annotation.height).normalized()
painter.setPen(shape_pen(self.annotation.properties))
fill = self.annotation.properties.get("fill", "none")
painter.setBrush(
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
)
if self.annotation.kind == "box":
radius = float(self.annotation.properties.get("cornerRadius", 0))
painter.drawRoundedRect(rect, radius, radius)
else:
painter.setPen(
QColor(
self.annotation.properties.get(
"color", self.annotation.properties.get("stroke", "#202020")
)
)
)
font = painter.font()
font.setPointSizeF(float(self.annotation.properties.get("fontSize", 12)))
painter.setFont(font)
painter.drawText(
rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, self.annotation.text
)
if self.isSelected():
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
painter.drawRect(self.boundingRect())
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.drag_start = self.pos()
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
super().mouseReleaseEvent(event)
new_pos = _snapped(self.pos())
self.setPos(new_pos)
old = {
"x": self.drag_start.x(),
"y": self.drag_start.y(),
"width": self.annotation.width,
"height": self.annotation.height,
}
new = {**old, "x": new_pos.x(), "y": new_pos.y()}
self.controller.set_annotation_geometry(self.annotation_id, old, new)
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
return _snapped(value)
return super().itemChange(change, value)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
scene = self.scene()
if not isinstance(scene, GraphScene):
return
if not self.isSelected():
scene.clearSelection()
self.setSelected(True)
menu = QMenu()
options_action = menu.addAction("Shape Options…")
menu.addSeparator()
actions = {
menu.addAction("Bring Forward"): "forward",
menu.addAction("Send Backward"): "backward",
menu.addAction("Bring to Front"): "front",
menu.addAction("Send to Back"): "back",
}
selected = menu.exec(event.screenPos())
if selected is options_action:
scene.edit_annotation_options(self.annotation_id)
elif selected in actions:
scene.reorder_selected_annotations(actions[selected])
event.accept()
class LineAnnotationGraphicsItem(QGraphicsPathItem):
def __init__(self, annotation: Annotation) -> None:
super().__init__()
self.annotation = annotation
self.annotation_id = annotation.id
self.handles: list[WaypointHandle] = []
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(annotation.layer)
self._update_pen()
def _update_pen(self) -> None:
pen = shape_pen(self.annotation.properties)
if self.isSelected():
pen.setColor(QColor("#2563eb"))
pen.setWidthF(max(3.0, pen.widthF()))
self.setPen(pen)
def set_waypoints(self, points: list[QPointF], start: QPointF, end: QPointF) -> None:
for handle in self.handles:
if handle.scene() is not None:
handle.scene().removeItem(handle)
self.handles = [WaypointHandle("annotation", self.annotation_id, -1, start)]
self.handles.extend(
WaypointHandle("annotation", self.annotation_id, index, point)
for index, point in enumerate(points)
)
self.handles.append(WaypointHandle("annotation", self.annotation_id, -2, end))
scene = self.scene()
if scene is not None:
for handle in self.handles:
scene.addItem(handle)
for handle in self.handles:
handle.setVisible(self.isSelected())
def itemChange(self, change, value): # noqa: N802
result = super().itemChange(change, value)
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
for handle in self.handles:
handle.setVisible(self.isSelected())
self._update_pen()
return result
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
menu = QMenu()
add_action = menu.addAction("Add Node")
options_action = menu.addAction("Shape Options…")
menu.addSeparator()
actions = {
menu.addAction("Bring Forward"): "forward",
menu.addAction("Send Backward"): "backward",
menu.addAction("Bring to Front"): "front",
menu.addAction("Send to Back"): "back",
}
selected = menu.exec(event.screenPos())
scene = self.scene()
if isinstance(scene, GraphScene):
if selected is add_action:
scene.add_route_node("annotation", self.annotation_id, event.scenePos())
elif selected is options_action:
scene.edit_annotation_options(self.annotation_id)
elif selected in actions:
scene.reorder_selected_annotations(actions[selected])
event.accept()
class GraphScene(QGraphicsScene):
@@ -322,17 +564,21 @@ class GraphScene(QGraphicsScene):
self.input_items: dict[str, InterfaceTerminalItem] = {}
self.output_items: dict[str, InterfaceTerminalItem] = {}
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
self.annotation_items: dict[str, QGraphicsItem] = {}
self.pending_source: ConnectionPortItem | None = None
self.pending_waypoints: list[QPointF] = []
self.pending_preview: QGraphicsPathItem | None = None
self.interaction_mode = "pointer"
self.connection_routing = "spline"
self.connection_routing = "angled"
self.drawing_start: QPointF | None = None
self.drawing_waypoints: list[QPointF] = []
self.setSceneRect(-2000, -2000, 4000, 4000)
controller.documentReset.connect(self.rebuild)
controller.activeGraphChanged.connect(self.rebuild)
controller.componentMoved.connect(self.set_component_position)
controller.componentRotated.connect(self.set_component_rotation)
controller.graphItemChanged.connect(self.refresh_graph_item)
self.rebuild()
def rebuild(self) -> None:
@@ -341,6 +587,7 @@ class GraphScene(QGraphicsScene):
self.input_items.clear()
self.output_items.clear()
self.connection_items.clear()
self.annotation_items.clear()
self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
@@ -371,6 +618,16 @@ class GraphScene(QGraphicsScene):
self.addItem(item)
self.connection_items[connection.id] = item
self.update_connection(connection.id)
for annotation in owner.graph.annotations.values():
if annotation.kind == "line":
item = LineAnnotationGraphicsItem(annotation)
self.addItem(item)
self.annotation_items[annotation.id] = item
self.update_annotation(annotation.id)
else:
item = AnnotationGraphicsItem(annotation, self.controller)
self.addItem(item)
self.annotation_items[annotation.id] = item
def set_component_position(self, component_id: str, position: QPointF) -> None:
item = self.component_items.get(component_id)
@@ -383,6 +640,52 @@ class GraphScene(QGraphicsScene):
item.setRotation(rotation)
self.update_connections_for_block(component_id)
def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
if item_kind == "connection":
self.update_connection(item_id)
return
annotation = self.controller.active_graph.annotations.get(item_id)
graphics = self.annotation_items.get(item_id)
if annotation is None or graphics is None:
return
graphics.setZValue(annotation.layer)
if isinstance(graphics, LineAnnotationGraphicsItem):
graphics._update_pen()
self.update_annotation(item_id)
elif isinstance(graphics, AnnotationGraphicsItem):
graphics.prepareGeometryChange()
graphics.setPos(annotation.x, annotation.y)
graphics.update()
def edit_annotation_options(self, annotation_id: str) -> None:
annotation = self.controller.active_graph.annotations.get(annotation_id)
if annotation is None:
return
element = {
"type": {"box": "rectangle", "line": "line", "text": "text"}[annotation.kind],
"width": abs(annotation.width),
"height": abs(annotation.height),
"text": annotation.text,
**annotation.properties,
}
dialog = ShapeOptionsDialog(element)
if dialog.exec() != dialog.DialogCode.Accepted:
return
values = annotation.to_dict()
values["size"] = {
"width": dialog.element["width"] * (-1 if annotation.width < 0 else 1),
"height": dialog.element["height"] * (-1 if annotation.height < 0 else 1),
}
values["text"] = str(dialog.element.get("text", annotation.text))
values["properties"].update(
{
key: value
for key, value in dialog.element.items()
if key not in {"type", "x", "y", "width", "height", "text"}
}
)
self.controller.edit_annotation(annotation_id, values)
def update_connections_for_block(self, component_id: str) -> None:
for connection in self.controller.active_graph.connections.values():
if component_id in (connection.source.block, connection.target.block):
@@ -418,7 +721,7 @@ class GraphScene(QGraphicsScene):
if source is None or target is None:
return
start, end = source.scenePos(), target.scenePos()
routing = str(connection.properties.get("routing", "spline"))
routing = str(connection.properties.get("routing", "angled"))
waypoints = [
QPointF(float(point["x"]), float(point["y"]))
for point in connection.properties.get("waypoints", [])
@@ -426,6 +729,35 @@ class GraphScene(QGraphicsScene):
]
path, direction_points = self._route_path(start, end, routing, waypoints)
graphics.set_connection_path(path, direction_points)
graphics.set_waypoints(waypoints)
def update_annotation(self, annotation_id: str) -> None:
annotation = self.controller.active_graph.annotations.get(annotation_id)
graphics = self.annotation_items.get(annotation_id)
if annotation is None or not isinstance(graphics, LineAnnotationGraphicsItem):
return
start = QPointF(annotation.x, annotation.y)
end = start + QPointF(annotation.width, annotation.height)
waypoints = [
QPointF(float(p["x"]), float(p["y"]))
for p in annotation.properties.get("waypoints", [])
]
routing = str(annotation.properties.get("routing", "angled"))
path, points = self._route_path(start, end, routing, waypoints)
graphics.setPath(path)
graphics.set_waypoints(waypoints, start, end)
@staticmethod
def _orthogonal_points(start: QPointF, end: QPointF, waypoints: list[QPointF]) -> list[QPointF]:
points = [start]
for waypoint in [*waypoints, end]:
previous = points[-1]
if previous.x() != waypoint.x() and previous.y() != waypoint.y():
elbow = QPointF(waypoint.x(), previous.y())
points.append(elbow)
if waypoint != points[-1]:
points.append(waypoint)
return points
@staticmethod
def _route_path(
@@ -437,16 +769,23 @@ class GraphScene(QGraphicsScene):
waypoints = waypoints or []
path = QPainterPath(start)
if routing == "angled":
points = [start, *waypoints, end]
points = GraphScene._orthogonal_points(start, end, waypoints)
for point in points[1:]:
path.lineTo(point)
return path, points
if routing == "direct":
path.lineTo(end)
return path, [start, end]
if waypoints:
points = [start, *waypoints, end]
for index, point in enumerate(points[1:-1], start=1):
following = points[index + 1]
midpoint = QPointF((point.x() + following.x()) / 2, (point.y() + following.y()) / 2)
path.quadTo(point, midpoint)
path.quadTo(points[-2], end)
return path, points
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
first_control = start + QPointF(distance, 0)
second_control = end - QPointF(distance, 0)
first_control, second_control = start + QPointF(distance, 0), end - QPointF(distance, 0)
path.cubicTo(first_control, second_control, end)
return path, [start, first_control, second_control, end]
@@ -454,12 +793,16 @@ class GraphScene(QGraphicsScene):
self.interaction_mode = mode
if mode != "connect":
self._clear_pending_source()
self._clear_drawing()
def set_connection_routing(self, routing: str) -> None:
self.connection_routing = routing
self._clear_pending_source()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode in {"box", "text", "line"}:
self._drawing_press(event)
return
if self.interaction_mode != "connect":
self._clear_pending_source()
super().mousePressEvent(event)
@@ -483,7 +826,15 @@ class GraphScene(QGraphicsScene):
self.pending_source.endpoint,
item.endpoint,
routing=self.connection_routing,
waypoints=self.pending_waypoints,
waypoints=(
self._completed_angled_waypoints(
self.pending_source.scenePos(),
item.scenePos(),
self.pending_waypoints,
)
if self.connection_routing == "angled"
else self.pending_waypoints
),
)
except ValueError as error:
QToolTip.showText(event.screenPos(), str(error))
@@ -500,8 +851,222 @@ class GraphScene(QGraphicsScene):
self._update_preview(event.scenePos())
event.accept()
return
if self.interaction_mode in {"box", "line"} and self.drawing_start is not None:
self._update_drawing_preview(event.scenePos())
event.accept()
return
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode == "box" and self.drawing_start is not None:
end = _snapped(event.scenePos())
if end != self.drawing_start:
self.controller.add_annotation("box", self.drawing_start, end)
self._clear_drawing()
event.accept()
return
super().mouseReleaseEvent(event)
def _drawing_press(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.RightButton:
self._clear_drawing()
event.accept()
return
point = _snapped(event.scenePos())
if self.interaction_mode == "text":
text_value, accepted = QInputDialog.getText(None, "Add Text", "Text:")
if accepted and text_value:
self.controller.add_annotation(
"text", point, point + QPointF(160, 48), text=text_value
)
event.accept()
return
if self.drawing_start is None:
self.drawing_start = point
self.drawing_waypoints = []
self._create_preview()
elif self.interaction_mode == "line" and self.connection_routing == "angled":
self.drawing_waypoints.append(point)
self._update_drawing_preview(point)
elif self.interaction_mode == "line":
self.controller.add_annotation(
"line", self.drawing_start, point, routing=self.connection_routing
)
self._clear_drawing()
event.accept()
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode == "line" and self.drawing_start is not None:
end = _snapped(event.scenePos())
waypoints = (
self.drawing_waypoints[:-1]
if self.drawing_waypoints and self.drawing_waypoints[-1] == end
else self.drawing_waypoints
)
completed = self._completed_angled_waypoints(self.drawing_start, end, waypoints)
self.controller.add_annotation(
"line", self.drawing_start, end, routing="angled", waypoints=completed
)
self._clear_drawing()
event.accept()
return
super().mouseDoubleClickEvent(event)
@staticmethod
def _completed_angled_waypoints(
start: QPointF, end: QPointF, waypoints: list[QPointF]
) -> list[QPointF]:
return GraphScene._orthogonal_points(start, end, waypoints)[1:-1]
def _update_drawing_preview(self, cursor: QPointF) -> None:
if self.drawing_start is None or self.pending_preview is None:
return
end = _snapped(cursor)
if self.interaction_mode == "box":
path = QPainterPath()
path.addRect(QRectF(self.drawing_start, end).normalized())
else:
path, _ = self._route_path(
self.drawing_start, end, self.connection_routing, self.drawing_waypoints
)
self.pending_preview.setPath(path)
def _clear_drawing(self) -> None:
if self.pending_preview is not None and self.pending_source is None:
self.removeItem(self.pending_preview)
self.pending_preview = None
self.drawing_start = None
self.drawing_waypoints.clear()
def _route_values(self, item_kind: str, item_id: str):
item = (
self.controller.active_graph.connections
if item_kind == "connection"
else self.controller.active_graph.annotations
).get(item_id)
if item is None:
return None, []
points = [
QPointF(float(p["x"]), float(p["y"])) for p in item.properties.get("waypoints", [])
]
return item, points
def add_route_node(self, item_kind: str, item_id: str, position: QPointF) -> None:
item, points = self._route_values(item_kind, item_id)
if item is None:
return
snapped = _snapped(position)
if item_kind == "annotation":
start = QPointF(item.x, item.y)
end = QPointF(item.x + item.width, item.y + item.height)
else:
graphics = self.connection_items[item_id]
start, end = graphics.start, graphics.end
anchors = [start, *points, end]
def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float:
delta = second - first
length_squared = delta.x() ** 2 + delta.y() ** 2
if length_squared == 0:
return (point - first).manhattanLength()
ratio = max(
0.0,
min(
1.0,
((point.x() - first.x()) * delta.x() + (point.y() - first.y()) * delta.y())
/ length_squared,
),
)
nearest = first + delta * ratio
return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2
insertion = min(
range(len(anchors) - 1),
key=lambda index: segment_distance(snapped, anchors[index], anchors[index + 1]),
)
points.insert(insertion, snapped)
routing = "angled" if item.properties.get("routing") == "direct" else None
self.controller.set_route_waypoints(item_kind, item_id, points, routing)
def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
item, points = self._route_values(item_kind, item_id)
if item is None:
return
position = _snapped(position)
if item_kind == "annotation" and index in {-1, -2}:
old = {
"x": item.x,
"y": item.y,
"width": item.width,
"height": item.height,
}
start = position if index == -1 else QPointF(item.x, item.y)
end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
new = {
"x": start.x(),
"y": start.y(),
"width": end.x() - start.x(),
"height": end.y() - start.y(),
}
self.controller.set_annotation_geometry(item_id, old, new)
return
if index < len(points):
points[index] = position
self.controller.set_route_waypoints(item_kind, item_id, points)
def preview_route_node(
self, item_kind: str, item_id: str, index: int, position: QPointF
) -> None:
item, points = self._route_values(item_kind, item_id)
if item is None:
return
if item_kind == "annotation" and index in {-1, -2}:
graphics = self.annotation_items.get(item_id)
if not isinstance(graphics, LineAnnotationGraphicsItem):
return
start = position if index == -1 else QPointF(item.x, item.y)
end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height)
path, _direction_points = self._route_path(
start,
end,
str(item.properties.get("routing", "angled")),
points,
)
graphics.setPath(path)
return
if index >= len(points):
return
points[index] = position
routing = str(item.properties.get("routing", "angled"))
if item_kind == "connection":
graphics = self.connection_items.get(item_id)
if graphics is None:
return
path, direction_points = self._route_path(graphics.start, graphics.end, routing, points)
graphics.set_connection_path(path, direction_points)
return
graphics = self.annotation_items.get(item_id)
if not isinstance(graphics, LineAnnotationGraphicsItem):
return
start = QPointF(item.x, item.y)
end = QPointF(item.x + item.width, item.y + item.height)
path, _direction_points = self._route_path(start, end, routing, points)
graphics.setPath(path)
def delete_route_node(self, item_kind: str, item_id: str, index: int) -> None:
_item, points = self._route_values(item_kind, item_id)
if 0 <= index < len(points):
points.pop(index)
self.controller.set_route_waypoints(item_kind, item_id, points)
def reorder_selected_annotations(self, operation: str) -> None:
ids = {
item.annotation_id
for item in self.selectedItems()
if isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem))
}
self.controller.reorder_annotations(ids, operation)
def _create_preview(self) -> None:
self.pending_preview = QGraphicsPathItem()
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
@@ -511,9 +1076,10 @@ class GraphScene(QGraphicsScene):
def _update_preview(self, cursor: QPointF) -> None:
if self.pending_source is None or self.pending_preview is None:
return
preview_cursor = _snapped(cursor) if self.connection_routing == "angled" else cursor
path, _points = self._route_path(
self.pending_source.scenePos(),
cursor,
preview_cursor,
self.connection_routing,
self.pending_waypoints,
)
@@ -622,6 +1188,7 @@ class GraphWorkspaceView(QGraphicsView):
connections: set[str] = set()
inputs: set[str] = set()
outputs: set[str] = set()
annotations: set[str] = set()
for item in self.scene().selectedItems():
if isinstance(item, ComponentGraphicsItem):
blocks.add(item.component_id)
@@ -629,32 +1196,29 @@ class GraphWorkspaceView(QGraphicsView):
connections.add(item.connection_id)
elif isinstance(item, InterfaceTerminalItem):
(inputs if item.direction == "input" else outputs).add(item.port.id)
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
annotations.add(item.annotation_id)
self.controller.delete_selection(blocks, connections, inputs, outputs)
self.controller.delete_annotations(annotations)
def has_selected_components(self) -> bool:
scene = self.scene()
return bool(
scene
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
scene and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems())
)
def has_single_selected_component(self) -> bool:
scene = self.scene()
return bool(
scene
and sum(
isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()
)
== 1
and sum(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) == 1
)
def open_selected_component(self) -> bool:
if self.controller is None or self.scene() is None:
return False
selected = [
item
for item in self.scene().selectedItems()
if isinstance(item, ComponentGraphicsItem)
item for item in self.scene().selectedItems() if isinstance(item, ComponentGraphicsItem)
]
if len(selected) != 1:
return False