This commit is contained in:
2026-07-20 14:02:22 +02:00
parent 8f87a3b477
commit bf2512995f
9 changed files with 295 additions and 42 deletions

View File

@@ -200,6 +200,19 @@ class EditComponentAppearanceCommand(QUndoCommand):
self.controller._set_component_appearance(self.component_id, self.old)
class EditComponentPropertiesCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict, text: str) -> None:
super().__init__(text)
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_properties(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_properties(self.component_id, self.old)
class RenameInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
super().__init__("Rename interface port")

View File

@@ -15,6 +15,7 @@ from bedit.gui.controllers.commands import (
EditGraphItemCommand,
EditTextDefinitionCommand,
EditComponentAppearanceCommand,
EditComponentPropertiesCommand,
MoveComponentCommand,
MoveInterfacePortCommand,
PasteSelectionCommand,
@@ -45,6 +46,7 @@ class DocumentController(QObject):
componentRemoved = Signal(str)
componentMoved = Signal(str, QPointF)
componentRotated = Signal(str, float)
componentPropertiesChanged = Signal(str)
connectionAdded = Signal(str)
connectionRemoved = Signal(str)
graphItemChanged = Signal(str, str)
@@ -516,6 +518,7 @@ class DocumentController(QObject):
inputs: list[Port],
outputs: list[Port],
show_subtree: bool,
show_name: bool,
) -> None:
if self.document is None:
return
@@ -528,13 +531,20 @@ class DocumentController(QObject):
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
properties = deepcopy(component.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = {
"name": name,
"icon": icon.to_dict(),
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"show_subtree": show_subtree,
"properties": properties,
}
if old != new:
candidate = deepcopy(self.document)
@@ -543,9 +553,75 @@ class DocumentController(QObject):
candidate_component.icon = Icon.from_dict(icon.to_dict())
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.properties = deepcopy(properties)
candidate.validate()
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
def move_component_name_label(self, component_id: str, position: QPointF) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is None:
return
old = deepcopy(component.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditComponentPropertiesCommand(
self, component_id, old, new, "Move component name"
)
)
def edit_connection_options(
self, connection_id: str, name: str, show_name: bool
) -> None:
owner = self.active_component
if owner is None or self.active_component_id is None:
return
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
old = {"name": connection.name, "properties": deepcopy(connection.properties)}
properties = deepcopy(connection.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = {"name": name, "properties": properties}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection_data",
connection_id,
old,
new,
"Edit connection options",
)
)
def move_connection_name_label(self, connection_id: str, position: QPointF) -> None:
connection = self.active_graph.connections.get(connection_id)
if connection is None or self.active_component_id is None:
return
old = deepcopy(connection.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection",
connection_id,
old,
new,
"Move connection name",
)
)
def edit_component_ports(
self, component_id: str, inputs: list[Port], outputs: list[Port]
) -> None:
@@ -585,6 +661,7 @@ class DocumentController(QObject):
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
new = {
**old,
@@ -750,7 +827,12 @@ class DocumentController(QObject):
self, owner_id: str, item_kind: str, item_id: str, values: dict
) -> None:
graph = self._graph_for(owner_id)
if item_kind == "connection":
if item_kind == "connection_data":
item = graph.connections.get(item_id)
if item is not None:
item.name = values["name"]
item.properties = deepcopy(values["properties"])
elif item_kind == "connection":
item = graph.connections.get(item_id)
if item is not None:
item.properties = deepcopy(values)
@@ -867,10 +949,19 @@ class DocumentController(QObject):
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
component.show_subtree_in_library = values["show_subtree"]
component.properties = deepcopy(values["properties"])
self.documentReset.emit()
if component_id == self.active_component_id:
self.activeGraphChanged.emit()
def _set_component_properties(self, component_id: str, properties: dict) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is not None:
component.properties = deepcopy(properties)
self.componentPropertiesChanged.emit(component_id)
def _delete_items(
self,
owner_id: str | None,

View File

@@ -17,6 +17,7 @@ class ComponentOptionsDialog(QDialog):
self.ui.nameEdit.setText(component.name)
self.ui.editIconButton.clicked.connect(self.edit_icon)
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
self.ui.showNameCheckBox.setChecked(bool(component.properties.get("showName", False)))
def edit_icon(self) -> None:
working = Component.from_dict(self.component.to_dict())

View File

@@ -1,6 +1,7 @@
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QCheckBox,
QFormLayout,
QLineEdit,
QMessageBox,
@@ -11,7 +12,15 @@ from PySide6.QtWidgets import (
class ItemOptionsDialog(QDialog):
"""Small, extensible options dialog shared by ports and connections."""
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None:
def __init__(
self,
title: str,
name: str,
parent=None,
*,
name_required: bool = True,
show_name: bool | None = None,
) -> None:
super().__init__(parent)
self.name_required = name_required
self.setWindowTitle(title)
@@ -21,6 +30,11 @@ class ItemOptionsDialog(QDialog):
self.form = QFormLayout()
self.name_edit = QLineEdit(name, self)
self.form.addRow("Name:", self.name_edit)
self.show_name_check = None
if show_name is not None:
self.show_name_check = QCheckBox("Show name below connection", self)
self.show_name_check.setChecked(show_name)
self.form.addRow("", self.show_name_check)
layout.addLayout(self.form)
buttons = QDialogButtonBox(
@@ -35,6 +49,10 @@ class ItemOptionsDialog(QDialog):
def name(self) -> str:
return self.name_edit.text().strip()
@property
def show_name(self) -> bool:
return bool(self.show_name_check and self.show_name_check.isChecked())
def accept(self) -> None:
if self.name_required and not self.name:
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")

View File

@@ -55,6 +55,11 @@ class Ui_ComponentOptionsDialog(object):
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
self.showNameCheckBox = QCheckBox(ComponentOptionsDialog)
self.showNameCheckBox.setObjectName(u"showNameCheckBox")
self.optionsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.showNameCheckBox)
self.dialogLayout.addLayout(self.optionsForm)
@@ -85,5 +90,6 @@ class Ui_ComponentOptionsDialog(object):
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
#endif // QT_CONFIG(tooltip)
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
self.showNameCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show name below component", None))
# retranslateUi

View File

@@ -23,6 +23,7 @@ from PySide6.QtWidgets import (
QGraphicsScene,
QGraphicsSceneContextMenuEvent,
QGraphicsSceneMouseEvent,
QGraphicsSimpleTextItem,
QGraphicsView,
QApplication,
QMenu,
@@ -78,6 +79,48 @@ class ConnectionPortItem(QGraphicsEllipseItem):
self.setToolTip(label)
class NameLabelItem(QGraphicsSimpleTextItem):
"""Movable italic name label whose position is stored by its owner."""
def __init__(
self,
text: str,
owner_kind: str,
owner_id: str,
controller: DocumentController,
parent: QGraphicsItem | None = None,
) -> None:
super().__init__(text, parent)
self.owner_kind = owner_kind
self.owner_id = owner_id
self.controller = controller
font = self.font()
font.setItalic(True)
self.setFont(font)
self.setBrush(QColor("#303030"))
self.setCursor(Qt.CursorShape.SizeAllCursor)
self.drag_offset = QPointF()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
parent = self.parentItem()
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
self.drag_offset = self.pos() - point
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
parent = self.parentItem()
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
self.setPos(_snapped(point + self.drag_offset))
event.accept()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.owner_kind == "component":
self.controller.move_component_name_label(self.owner_id, self.pos())
else:
self.controller.move_connection_name_label(self.owner_id, self.pos())
event.accept()
class ComponentGraphicsItem(QGraphicsObject):
WIDTH = 128.0
HEIGHT = 128.0
@@ -98,6 +141,31 @@ class ComponentGraphicsItem(QGraphicsObject):
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
self.setTransformOriginPoint(self.hitbox.center())
self.setRotation(component.rotation)
self.name_label: NameLabelItem | None = None
self.sync_name_label(component)
def sync_name_label(self, component: Component) -> None:
if component.properties.get("showName", False):
if self.name_label is None:
self.name_label = NameLabelItem(
component.name, "component", component.id, self.controller, self
)
self.name_label.setText(component.name)
position = component.properties.get("nameLabelPosition")
if isinstance(position, dict):
self.name_label.setPos(float(position["x"]), float(position["y"]))
else:
bounds = self.name_label.boundingRect()
self.name_label.setPos(
self.hitbox.center().x() - bounds.width() / 2,
self.hitbox.bottom() + 6,
)
self.name_label.setRotation(-component.rotation)
elif self.name_label is not None:
if self.name_label.scene() is not None:
self.name_label.setParentItem(None)
self.name_label.scene().removeItem(self.name_label)
self.name_label = None
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
result = {}
@@ -255,13 +323,14 @@ class InterfaceTerminalItem(QGraphicsObject):
class ConnectionGraphicsItem(QGraphicsPathItem):
def __init__(
self,
connection_id: str,
name: str = "",
connection: Connection,
controller: DocumentController,
style: ConnectionStyle | None = None,
) -> None:
super().__init__()
self.connection_id = connection_id
self.name = name
self.connection_id = connection.id
self.name = connection.name
self.controller = controller
self.style = style or ConnectionStyle()
self.start = QPointF()
self.end = QPointF()
@@ -270,8 +339,34 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen()
self.setZValue(0)
self.setToolTip(name or "Connection")
self.setToolTip(connection.name or "Connection")
self.handles: list[WaypointHandle] = []
self.name_label: NameLabelItem | None = None
self.sync_name_label(connection)
def sync_name_label(self, connection: Connection) -> None:
visible = bool(connection.properties.get("showName", False))
if not visible:
if self.name_label is not None and self.name_label.scene() is not None:
self.name_label.setParentItem(None)
self.name_label.scene().removeItem(self.name_label)
self.name_label = None
return
if self.name_label is None:
self.name_label = NameLabelItem(
connection.name, "connection", connection.id, self.controller, self
)
self.name_label.setText(connection.name)
position = connection.properties.get("nameLabelPosition")
if isinstance(position, dict):
self.name_label.setPos(float(position["x"]), float(position["y"]))
elif not self.path().isEmpty():
bounds = self.path().boundingRect()
label_bounds = self.name_label.boundingRect()
self.name_label.setPos(
bounds.center().x() - label_bounds.width() / 2,
bounds.bottom() + 6,
)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if not self.isSelected():
@@ -318,6 +413,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
if len(points) > 1:
self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2]
if self.name_label is not None:
connection = self.controller.active_graph.connections.get(self.connection_id)
if connection is not None:
self.sync_name_label(connection)
def set_waypoints(self, points: list[QPointF]) -> None:
for handle in self.handles:
@@ -713,6 +812,7 @@ class GraphScene(QGraphicsScene):
controller.activeGraphChanged.connect(self.rebuild)
controller.componentMoved.connect(self.set_component_position)
controller.componentRotated.connect(self.set_component_rotation)
controller.componentPropertiesChanged.connect(self.refresh_component_properties)
controller.graphItemChanged.connect(self.refresh_graph_item)
self.rebuild()
@@ -746,8 +846,8 @@ class GraphScene(QGraphicsScene):
self.component_items[component.id] = item
for connection in owner.graph.connections.values():
item = ConnectionGraphicsItem(
connection.id,
connection.name,
connection,
self.controller,
connection_style(self.controller.connection_port_type(connection)),
)
self.addItem(item)
@@ -774,10 +874,22 @@ class GraphScene(QGraphicsScene):
item = self.component_items.get(component_id)
if item is not None:
item.setRotation(rotation)
if item.name_label is not None:
item.name_label.setRotation(-rotation)
self.update_connections_for_block(component_id)
def refresh_component_properties(self, component_id: str) -> None:
component = (
self.controller.document.find_component(component_id)
if self.controller.document
else None
)
item = self.component_items.get(component_id)
if component is not None and item is not None:
item.sync_name_label(component)
def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
if item_kind == "connection":
if item_kind in {"connection", "connection_data"}:
self.update_connection(item_id)
return
annotation = self.controller.active_graph.annotations.get(item_id)
@@ -908,6 +1020,7 @@ class GraphScene(QGraphicsScene):
start, end = geometry
path, direction_points = self._route_path(start, end, waypoints)
graphics.set_connection_path(path, direction_points)
graphics.sync_name_label(connection)
graphics.set_waypoints(waypoints)
def update_annotation(self, annotation_id: str) -> None:

View File

@@ -131,6 +131,7 @@ class MainWindow(QMainWindow):
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
@@ -527,6 +528,7 @@ class MainWindow(QMainWindow):
dialog.edited_inputs,
dialog.edited_outputs,
dialog.ui.showSubtreeCheckBox.isChecked(),
dialog.ui.showNameCheckBox.isChecked(),
)
@Slot(str, str)
@@ -550,9 +552,17 @@ class MainWindow(QMainWindow):
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
dialog = ItemOptionsDialog(
"Connection Options",
connection.name,
self,
name_required=False,
show_name=bool(connection.properties.get("showName", False)),
)
if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.rename_connection(connection_id, dialog.name)
self.document_controller.edit_connection_options(
connection_id, dialog.name, dialog.show_name
)
@Slot()
def show_about(self) -> None: