Added new connection styles

This commit is contained in:
2026-07-20 12:45:47 +02:00
parent 8fa450d734
commit ee859c4311
9 changed files with 348 additions and 182 deletions

View File

@@ -60,6 +60,12 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Port removal or reorientation must be rejected when it would invalidate an - Port removal or reorientation must be rejected when it would invalidate an
existing connection. existing connection.
- Connections reference port IDs, never port names. - Connections reference port IDs, never port names.
- Graph interaction has separate Pointer and Connect modes. Connections store a
`properties.routing` value (`direct`, `angled`, or `spline`); angled routes
store absolute scene points in `properties.waypoints`.
- Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters.
- Icon editing uses a fixed 128×128 coordinate space. - Icon editing uses a fixed 128×128 coordinate space.
- The visible/selectable component hitbox is calculated from vector elements, - The visible/selectable component hitbox is calculated from vector elements,
not from the complete 128×128 icon canvas. not from the complete 128×128 icon canvas.

View File

@@ -110,7 +110,9 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
- Components, interface terminals, and connections are selectable. Use a rubber - Components, interface terminals, and connections are selectable. Use a rubber
band or Ctrl-click for multiple selection, Delete to remove items, and the band or Ctrl-click for multiple selection, Delete to remove items, and the
standard Cut/Copy/Paste shortcuts to duplicate selected component groups. standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
- Click an output port and then an input port to create a connection. - Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then
click an output and input. Angled connections accept intermediate corner
clicks; right-click cancels an unfinished connection.
- Double-click a graph component to open its owned subgraph; use **Up** to return. - Double-click a graph component to open its owned subgraph; use **Up** to return.
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an - Graph components show **Pointer**, **Input**, and **Output** tools. Select an
interface tool and click the canvas to add a visible internal terminal and a interface tool and click the canvas to add a visible internal terminal and a

View File

@@ -225,7 +225,14 @@ class DocumentController(QObject):
RotateComponentsCommand(self, self.active_component_id, rotations) RotateComponentsCommand(self, self.active_component_id, rotations)
) )
def connect(self, source: Endpoint, target: Endpoint) -> str: def connect(
self,
source: Endpoint,
target: Endpoint,
*,
routing: str = "spline",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None: if self.active_component_id is None:
raise ValueError("There is no active graph") raise ValueError("There is no active graph")
source_port = self._port_for_endpoint(source, "source") source_port = self._port_for_endpoint(source, "source")
@@ -236,7 +243,19 @@ class DocumentController(QObject):
raise ValueError( raise ValueError(
f"Cannot connect {source_port.type!r} to {target_port.type!r}" f"Cannot connect {source_port.type!r} to {target_port.type!r}"
) )
connection = Connection(str(uuid4()), source, target) if routing not in {"direct", "angled", "spline"}:
raise ValueError(f"Unknown connection routing: {routing}")
connection = Connection(
str(uuid4()),
source,
target,
properties={
"routing": routing,
"waypoints": [
{"x": point.x(), "y": point.y()} for point in (waypoints or [])
],
},
)
self.undo_stack.push( self.undo_stack.push(
AddConnectionCommand(self, self.active_component_id, connection) AddConnectionCommand(self, self.active_component_id, connection)
) )
@@ -255,6 +274,10 @@ class DocumentController(QObject):
ports = component.outputs if role == "source" else component.inputs ports = component.outputs if role == "source" else component.inputs
return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None) return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None)
def connection_port_type(self, connection: Connection) -> str:
port = self._port_for_endpoint(connection.source, "source")
return port.type if port is not None else "signal"
def add_interface_port(self, direction: str, position: QPointF) -> str: def add_interface_port(self, direction: str, position: QPointF) -> str:
component = self.active_component component = self.active_component
if component is None or component.implementation_kind != "graph": if component is None or component.implementation_kind != "graph":
@@ -489,10 +512,17 @@ class DocumentController(QObject):
for source in source_connections: for source in source_connections:
if source.source.block not in id_map or source.target.block not in id_map: if source.source.block not in id_map or source.target.block not in id_map:
continue continue
properties = deepcopy(source.properties)
for point in properties.get("waypoints", []):
if isinstance(point, dict):
point["x"] = float(point.get("x", 0)) + offset.x()
point["y"] = float(point.get("y", 0)) + offset.y()
connection = Connection( connection = Connection(
id=str(uuid4()), id=str(uuid4()),
source=Endpoint(block=id_map[source.source.block], port=source.source.port), source=Endpoint(block=id_map[source.source.block], port=source.source.port),
target=Endpoint(block=id_map[source.target.block], port=source.target.port), target=Endpoint(block=id_map[source.target.block], port=source.target.port),
name=source.name,
properties=properties,
) )
connections[connection.id] = connection connections[connection.id] = connection
if blocks: if blocks:

View File

@@ -182,6 +182,12 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.addWidget(self.navigateUpButton) self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
self.graphBreadcrumbLabel = QLabel(self.workspaceHeader) self.graphBreadcrumbLabel = QLabel(self.workspaceHeader)
self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel") self.graphBreadcrumbLabel.setObjectName(u"graphBreadcrumbLabel")
@@ -206,10 +212,39 @@ class Ui_MainWindow(object):
self.pointerToolButton.setObjectName(u"pointerToolButton") self.pointerToolButton.setObjectName(u"pointerToolButton")
self.pointerToolButton.setCheckable(True) self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True) self.pointerToolButton.setChecked(True)
self.pointerToolButton.setAutoExclusive(True)
self.workspaceHeaderLayout.addWidget(self.pointerToolButton) self.workspaceHeaderLayout.addWidget(self.pointerToolButton)
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.routingLabel = QLabel(self.workspaceHeader)
self.routingLabel.setObjectName(u"routingLabel")
self.workspaceHeaderLayout.addWidget(self.routingLabel)
self.directRoutingButton = QToolButton(self.workspaceHeader)
self.directRoutingButton.setObjectName(u"directRoutingButton")
self.directRoutingButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.directRoutingButton)
self.angledRoutingButton = QToolButton(self.workspaceHeader)
self.angledRoutingButton.setObjectName(u"angledRoutingButton")
self.angledRoutingButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
self.splineRoutingButton = QToolButton(self.workspaceHeader)
self.splineRoutingButton.setObjectName(u"splineRoutingButton")
self.splineRoutingButton.setCheckable(True)
self.splineRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
self.workspaceEditorLayout.addWidget(self.workspaceHeader) self.workspaceEditorLayout.addWidget(self.workspaceHeader)
@@ -437,11 +472,20 @@ class Ui_MainWindow(object):
self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None)) self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None)) self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None))
#endif // QT_CONFIG(tooltip)
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
#if QT_CONFIG(tooltip)
self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None)) self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None)) self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None)) self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None)) self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
self.splineRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Spline", None))
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None)) self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None)) self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None)) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))

View File

@@ -0,0 +1,25 @@
from dataclasses import dataclass
from PySide6.QtCore import Qt
@dataclass(frozen=True)
class ConnectionStyle:
color: str = "#285f9e"
selected_color: str = "#f59e0b"
width: float = 2.5
selected_width: float = 4.0
line_style: Qt.PenStyle = Qt.PenStyle.SolidLine
arrow_at_source: bool = False
arrow_at_target: bool = True
arrow_size: float = 10.0
# This is the intentional code-level styling point for every port/connection type.
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
"signal": ConnectionStyle(),
}
def connection_style(port_type: str) -> ConnectionStyle:
return CONNECTION_STYLES.get(port_type, ConnectionStyle())

View File

@@ -10,6 +10,7 @@ from PySide6.QtGui import (
QPainter, QPainter,
QPainterPath, QPainterPath,
QPen, QPen,
QPolygonF,
QTransform, QTransform,
) )
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -32,6 +33,7 @@ from bedit.core.model import Component, Connection, Endpoint, Port
from bedit.gui.controllers.document import DocumentController from bedit.gui.controllers.document import DocumentController
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE 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_renderer import icon_bounds, paint_icon
from bedit.gui.graphics.connection_styles import ConnectionStyle, connection_style
from bedit.gui.preferences import application_settings from bedit.gui.preferences import application_settings
@@ -235,10 +237,20 @@ class InterfaceTerminalItem(QGraphicsObject):
class ConnectionGraphicsItem(QGraphicsPathItem): class ConnectionGraphicsItem(QGraphicsPathItem):
def __init__(self, connection_id: str, name: str = "") -> None: def __init__(
self,
connection_id: str,
name: str = "",
style: ConnectionStyle | None = None,
) -> None:
super().__init__() super().__init__()
self.connection_id = connection_id self.connection_id = connection_id
self.name = name self.name = name
self.style = style or ConnectionStyle()
self.start = QPointF()
self.end = QPointF()
self.start_direction = QPointF(1, 0)
self.end_direction = QPointF(1, 0)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen() self._update_pen()
self.setZValue(-1) self.setZValue(-1)
@@ -262,11 +274,40 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
def _update_pen(self) -> None: def _update_pen(self) -> None:
self.setPen( self.setPen(
QPen( QPen(
QColor("#f59e0b") if self.isSelected() else QColor("#285f9e"), QColor(self.style.selected_color if self.isSelected() else self.style.color),
4.0 if self.isSelected() else 2.5, self.style.selected_width if self.isSelected() else self.style.width,
self.style.line_style,
) )
) )
def set_connection_path(
self,
path: QPainterPath,
points: list[QPointF],
) -> None:
self.setPath(path)
self.start, self.end = points[0], points[-1]
if len(points) > 1:
self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2]
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
unit = QPointF(direction.x() / length, direction.y() / length)
normal = QPointF(-unit.y(), unit.x())
base = end - unit * size
return QPolygonF([end, base + normal * size * 0.45, base - normal * size * 0.45])
def paint(self, painter: QPainter, option, widget=None) -> None:
super().paint(painter, option, widget)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color())
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))
class GraphScene(QGraphicsScene): class GraphScene(QGraphicsScene):
componentOptionsRequested = Signal(str) componentOptionsRequested = Signal(str)
@@ -282,6 +323,10 @@ class GraphScene(QGraphicsScene):
self.output_items: dict[str, InterfaceTerminalItem] = {} self.output_items: dict[str, InterfaceTerminalItem] = {}
self.connection_items: dict[str, ConnectionGraphicsItem] = {} self.connection_items: dict[str, ConnectionGraphicsItem] = {}
self.pending_source: ConnectionPortItem | None = None 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.setSceneRect(-2000, -2000, 4000, 4000) self.setSceneRect(-2000, -2000, 4000, 4000)
controller.documentReset.connect(self.rebuild) controller.documentReset.connect(self.rebuild)
@@ -297,6 +342,8 @@ class GraphScene(QGraphicsScene):
self.output_items.clear() self.output_items.clear()
self.connection_items.clear() self.connection_items.clear()
self.pending_source = None self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
owner = self.controller.active_component owner = self.controller.active_component
if owner is None or owner.implementation_kind != "graph": if owner is None or owner.implementation_kind != "graph":
return return
@@ -316,7 +363,11 @@ class GraphScene(QGraphicsScene):
item.setPos(component.x, component.y) item.setPos(component.x, component.y)
self.component_items[component.id] = item self.component_items[component.id] = item
for connection in owner.graph.connections.values(): for connection in owner.graph.connections.values():
item = ConnectionGraphicsItem(connection.id, connection.name) item = ConnectionGraphicsItem(
connection.id,
connection.name,
connection_style(self.controller.connection_port_type(connection)),
)
self.addItem(item) self.addItem(item)
self.connection_items[connection.id] = item self.connection_items[connection.id] = item
self.update_connection(connection.id) self.update_connection(connection.id)
@@ -367,34 +418,115 @@ class GraphScene(QGraphicsScene):
if source is None or target is None: if source is None or target is None:
return return
start, end = source.scenePos(), target.scenePos() start, end = source.scenePos(), target.scenePos()
distance = max(50.0, abs(end.x() - start.x()) * 0.5) routing = str(connection.properties.get("routing", "spline"))
waypoints = [
QPointF(float(point["x"]), float(point["y"]))
for point in connection.properties.get("waypoints", [])
if isinstance(point, dict) and "x" in point and "y" in point
]
path, direction_points = self._route_path(start, end, routing, waypoints)
graphics.set_connection_path(path, direction_points)
@staticmethod
def _route_path(
start: QPointF,
end: QPointF,
routing: str,
waypoints: list[QPointF] | None = None,
) -> tuple[QPainterPath, list[QPointF]]:
waypoints = waypoints or []
path = QPainterPath(start) path = QPainterPath(start)
path.cubicTo(start + QPointF(distance, 0), end - QPointF(distance, 0), end) if routing == "angled":
graphics.setPath(path) points = [start, *waypoints, end]
for point in points[1:]:
path.lineTo(point)
return path, points
if routing == "direct":
path.lineTo(end)
return path, [start, end]
distance = max(50.0, abs(end.x() - start.x()) * 0.5)
first_control = start + QPointF(distance, 0)
second_control = end - QPointF(distance, 0)
path.cubicTo(first_control, second_control, end)
return path, [start, first_control, second_control, end]
def set_interaction_mode(self, mode: str) -> None:
self.interaction_mode = mode
if mode != "connect":
self._clear_pending_source()
def set_connection_routing(self, routing: str) -> None:
self.connection_routing = routing
self._clear_pending_source()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode != "connect":
self._clear_pending_source()
super().mousePressEvent(event)
return
if event.button() == Qt.MouseButton.RightButton:
self._clear_pending_source()
event.accept()
return
item = self.itemAt(event.scenePos(), QTransform()) item = self.itemAt(event.scenePos(), QTransform())
if isinstance(item, ConnectionPortItem): if isinstance(item, ConnectionPortItem):
if item.role == "source": if item.role == "source":
self._clear_pending_source() self._clear_pending_source()
self.pending_source = item self.pending_source = item
self.pending_waypoints = []
item.setBrush(QColor("#f5b642")) item.setBrush(QColor("#f5b642"))
self._create_preview()
elif self.pending_source is not None: elif self.pending_source is not None:
if self.pending_source.endpoint != item.endpoint: if self.pending_source.endpoint != item.endpoint:
try: try:
self.controller.connect(self.pending_source.endpoint, item.endpoint) self.controller.connect(
self.pending_source.endpoint,
item.endpoint,
routing=self.connection_routing,
waypoints=self.pending_waypoints,
)
except ValueError as error: except ValueError as error:
QToolTip.showText(event.screenPos(), str(error)) QToolTip.showText(event.screenPos(), str(error))
self._clear_pending_source() self._clear_pending_source()
event.accept() event.accept()
return return
self._clear_pending_source() if self.pending_source is not None and self.connection_routing == "angled":
super().mousePressEvent(event) self.pending_waypoints.append(_snapped(event.scenePos()))
self._update_preview(event.scenePos())
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.interaction_mode == "connect" and self.pending_source is not None:
self._update_preview(event.scenePos())
event.accept()
return
super().mouseMoveEvent(event)
def _create_preview(self) -> None:
self.pending_preview = QGraphicsPathItem()
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
self.pending_preview.setZValue(-0.5)
self.addItem(self.pending_preview)
def _update_preview(self, cursor: QPointF) -> None:
if self.pending_source is None or self.pending_preview is None:
return
path, _points = self._route_path(
self.pending_source.scenePos(),
cursor,
self.connection_routing,
self.pending_waypoints,
)
self.pending_preview.setPath(path)
def _clear_pending_source(self) -> None: def _clear_pending_source(self) -> None:
if self.pending_source is not None: if self.pending_source is not None:
self.pending_source.setBrush(QColor("#ffffff")) self.pending_source.setBrush(QColor("#ffffff"))
if self.pending_preview is not None:
self.removeItem(self.pending_preview)
self.pending_source = None self.pending_source = None
self.pending_waypoints.clear()
self.pending_preview = None
class GraphWorkspaceView(QGraphicsView): class GraphWorkspaceView(QGraphicsView):
@@ -411,6 +543,7 @@ class GraphWorkspaceView(QGraphicsView):
self.tool_mode = "pointer" self.tool_mode = "pointer"
self.paste_count = 0 self.paste_count = 0
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.setMouseTracking(True)
self.setRenderHint(QPainter.RenderHint.Antialiasing) self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
self.setBackgroundBrush(QColor("#f7f7f7")) self.setBackgroundBrush(QColor("#f7f7f7"))
@@ -505,6 +638,29 @@ class GraphWorkspaceView(QGraphicsView):
and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) 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
)
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)
]
if len(selected) != 1:
return False
self.controller.activate_component(selected[0].component_id)
return True
def rotate_selected(self) -> None: def rotate_selected(self) -> None:
if self.controller is None or self.scene() is None: if self.controller is None or self.scene() is None:
return return
@@ -578,6 +734,14 @@ class GraphWorkspaceView(QGraphicsView):
if mode == "pointer" if mode == "pointer"
else QGraphicsView.DragMode.NoDrag else QGraphicsView.DragMode.NoDrag
) )
scene = self.scene()
if isinstance(scene, GraphScene):
scene.set_interaction_mode(mode)
def set_connection_routing(self, routing: str) -> None:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.set_connection_routing(routing)
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802 def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
super().mousePressEvent(event) super().mousePressEvent(event)

View File

@@ -4,7 +4,7 @@ from pathlib import Path
from PySide6.QtCore import Qt, Slot from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QCloseEvent from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox from PySide6.QtWidgets import QButtonGroup, QFileDialog, QMainWindow, QMenu, QMessageBox
from bedit.core.model import Component, Port from bedit.core.model import Component, Port
from bedit.core.serializer import JsonDocumentSerializer from bedit.core.serializer import JsonDocumentSerializer
@@ -93,8 +93,31 @@ class MainWindow(QMainWindow):
self.ui.graphView.selectionAvailabilityChanged.connect( self.ui.graphView.selectionAvailabilityChanged.connect(
lambda _available: self._update_edit_actions() lambda _available: self._update_edit_actions()
) )
self.mode_button_group = QButtonGroup(self)
self.mode_button_group.setExclusive(True)
self.mode_button_group.addButton(self.ui.pointerToolButton)
self.mode_button_group.addButton(self.ui.connectToolButton)
self.routing_button_group = QButtonGroup(self)
self.routing_button_group.setExclusive(True)
for button in (
self.ui.directRoutingButton,
self.ui.angledRoutingButton,
self.ui.splineRoutingButton,
):
self.routing_button_group.addButton(button)
self.ui.navigateUpButton.clicked.connect(self.navigate_up) self.ui.navigateUpButton.clicked.connect(self.navigate_up)
self.ui.navigateDownButton.clicked.connect(self.navigate_down)
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer")) self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect"))
self.ui.directRoutingButton.clicked.connect(
lambda: self.set_connection_routing("direct")
)
self.ui.angledRoutingButton.clicked.connect(
lambda: self.set_connection_routing("angled")
)
self.ui.splineRoutingButton.clicked.connect(
lambda: self.set_connection_routing("spline")
)
self.ui.applyJsonButton.clicked.connect(self.apply_json) self.ui.applyJsonButton.clicked.connect(self.apply_json)
self.document_controller.activeGraphChanged.connect(self._active_graph_changed) self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
@@ -173,7 +196,7 @@ class MainWindow(QMainWindow):
self.ui.workspaceModeLabel.setText("") self.ui.workspaceModeLabel.setText("")
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage) self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
self.ui.applyJsonButton.setVisible(False) self.ui.applyJsonButton.setVisible(False)
self.ui.pointerToolButton.setVisible(False) self._set_graph_controls_visible(False)
self._update_edit_actions() self._update_edit_actions()
return return
self.ui.graphBreadcrumbLabel.setText(" ".join(self.document_controller.breadcrumb())) self.ui.graphBreadcrumbLabel.setText(" ".join(self.document_controller.breadcrumb()))
@@ -184,7 +207,7 @@ class MainWindow(QMainWindow):
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage) self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
self.ui.applyJsonButton.setVisible(not is_graph) self.ui.applyJsonButton.setVisible(not is_graph)
self.ui.pointerToolButton.setVisible(is_graph) self._set_graph_controls_visible(is_graph)
if is_graph: if is_graph:
self.set_graph_tool("pointer") self.set_graph_tool("pointer")
else: else:
@@ -202,15 +225,43 @@ class MainWindow(QMainWindow):
) )
self.ui.actionSelectAll.setEnabled(is_graph) self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph) self.ui.actionPaste.setEnabled(is_graph)
self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component()
)
@Slot() @Slot()
def navigate_up(self) -> None: def navigate_up(self) -> None:
if self._resolve_source_edits(): if self._resolve_source_edits():
self.document_controller.navigate_up() self.document_controller.navigate_up()
@Slot()
def navigate_down(self) -> None:
if self._resolve_source_edits():
self.ui.graphView.open_selected_component()
def _set_graph_controls_visible(self, visible: bool) -> None:
for widget in (
self.ui.pointerToolButton,
self.ui.connectToolButton,
self.ui.routingLabel,
self.ui.directRoutingButton,
self.ui.angledRoutingButton,
self.ui.splineRoutingButton,
):
widget.setVisible(visible)
def set_graph_tool(self, mode: str) -> None: def set_graph_tool(self, mode: str) -> None:
self.ui.graphView.set_tool_mode("pointer") self.ui.graphView.set_tool_mode(mode)
self.ui.pointerToolButton.setChecked(True) (self.ui.pointerToolButton if mode == "pointer" else self.ui.connectToolButton).setChecked(True)
def set_connection_routing(self, routing: str) -> None:
self.ui.graphView.set_connection_routing(routing)
buttons = {
"direct": self.ui.directRoutingButton,
"angled": self.ui.angledRoutingButton,
"spline": self.ui.splineRoutingButton,
}
buttons[routing].setChecked(True)
def _load_source_json(self) -> None: def _load_source_json(self) -> None:
component = self.document_controller.active_component component = self.document_controller.active_component

View File

@@ -1,162 +0,0 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Untitled"
},
"roots": [
{
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": []
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 1.0,
"y": 1.0,
"width": 126.0,
"height": 126.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 8.0,
"y": 8.0,
"width": 112.0,
"height": 112.0,
"text": "Graph",
"color": "#202020",
"fontSize": 12.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [
{
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
"name": "New Graph Block 1",
"position": {
"x": 0.0,
"y": -64.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-e5cb81a5",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-eb0034af",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "Graph",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
],
"connections": []
}
}
}
]
}

View File

@@ -144,11 +144,17 @@
<property name="rightMargin"><number>6</number></property> <property name="rightMargin"><number>6</number></property>
<property name="bottomMargin"><number>2</number></property> <property name="bottomMargin"><number>2</number></property>
<item><widget class="QToolButton" name="navigateUpButton"><property name="text"><string>Up</string></property><property name="toolTip"><string>Open the containing graph</string></property></widget></item> <item><widget class="QToolButton" name="navigateUpButton"><property name="text"><string>Up</string></property><property name="toolTip"><string>Open the containing graph</string></property></widget></item>
<item><widget class="QToolButton" name="navigateDownButton"><property name="text"><string>Down</string></property><property name="toolTip"><string>Open the selected block</string></property><property name="enabled"><bool>false</bool></property></widget></item>
<item><widget class="QLabel" name="graphBreadcrumbLabel"><property name="text"><string>Untitled</string></property></widget></item> <item><widget class="QLabel" name="graphBreadcrumbLabel"><property name="text"><string>Untitled</string></property></widget></item>
<item><widget class="QLabel" name="workspaceModeLabel"><property name="text"><string>Graph</string></property></widget></item> <item><widget class="QLabel" name="workspaceModeLabel"><property name="text"><string>Graph</string></property></widget></item>
<item><spacer name="workspaceHeaderSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item> <item><spacer name="workspaceHeaderSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item><widget class="QPushButton" name="applyJsonButton"><property name="text"><string>Apply JSON</string></property><property name="visible"><bool>false</bool></property></widget></item> <item><widget class="QPushButton" name="applyJsonButton"><property name="text"><string>Apply JSON</string></property><property name="visible"><bool>false</bool></property></widget></item>
<item><widget class="QToolButton" name="pointerToolButton"><property name="text"><string>Pointer</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><property name="autoExclusive"><bool>true</bool></property></widget></item> <item><widget class="QToolButton" name="pointerToolButton"><property name="text"><string>Pointer</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="connectToolButton"><property name="text"><string>Connect</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QLabel" name="routingLabel"><property name="text"><string>Line:</string></property></widget></item>
<item><widget class="QToolButton" name="directRoutingButton"><property name="text"><string>Direct</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="angledRoutingButton"><property name="text"><string>Angled</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="splineRoutingButton"><property name="text"><string>Spline</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property></widget></item>
</layout> </layout>
</widget> </widget>
</item> </item>