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

@@ -66,6 +66,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Connection appearance is configured per port type in - Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and `gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters. source/target arrowheads. Do not scatter those constants through painters.
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
They use integer layers below or above graph layer 0. Annotation lines reuse
connection routing and absolute `properties.waypoints` semantics. Graph
annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so
their style fields and rendering must remain aligned.
- 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

@@ -112,26 +112,36 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
standard Cut/Copy/Paste shortcuts to duplicate selected component groups. standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
- Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then - Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then
click an output and input. Angled connections accept intermediate corner click an output and input. Angled connections accept intermediate corner
clicks; right-click cancels an unfinished connection. clicks, use right-angle segments, and snap to the graph snapping grid.
- Double-click a graph component to open its owned subgraph; use **Up** to return. Right-click cancels an unfinished connection.
- Select a routed connection to reveal its draggable nodes. Right-click a line
to add a node, or right-click a node to delete it.
- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines
share the Direct, Angled, and Spline routing controls. Annotation context menus
provide the same shape styling as the icon editor and can move annotations
through integer layers below or above graph layer 0.
- Double-click a graph component or select it and use **Down** 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
corresponding external block port. Interface terminals can be moved afterward. corresponding external block port. Interface terminals can be moved afterward.
- Right-click a component on the canvas or in Current Document to edit its name, - Right-click a component on the canvas or in Current Document to edit its name,
icon shape, icon text, fill color, and border color. The same dialog can hide icon shape, icon text, fill color, and border color. The same dialog can hide
that component's contained subtree from the Libraries tree. that component's contained subtree from the Libraries tree.
- The icon editor uses Pointer and click-drag drawing tools. Its toolbar and
mouse wheel provide zoom in, zoom out, and fit-to-canvas controls.
- Double-click a text component to edit its input list, output list, and - Double-click a text component to edit its input list, output list, and
`implementation.source` JSON. `implementation.source` JSON.
- Right-click any graph component under Current Document to add nested graph or - Right-click any graph component under Current Document to add nested graph or
text blocks. Any current-document component can also be deleted there. text blocks. Any current-document component can also be deleted there.
- The active document hierarchy has its own Document panel; the Libraries panel - The left navigator presents the current Document and external Libraries as tabs.
contains only configured external libraries. - Select one or more blocks and press `Ctrl+R`, or use **Rotate** in the graph header, to
- Select one or more blocks and press `Ctrl+R`, or use the Transform toolbar, to
rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo. rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo.
**Apply JSON** updates that source and participates in undo/redo. **Apply JSON** updates that source and participates in undo/redo.
- File → Save writes the complete recursive document to JSON. - File → Save writes the complete recursive document to JSON.
- File → Close Document removes the active document and returns to an empty - File → Close Document removes the active document and returns to an empty
workspace. An open graph uses a light gray, 32-unit dotted canvas. workspace. An open graph uses a light gray grid whose visible and snapping
spacing are configured separately.
- Edit → Settings → Libraries accepts document files or folders of JSON files. - Edit → Settings → Libraries accepts document files or folders of JSON files.
Every component owns its ports, declarative icon, properties, and child graph: Every component owns its ports, declarative icon, properties, and child graph:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,14 +1,25 @@
<RCC> <RCC>
<qresource prefix="icons"> <qresource prefix="icons">
<file>icons/arrow-down.png</file>
<file>icons/configure.png</file>
<file>icons/arrow-up.png</file>
<file>icons/document-new.png</file>
<file>icons/document-open.png</file>
<file>icons/document-save-as.png</file>
<file>icons/document-save.png</file>
<file>icons/draw-bezier-curves.png</file>
<file>icons/draw-rectangle.png</file>
<file>icons/draw-text.png</file>
<file>icons/edit-copy.png</file>
<file>icons/edit-cut.png</file>
<file>icons/edit-paste.png</file> <file>icons/edit-paste.png</file>
<file>icons/edit-redo.png</file> <file>icons/edit-redo.png</file>
<file>icons/edit-cut.png</file> <file>icons/edit-select.png</file>
<file>icons/edit-copy.png</file>
<file>icons/edit-undo.png</file> <file>icons/edit-undo.png</file>
<file>icons/document-save.png</file> <file>icons/media-playback-start.png</file>
<file>icons/document-save-as.png</file>
<file>icons/document-open.png</file>
<file>icons/document-new.png</file>
<file>icons/transform-rotate.png</file> <file>icons/transform-rotate.png</file>
<file>icons/zoom-in.png</file>
<file>icons/zoom-original.png</file>
<file>icons/zoom-out.png</file>
</qresource> </qresource>
</RCC> </RCC>

File diff suppressed because it is too large Load Diff

View File

@@ -51,18 +51,33 @@ class Icon:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self.elements: if not self.elements:
self.elements = [{ self.elements = [
"type": "ellipse" if self.shape == "ellipse" else "rectangle", {
"x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0, "type": "ellipse" if self.shape == "ellipse" else "rectangle",
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5, "x": 32.0,
"lineStyle": "solid", "cornerRadius": 5.0, "y": 32.0,
}] "width": 64.0,
"height": 64.0,
"fill": self.fill,
"stroke": self.border,
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0,
}
]
if self.text: if self.text:
self.elements.append({ self.elements.append(
"type": "text", "x": 40.0, "y": 40.0, {
"width": 48.0, "height": 48.0, "type": "text",
"text": self.text, "color": "#202020", "fontSize": 12.0, "x": 40.0,
}) "y": 40.0,
"width": 48.0,
"height": 48.0,
"text": self.text,
"color": "#202020",
"fontSize": 12.0,
}
)
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -137,15 +152,57 @@ class Connection:
) )
@dataclass
class Annotation:
id: str
kind: str
x: float = 0.0
y: float = 0.0
width: float = 0.0
height: float = 0.0
text: str = ""
layer: int = -1
properties: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"kind": self.kind,
"position": {"x": self.x, "y": self.y},
"size": {"width": self.width, "height": self.height},
"text": self.text,
"layer": self.layer,
"properties": deepcopy(self.properties),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Annotation":
position = data.get("position", {})
size = data.get("size", {})
return cls(
id=str(data["id"]),
kind=str(data["kind"]),
x=float(position.get("x", 0)),
y=float(position.get("y", 0)),
width=float(size.get("width", 0)),
height=float(size.get("height", 0)),
text=str(data.get("text", "")),
layer=int(data.get("layer", -1)),
properties=deepcopy(data.get("properties", {})),
)
@dataclass @dataclass
class Graph: class Graph:
blocks: dict[str, Component] = field(default_factory=dict) blocks: dict[str, Component] = field(default_factory=dict)
connections: dict[str, Connection] = field(default_factory=dict) connections: dict[str, Connection] = field(default_factory=dict)
annotations: dict[str, Annotation] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
"blocks": [block.to_dict() for block in self.blocks.values()], "blocks": [block.to_dict() for block in self.blocks.values()],
"connections": [connection.to_dict() for connection in self.connections.values()], "connections": [connection.to_dict() for connection in self.connections.values()],
"annotations": [item.to_dict() for item in self.annotations.values()],
} }
@classmethod @classmethod
@@ -153,13 +210,19 @@ class Graph:
data = data or {} data = data or {}
blocks = [Component.from_dict(item) for item in data.get("blocks", [])] blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
connections = [Connection.from_dict(item) for item in data.get("connections", [])] connections = [Connection.from_dict(item) for item in data.get("connections", [])]
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
if len({block.id for block in blocks}) != len(blocks): if len({block.id for block in blocks}) != len(blocks):
raise ValueError("A graph contains duplicate component IDs") raise ValueError("A graph contains duplicate component IDs")
if len({connection.id for connection in connections}) != len(connections): if len({connection.id for connection in connections}) != len(connections):
raise ValueError("A graph contains duplicate connection IDs") raise ValueError("A graph contains duplicate connection IDs")
if len({item.id for item in annotations}) != len(annotations):
raise ValueError("A graph contains duplicate annotation IDs")
if any(item.kind not in {"box", "line", "text"} for item in annotations):
raise ValueError("A graph contains an unknown annotation kind")
return cls( return cls(
blocks={block.id: block for block in blocks}, blocks={block.id: block for block in blocks},
connections={connection.id: connection for connection in connections}, connections={connection.id: connection for connection in connections},
annotations={item.id: item for item in annotations},
) )
@@ -349,14 +412,35 @@ def clone_component(source: Component) -> Component:
for connection in current.graph.connections.values() for connection in current.graph.connections.values()
for new_id in [str(uuid4())] for new_id in [str(uuid4())]
}, },
annotations={
new_id: Annotation(
new_id,
annotation.kind,
annotation.x,
annotation.y,
annotation.width,
annotation.height,
annotation.text,
annotation.layer,
deepcopy(annotation.properties),
)
for annotation in current.graph.annotations.values()
for new_id in [str(uuid4())]
},
) )
return Component( return Component(
id=str(uuid4()), id=str(uuid4()),
name=current.name, name=current.name,
x=current.x, x=current.x,
y=current.y, y=current.y,
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.inputs], inputs=[
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.outputs], Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
for port in current.inputs
],
outputs=[
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
for port in current.outputs
],
icon=Icon.from_dict(current.icon.to_dict()), icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties), properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind, implementation_kind=current.implementation_kind,

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand from PySide6.QtGui import QUndoCommand
from bedit.core.model import Component, Connection, Port from bedit.core.model import Annotation, Component, Connection, Port
class AddComponentCommand(QUndoCommand): class AddComponentCommand(QUndoCommand):
@@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand):
self.controller._remove_connection(self.owner_id, self.connection.id) self.controller._remove_connection(self.owner_id, self.connection.id)
class AddAnnotationCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
super().__init__(f"Draw {annotation.kind}")
self.controller, self.owner_id, self.annotation = controller, owner_id, annotation
def redo(self) -> None:
self.controller._insert_annotation(self.owner_id, self.annotation)
def undo(self) -> None:
self.controller._remove_annotation(self.owner_id, self.annotation.id)
class EditGraphItemCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
item_kind: str,
item_id: str,
old: dict,
new: dict,
text: str,
) -> None:
super().__init__(text)
self.controller, self.owner_id = controller, owner_id
self.item_kind, self.item_id = item_kind, item_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new)
def undo(self) -> None:
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
class DeleteAnnotationsCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
super().__init__("Delete annotations")
self.controller, self.owner_id, self.annotations = controller, owner_id, annotations
def redo(self) -> None:
for annotation_id in self.annotations:
self.controller._remove_annotation(self.owner_id, annotation_id)
def undo(self) -> None:
for annotation in self.annotations.values():
self.controller._insert_annotation(self.owner_id, annotation)
class ReplaceComponentCommand(QUndoCommand): class ReplaceComponentCommand(QUndoCommand):
def __init__(self, controller, old: Component, new: Component) -> None: def __init__(self, controller, old: Component, new: Component) -> None:
super().__init__("Apply JSON changes") super().__init__("Apply JSON changes")

View File

@@ -8,8 +8,11 @@ from PySide6.QtGui import QUndoStack
from bedit.gui.controllers.commands import ( from bedit.gui.controllers.commands import (
AddComponentCommand, AddComponentCommand,
AddConnectionCommand, AddConnectionCommand,
AddAnnotationCommand,
AddInterfacePortCommand, AddInterfacePortCommand,
DeleteSelectionCommand, DeleteSelectionCommand,
DeleteAnnotationsCommand,
EditGraphItemCommand,
EditTextDefinitionCommand, EditTextDefinitionCommand,
EditComponentAppearanceCommand, EditComponentAppearanceCommand,
MoveComponentCommand, MoveComponentCommand,
@@ -21,6 +24,7 @@ from bedit.gui.controllers.commands import (
RotateComponentsCommand, RotateComponentsCommand,
) )
from bedit.core.model import ( from bedit.core.model import (
Annotation,
Component, Component,
Connection, Connection,
Endpoint, Endpoint,
@@ -43,6 +47,9 @@ class DocumentController(QObject):
componentRotated = Signal(str, float) componentRotated = Signal(str, float)
connectionAdded = Signal(str) connectionAdded = Signal(str)
connectionRemoved = Signal(str) connectionRemoved = Signal(str)
graphItemChanged = Signal(str, str)
annotationAdded = Signal(str)
annotationRemoved = Signal(str)
interfaceChanged = Signal() interfaceChanged = Signal()
filePathChanged = Signal(object) filePathChanged = Signal(object)
modifiedChanged = Signal(bool) modifiedChanged = Signal(bool)
@@ -221,16 +228,14 @@ class DocumentController(QObject):
if (component := self.active_component.graph.blocks.get(component_id)) is not None if (component := self.active_component.graph.blocks.get(component_id)) is not None
} }
if rotations: if rotations:
self.undo_stack.push( self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
RotateComponentsCommand(self, self.active_component_id, rotations)
)
def connect( def connect(
self, self,
source: Endpoint, source: Endpoint,
target: Endpoint, target: Endpoint,
*, *,
routing: str = "spline", routing: str = "angled",
waypoints: list[QPointF] | None = None, waypoints: list[QPointF] | None = None,
) -> str: ) -> str:
if self.active_component_id is None: if self.active_component_id is None:
@@ -240,9 +245,7 @@ class DocumentController(QObject):
if source_port is None or target_port is None: if source_port is None or target_port is None:
raise ValueError("A connection endpoint no longer exists") raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type): if not PortTypeRegistry.compatible(source_port.type, target_port.type):
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}"
)
if routing not in {"direct", "angled", "spline"}: if routing not in {"direct", "angled", "spline"}:
raise ValueError(f"Unknown connection routing: {routing}") raise ValueError(f"Unknown connection routing: {routing}")
connection = Connection( connection = Connection(
@@ -251,16 +254,154 @@ class DocumentController(QObject):
target, target,
properties={ properties={
"routing": routing, "routing": routing,
"waypoints": [ "waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
{"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)
)
return connection.id return connection.id
def add_annotation(
self,
kind: str,
start: QPointF,
end: QPointF,
*,
text: str = "",
routing: str = "angled",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None:
raise ValueError("There is no active graph")
if kind != "line":
left, right = sorted((start.x(), end.x()))
top, bottom = sorted((start.y(), end.y()))
start, end = QPointF(left, top), QPointF(right, bottom)
style = {
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"fill": "none" if kind in {"line", "text"} else "#dbeafe",
}
if kind == "box":
style["cornerRadius"] = 0.0
if kind == "text":
style.update({"fontSize": 12.0, "color": "#202020"})
annotation = Annotation(
id=str(uuid4()),
kind=kind,
x=start.x(),
y=start.y(),
width=end.x() - start.x(),
height=end.y() - start.y(),
text=text,
layer=-1,
properties={
**style,
"routing": routing,
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
)
self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
return annotation.id
def set_route_waypoints(
self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
) -> None:
item = (
self.active_graph.connections
if item_kind == "connection"
else self.active_graph.annotations
).get(item_id)
if item is None or self.active_component_id is None:
return
old = deepcopy(item.properties)
new = deepcopy(old)
if routing is not None:
new["routing"] = routing
new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes"
)
)
def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None:
if old != new and self.active_component_id is not None:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"annotation_geometry",
annotation_id,
old,
new,
"Move annotation",
)
)
def edit_annotation(self, annotation_id: str, values: dict) -> None:
annotation = self.active_graph.annotations.get(annotation_id)
if annotation is None or self.active_component_id is None:
return
old = annotation.to_dict()
if old != values:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"annotation_data",
annotation_id,
old,
deepcopy(values),
"Edit shape",
)
)
def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None:
if not annotation_ids or self.active_component_id is None:
return
graph = self.active_graph
layers = [item.layer for item in graph.annotations.values()]
minimum, maximum = min(layers, default=-1), max(layers, default=1)
for annotation_id in annotation_ids:
item = graph.annotations.get(annotation_id)
if item is None:
continue
old = {"layer": item.layer}
forward = item.layer + 1
backward = item.layer - 1
if forward == 0:
forward = 1
if backward == 0:
backward = -1
layer = {
"forward": forward,
"backward": backward,
"front": max(1, maximum + 1),
"back": min(-1, minimum - 1),
}[operation]
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"annotation_layer",
annotation_id,
old,
{"layer": layer},
"Reorder annotation",
)
)
def delete_annotations(self, annotation_ids: set[str]) -> None:
items = {
key: self.active_graph.annotations[key]
for key in annotation_ids
if key in self.active_graph.annotations
}
if items and self.active_component_id is not None:
self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items))
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None: def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
owner = self.active_component owner = self.active_component
if owner is None: if owner is None:
@@ -272,7 +413,9 @@ class DocumentController(QObject):
if component is None: if component is None:
return None return None
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: def connection_port_type(self, connection: Connection) -> str:
port = self._port_for_endpoint(connection.source, "source") port = self._port_for_endpoint(connection.source, "source")
@@ -289,9 +432,7 @@ class DocumentController(QObject):
x=position.x(), x=position.x(),
y=position.y(), y=position.y(),
) )
self.undo_stack.push( self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
AddInterfacePortCommand(self, component.id, direction, port)
)
return port.id return port.id
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None: def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
@@ -350,11 +491,17 @@ class DocumentController(QObject):
parent = self.document.find_parent(component.id) parent = self.document.find_parent(component.id)
if parent is not None: if parent is not None:
for connection in parent.graph.connections.values(): for connection in parent.graph.connections.values():
if connection.target.block == component.id and connection.target.port not in input_ids: if (
connection.target.block == component.id
and connection.target.port not in input_ids
):
raise ValueError( raise ValueError(
f"Input {connection.target.port!r} is still connected in the containing graph" f"Input {connection.target.port!r} is still connected in the containing graph"
) )
if connection.source.block == component.id and connection.source.port not in output_ids: if (
connection.source.block == component.id
and connection.source.port not in output_ids
):
raise ValueError( raise ValueError(
f"Output {connection.source.port!r} is still connected in the containing graph" f"Output {connection.source.port!r} is still connected in the containing graph"
) )
@@ -422,9 +569,15 @@ class DocumentController(QObject):
parent = self.document.find_parent(component_id) parent = self.document.find_parent(component_id)
if parent is not None: if parent is not None:
for connection in parent.graph.connections.values(): for connection in parent.graph.connections.values():
if connection.target.block == component_id and connection.target.port not in input_ids: if (
connection.target.block == component_id
and connection.target.port not in input_ids
):
raise ValueError("An input cannot be removed or reoriented while connected") raise ValueError("An input cannot be removed or reoriented while connected")
if connection.source.block == component_id and connection.source.port not in output_ids: if (
connection.source.block == component_id
and connection.source.port not in output_ids
):
raise ValueError("An output cannot be removed or reoriented while connected") raise ValueError("An output cannot be removed or reoriented while connected")
for connection in component.graph.connections.values(): for connection in component.graph.connections.values():
if connection.source.interface and connection.source.interface not in input_ids: if connection.source.interface and connection.source.interface not in input_ids:
@@ -471,7 +624,9 @@ class DocumentController(QObject):
or connection.target.interface in output_ids or connection.target.interface in output_ids
): ):
all_connection_ids.add(connection.id) all_connection_ids.add(connection.id)
blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks} blocks = {
block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks
}
connections = { connections = {
connection_id: graph.connections[connection_id] connection_id: graph.connections[connection_id]
for connection_id in all_connection_ids for connection_id in all_connection_ids
@@ -526,9 +681,7 @@ class DocumentController(QObject):
) )
connections[connection.id] = connection connections[connection.id] = connection
if blocks: if blocks:
self.undo_stack.push( self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
PasteSelectionCommand(self, owner.id, blocks, connections)
)
return list(blocks) return list(blocks)
def _graph_for(self, owner_id: str): def _graph_for(self, owner_id: str):
@@ -571,9 +724,7 @@ class DocumentController(QObject):
self.componentMoved.emit(component_id, position) self.componentMoved.emit(component_id, position)
self.documentReset.emit() self.documentReset.emit()
def _rotate_component( def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None:
self, owner_id: str, component_id: str, rotation: float
) -> None:
component = self._graph_for(owner_id).blocks.get(component_id) component = self._graph_for(owner_id).blocks.get(component_id)
if component is None: if component is None:
return return
@@ -593,6 +744,46 @@ class DocumentController(QObject):
self.connectionRemoved.emit(connection_id) self.connectionRemoved.emit(connection_id)
self.documentReset.emit() self.documentReset.emit()
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
self._graph_for(owner_id).annotations[annotation.id] = annotation
if owner_id == self.active_component_id:
self.annotationAdded.emit(annotation.id)
self.documentReset.emit()
def _remove_annotation(self, owner_id: str, annotation_id: str) -> None:
self._graph_for(owner_id).annotations.pop(annotation_id, None)
if owner_id == self.active_component_id:
self.annotationRemoved.emit(annotation_id)
self.documentReset.emit()
def _set_graph_item_data(
self, owner_id: str, item_kind: str, item_id: str, values: dict
) -> None:
graph = self._graph_for(owner_id)
if item_kind == "connection":
item = graph.connections.get(item_id)
if item is not None:
item.properties = deepcopy(values)
else:
item = graph.annotations.get(item_id)
if item is not None:
if item_kind == "annotation_geometry":
item.x, item.y = float(values["x"]), float(values["y"])
item.width, item.height = float(values["width"]), float(values["height"])
elif item_kind == "annotation_layer":
item.layer = int(values["layer"])
elif item_kind == "annotation_data":
replacement = Annotation.from_dict(values)
item.kind = replacement.kind
item.x, item.y = replacement.x, replacement.y
item.width, item.height = replacement.width, replacement.height
item.text, item.layer = replacement.text, replacement.layer
item.properties = replacement.properties
else:
item.properties = deepcopy(values)
if owner_id == self.active_component_id:
self.graphItemChanged.emit(item_kind, item_id)
def _replace_component(self, old_id: str, replacement: Component) -> None: def _replace_component(self, old_id: str, replacement: Component) -> None:
if self.document is None: if self.document is None:
return return

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,13 @@ class Ui_IconEditorDialog(object):
self.dialogLayout.setObjectName(u"dialogLayout") self.dialogLayout.setObjectName(u"dialogLayout")
self.shapeToolbarLayout = QHBoxLayout() self.shapeToolbarLayout = QHBoxLayout()
self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout") self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout")
self.pointerButton = QToolButton(IconEditorDialog)
self.pointerButton.setObjectName(u"pointerButton")
self.pointerButton.setCheckable(True)
self.pointerButton.setChecked(True)
self.shapeToolbarLayout.addWidget(self.pointerButton)
self.addShapeLabel = QLabel(IconEditorDialog) self.addShapeLabel = QLabel(IconEditorDialog)
self.addShapeLabel.setObjectName(u"addShapeLabel") self.addShapeLabel.setObjectName(u"addShapeLabel")
@@ -38,31 +45,37 @@ class Ui_IconEditorDialog(object):
self.addRectangleButton = QToolButton(IconEditorDialog) self.addRectangleButton = QToolButton(IconEditorDialog)
self.addRectangleButton.setObjectName(u"addRectangleButton") self.addRectangleButton.setObjectName(u"addRectangleButton")
self.addRectangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addRectangleButton) self.shapeToolbarLayout.addWidget(self.addRectangleButton)
self.addCircleButton = QToolButton(IconEditorDialog) self.addCircleButton = QToolButton(IconEditorDialog)
self.addCircleButton.setObjectName(u"addCircleButton") self.addCircleButton.setObjectName(u"addCircleButton")
self.addCircleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addCircleButton) self.shapeToolbarLayout.addWidget(self.addCircleButton)
self.addEllipseButton = QToolButton(IconEditorDialog) self.addEllipseButton = QToolButton(IconEditorDialog)
self.addEllipseButton.setObjectName(u"addEllipseButton") self.addEllipseButton.setObjectName(u"addEllipseButton")
self.addEllipseButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addEllipseButton) self.shapeToolbarLayout.addWidget(self.addEllipseButton)
self.addLineButton = QToolButton(IconEditorDialog) self.addLineButton = QToolButton(IconEditorDialog)
self.addLineButton.setObjectName(u"addLineButton") self.addLineButton.setObjectName(u"addLineButton")
self.addLineButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addLineButton) self.shapeToolbarLayout.addWidget(self.addLineButton)
self.addTriangleButton = QToolButton(IconEditorDialog) self.addTriangleButton = QToolButton(IconEditorDialog)
self.addTriangleButton.setObjectName(u"addTriangleButton") self.addTriangleButton.setObjectName(u"addTriangleButton")
self.addTriangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTriangleButton) self.shapeToolbarLayout.addWidget(self.addTriangleButton)
self.addTextButton = QToolButton(IconEditorDialog) self.addTextButton = QToolButton(IconEditorDialog)
self.addTextButton.setObjectName(u"addTextButton") self.addTextButton.setObjectName(u"addTextButton")
self.addTextButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTextButton) self.shapeToolbarLayout.addWidget(self.addTextButton)
@@ -70,6 +83,21 @@ class Ui_IconEditorDialog(object):
self.shapeToolbarLayout.addItem(self.toolbarSpacer) self.shapeToolbarLayout.addItem(self.toolbarSpacer)
self.zoomInButton = QToolButton(IconEditorDialog)
self.zoomInButton.setObjectName(u"zoomInButton")
self.shapeToolbarLayout.addWidget(self.zoomInButton)
self.zoomOutButton = QToolButton(IconEditorDialog)
self.zoomOutButton.setObjectName(u"zoomOutButton")
self.shapeToolbarLayout.addWidget(self.zoomOutButton)
self.centerButton = QToolButton(IconEditorDialog)
self.centerButton.setObjectName(u"centerButton")
self.shapeToolbarLayout.addWidget(self.centerButton)
self.deleteSelectedButton = QPushButton(IconEditorDialog) self.deleteSelectedButton = QPushButton(IconEditorDialog)
self.deleteSelectedButton.setObjectName(u"deleteSelectedButton") self.deleteSelectedButton.setObjectName(u"deleteSelectedButton")
@@ -106,6 +134,7 @@ class Ui_IconEditorDialog(object):
def retranslateUi(self, IconEditorDialog): def retranslateUi(self, IconEditorDialog):
IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None)) IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None))
self.pointerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Pointer", None))
self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None)) self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None))
self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None)) self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None))
self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None)) self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None))
@@ -113,6 +142,18 @@ class Ui_IconEditorDialog(object):
self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None)) self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None))
self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None)) self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None))
self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None)) self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None))
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
#if QT_CONFIG(tooltip)
self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
#endif // QT_CONFIG(tooltip)
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
#if QT_CONFIG(tooltip)
self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
#endif // QT_CONFIG(tooltip)
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
#if QT_CONFIG(tooltip)
self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
#endif // QT_CONFIG(tooltip)
self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None)) self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None))
self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None)) self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None))
# retranslateUi # retranslateUi

View File

@@ -29,7 +29,7 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow): def setupUi(self, MainWindow):
if not MainWindow.objectName(): if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow") MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(1000, 700) MainWindow.resize(1209, 777)
self.actionNew = QAction(MainWindow) self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew") self.actionNew.setObjectName(u"actionNew")
icon = QIcon() icon = QIcon()
@@ -42,54 +42,63 @@ class Ui_MainWindow(object):
self.actionRotateClockwise.setIcon(icon1) self.actionRotateClockwise.setIcon(icon1)
self.actionZoomIn = QAction(MainWindow) self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn") self.actionZoomIn.setObjectName(u"actionZoomIn")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon2)
self.actionZoomOut = QAction(MainWindow) self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut") self.actionZoomOut.setObjectName(u"actionZoomOut")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon3)
self.actionCenterView = QAction(MainWindow) self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView") self.actionCenterView.setObjectName(u"actionCenterView")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon4)
self.actionOpen = QAction(MainWindow) self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen") self.actionOpen.setObjectName(u"actionOpen")
icon2 = QIcon() icon5 = QIcon()
icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon2) self.actionOpen.setIcon(icon5)
self.actionSave = QAction(MainWindow) self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave") self.actionSave.setObjectName(u"actionSave")
icon3 = QIcon() icon6 = QIcon()
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon3) self.actionSave.setIcon(icon6)
self.actionSaveAs = QAction(MainWindow) self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs") self.actionSaveAs.setObjectName(u"actionSaveAs")
icon4 = QIcon() icon7 = QIcon()
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon4) self.actionSaveAs.setIcon(icon7)
self.actionExit = QAction(MainWindow) self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit") self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow) self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose") self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow) self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo") self.actionUndo.setObjectName(u"actionUndo")
icon5 = QIcon() icon8 = QIcon()
icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon5) self.actionUndo.setIcon(icon8)
self.actionRedo = QAction(MainWindow) self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo") self.actionRedo.setObjectName(u"actionRedo")
icon6 = QIcon() icon9 = QIcon()
icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon6) self.actionRedo.setIcon(icon9)
self.actionCut = QAction(MainWindow) self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut") self.actionCut.setObjectName(u"actionCut")
icon7 = QIcon() icon10 = QIcon()
icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon7) self.actionCut.setIcon(icon10)
self.actionCopy = QAction(MainWindow) self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy") self.actionCopy.setObjectName(u"actionCopy")
icon8 = QIcon() icon11 = QIcon()
icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon8) self.actionCopy.setIcon(icon11)
self.actionPaste = QAction(MainWindow) self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste") self.actionPaste.setObjectName(u"actionPaste")
icon9 = QIcon() icon12 = QIcon()
icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon9) self.actionPaste.setIcon(icon12)
self.actionSelectAll = QAction(MainWindow) self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll") self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow) self.actionDelete = QAction(MainWindow)
@@ -129,9 +138,9 @@ class Ui_MainWindow(object):
self.librariesLayout.setContentsMargins(0, 0, 0, 0) self.librariesLayout.setContentsMargins(0, 0, 0, 0)
self.treeView = QTreeView(self.dockWidgetContents) self.treeView = QTreeView(self.dockWidgetContents)
self.treeView.setObjectName(u"treeView") self.treeView.setObjectName(u"treeView")
self.treeView.setIconSize(QSize(28, 28))
self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }") self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }")
self.treeView.setAlternatingRowColors(True) self.treeView.setAlternatingRowColors(True)
self.treeView.setIconSize(QSize(28, 28))
self.treeView.setUniformRowHeights(True) self.treeView.setUniformRowHeights(True)
self.librariesLayout.addWidget(self.treeView) self.librariesLayout.addWidget(self.treeView)
@@ -149,8 +158,8 @@ class Ui_MainWindow(object):
self.documentPanelLayout.setContentsMargins(0, 0, 0, 0) self.documentPanelLayout.setContentsMargins(0, 0, 0, 0)
self.documentTreeView = QTreeView(self.documentDockContents) self.documentTreeView = QTreeView(self.documentDockContents)
self.documentTreeView.setObjectName(u"documentTreeView") self.documentTreeView.setObjectName(u"documentTreeView")
self.documentTreeView.setIconSize(QSize(16, 16))
self.documentTreeView.setAlternatingRowColors(True) self.documentTreeView.setAlternatingRowColors(True)
self.documentTreeView.setIconSize(QSize(16, 16))
self.documentTreeView.setUniformRowHeights(True) self.documentTreeView.setUniformRowHeights(True)
self.documentPanelLayout.addWidget(self.documentTreeView) self.documentPanelLayout.addWidget(self.documentTreeView)
@@ -179,12 +188,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2) self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader) self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton") self.navigateUpButton.setObjectName(u"navigateUpButton")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon13)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton) self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader) self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton") self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False) self.navigateDownButton.setEnabled(False)
icon14 = QIcon()
icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon14)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton) self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -210,6 +225,9 @@ class Ui_MainWindow(object):
self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton") self.pointerToolButton.setObjectName(u"pointerToolButton")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon15)
self.pointerToolButton.setCheckable(True) self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True) self.pointerToolButton.setChecked(True)
@@ -221,6 +239,39 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.addWidget(self.connectToolButton) self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon16)
self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon17)
self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton")
icon18 = QIcon()
icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon18)
self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon1)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
self.routingLabel = QLabel(self.workspaceHeader) self.routingLabel = QLabel(self.workspaceHeader)
self.routingLabel.setObjectName(u"routingLabel") self.routingLabel.setObjectName(u"routingLabel")
@@ -235,13 +286,13 @@ class Ui_MainWindow(object):
self.angledRoutingButton = QToolButton(self.workspaceHeader) self.angledRoutingButton = QToolButton(self.workspaceHeader)
self.angledRoutingButton.setObjectName(u"angledRoutingButton") self.angledRoutingButton.setObjectName(u"angledRoutingButton")
self.angledRoutingButton.setCheckable(True) self.angledRoutingButton.setCheckable(True)
self.angledRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton) self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
self.splineRoutingButton = QToolButton(self.workspaceHeader) self.splineRoutingButton = QToolButton(self.workspaceHeader)
self.splineRoutingButton.setObjectName(u"splineRoutingButton") self.splineRoutingButton.setObjectName(u"splineRoutingButton")
self.splineRoutingButton.setCheckable(True) self.splineRoutingButton.setCheckable(True)
self.splineRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton) self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
@@ -296,7 +347,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow) self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar") self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1000, 24)) self.menubar.setGeometry(QRect(0, 0, 1209, 24))
self.menuFile = QMenu(self.menubar) self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile") self.menuFile.setObjectName(u"menuFile")
self.menuEdit = QMenu(self.menubar) self.menuEdit = QMenu(self.menubar)
@@ -324,9 +375,6 @@ class Ui_MainWindow(object):
self.editToolbar = QToolBar(MainWindow) self.editToolbar = QToolBar(MainWindow)
self.editToolbar.setObjectName(u"editToolbar") self.editToolbar.setObjectName(u"editToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar) MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar)
self.transformToolbar = QToolBar(MainWindow)
self.transformToolbar.setObjectName(u"transformToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar)
self.cameraToolbar = QToolBar(MainWindow) self.cameraToolbar = QToolBar(MainWindow)
self.cameraToolbar.setObjectName(u"cameraToolbar") self.cameraToolbar.setObjectName(u"cameraToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar) MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
@@ -366,7 +414,6 @@ class Ui_MainWindow(object):
self.editToolbar.addAction(self.actionCopy) self.editToolbar.addAction(self.actionCopy)
self.editToolbar.addAction(self.actionCut) self.editToolbar.addAction(self.actionCut)
self.editToolbar.addAction(self.actionPaste) self.editToolbar.addAction(self.actionPaste)
self.transformToolbar.addAction(self.actionRotateClockwise)
self.cameraToolbar.addAction(self.actionZoomIn) self.cameraToolbar.addAction(self.actionZoomIn)
self.cameraToolbar.addAction(self.actionZoomOut) self.cameraToolbar.addAction(self.actionZoomOut)
self.cameraToolbar.addAction(self.actionCenterView) self.cameraToolbar.addAction(self.actionCenterView)
@@ -469,19 +516,35 @@ class Ui_MainWindow(object):
self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None)) self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None))
self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None)) self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None))
self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None)) self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", 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) #endif // QT_CONFIG(tooltip)
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None)) self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None)) self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
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.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
#if QT_CONFIG(tooltip)
self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None))
#endif // QT_CONFIG(tooltip)
self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None))
#if QT_CONFIG(tooltip)
self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None))
#endif // QT_CONFIG(tooltip)
self.lineToolButton.setText(QCoreApplication.translate("MainWindow", u"Line", None))
#if QT_CONFIG(tooltip)
self.textToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add a text annotation", None))
#endif // QT_CONFIG(tooltip)
self.textToolButton.setText(QCoreApplication.translate("MainWindow", u"Text", None))
#if QT_CONFIG(tooltip)
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
#endif // QT_CONFIG(tooltip)
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None)) self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None)) self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None)) self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
@@ -496,7 +559,6 @@ class Ui_MainWindow(object):
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None)) self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None)) self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None))
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None)) self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
# retranslateUi # retranslateUi

View File

@@ -1,5 +1,6 @@
from PySide6.QtCore import QPointF, QRectF from PySide6.QtCore import QPointF, QRectF
from PySide6.QtGui import QColor, QPainter, QPen from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent
from PySide6.QtWidgets import QGraphicsView from PySide6.QtWidgets import QGraphicsView
from bedit.gui.preferences import application_settings from bedit.gui.preferences import application_settings
@@ -22,3 +23,25 @@ class IconCanvasView(QGraphicsView):
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom())) painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
for y in range(top, int(rect.bottom()) + grid, grid): for y in range(top, int(rect.bottom()) + grid, grid):
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)) painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
def _zoom(self, factor: float) -> None:
target = self.transform().m11() * factor
if 0.25 <= target <= 16.0:
self.scale(factor, factor)
def zoom_in(self) -> None:
self._zoom(1.2)
def zoom_out(self) -> None:
self._zoom(1 / 1.2)
def center_icon(self) -> None:
if self.scene() is not None:
self.fitInView(
self.scene().sceneRect().adjusted(-10, -10, 10, 10),
Qt.AspectRatioMode.KeepAspectRatio,
)
def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802
self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2)
event.accept()

View File

@@ -1,13 +1,15 @@
from copy import deepcopy from copy import deepcopy
from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QColorDialog, QColorDialog,
QButtonGroup,
QDialog, QDialog,
QGraphicsEllipseItem, QGraphicsEllipseItem,
QGraphicsItem, QGraphicsItem,
QGraphicsObject, QGraphicsObject,
QGraphicsPathItem,
QGraphicsScene, QGraphicsScene,
QGraphicsSceneContextMenuEvent, QGraphicsSceneContextMenuEvent,
QGraphicsSceneMouseEvent, QGraphicsSceneMouseEvent,
@@ -19,7 +21,7 @@ from bedit.core.model import Component, Icon, Port
from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog
from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog
from bedit.gui.graphics.icon_canvas import icon_grid_size from bedit.gui.graphics.icon_canvas import icon_grid_size
from bedit.gui.graphics.icon_renderer import _pen from bedit.gui.graphics.icon_renderer import shape_pen
def _icon_grid_size() -> int: def _icon_grid_size() -> int:
@@ -49,6 +51,21 @@ class ResizeHandle(QGraphicsEllipseItem):
if self.owner.scene() if self.owner.scene()
else QPointF(128, 128) else QPointF(128, 128)
) )
if self.owner.element.get("type") == "line":
minimum = (
self.owner.scene().sceneRect().topLeft() - self.owner.pos()
if self.owner.scene()
else QPointF(-128, -128)
)
value = QPointF(
min(maximum.x(), max(minimum.x(), _snap(value.x()))),
min(maximum.y(), max(minimum.y(), _snap(value.y()))),
)
self.owner.prepareGeometryChange()
self.owner.element["width"] = value.x()
self.owner.element["height"] = value.y()
self.owner.update()
return super().itemChange(change, value)
value = QPointF( value = QPointF(
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))), min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))), min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
@@ -82,9 +99,17 @@ class ShapeOptionsDialog(QDialog):
self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0))) self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0)))
self.ui.textEdit.setText(str(self.element.get("text", "Text"))) self.ui.textEdit.setText(str(self.element.get("text", "Text")))
self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12))) self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12)))
self._show_row(self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line") self._show_row(
self._show_row(self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line") self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line"
self._show_row(self.ui.cornerRadiusLabel, self.ui.cornerRadiusSpin, self.element.get("type") == "rectangle") )
self._show_row(
self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line"
)
self._show_row(
self.ui.cornerRadiusLabel,
self.ui.cornerRadiusSpin,
self.element.get("type") == "rectangle",
)
is_text = self.element.get("type") == "text" is_text = self.element.get("type") == "text"
self._show_row(self.ui.textLabel, self.ui.textEdit, is_text) self._show_row(self.ui.textLabel, self.ui.textEdit, is_text)
self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text) self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text)
@@ -99,7 +124,9 @@ class ShapeOptionsDialog(QDialog):
def _choose_color(self, current: str) -> str: def _choose_color(self, current: str) -> str:
color = QColorDialog.getColor( color = QColorDialog.getColor(
QColor(current), self, "Choose colour", QColor(current),
self,
"Choose colour",
QColorDialog.ColorDialogOption.ShowAlphaChannel, QColorDialog.ColorDialogOption.ShowAlphaChannel,
) )
if not color.isValid(): if not color.isValid():
@@ -129,7 +156,9 @@ class ShapeOptionsDialog(QDialog):
self.element["width"] = self.ui.widthSpin.value() self.element["width"] = self.ui.widthSpin.value()
self.element["height"] = self.ui.heightSpin.value() self.element["height"] = self.ui.heightSpin.value()
if self.element.get("type") != "line": if self.element.get("type") != "line":
self.element["fill"] = self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none" self.element["fill"] = (
self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none"
)
if self.element.get("type") == "rectangle": if self.element.get("type") == "rectangle":
self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value() self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value()
if self.element.get("type") == "text": if self.element.get("type") == "text":
@@ -144,21 +173,35 @@ class ShapeItem(QGraphicsObject):
super().__init__() super().__init__()
self.element = element self.element = element
self.setPos(float(element.get("x", 0)), float(element.get("y", 0))) self.setPos(float(element.get("x", 0)), float(element.get("y", 0)))
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
self.resize_handle = ResizeHandle(self) self.resize_handle = ResizeHandle(self)
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20))) self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
self.resize_handle.hide() self.resize_handle.hide()
def boundingRect(self) -> QRectF: # noqa: N802 def boundingRect(self) -> QRectF: # noqa: N802
margin = max(3.0, float(self.element.get("lineWidth", 1.5))) margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin) return (
QRectF(
0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
)
.normalized()
.adjusted(-margin, -margin, margin, margin)
)
def paint(self, painter: QPainter, option, widget=None) -> None: def paint(self, painter: QPainter, option, widget=None) -> None:
del option, widget del option, widget
rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))) rect = QRectF(
painter.setPen(_pen(self.element)) 0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))
)
painter.setPen(shape_pen(self.element))
fill = self.element.get("fill", "none") fill = self.element.get("fill", "none")
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)) painter.setBrush(
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
)
kind = self.element.get("type") kind = self.element.get("type")
if kind == "rectangle": if kind == "rectangle":
radius = float(self.element.get("cornerRadius", 0)) radius = float(self.element.get("cornerRadius", 0))
@@ -168,13 +211,17 @@ class ShapeItem(QGraphicsObject):
elif kind == "line": elif kind == "line":
painter.drawLine(rect.topLeft(), rect.bottomRight()) painter.drawLine(rect.topLeft(), rect.bottomRight())
elif kind == "triangle": elif kind == "triangle":
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])) painter.drawPolygon(
QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])
)
elif kind == "text": elif kind == "text":
painter.setPen(QColor(self.element.get("color", "#202020"))) painter.setPen(QColor(self.element.get("color", "#202020")))
font = painter.font() font = painter.font()
font.setPointSizeF(float(self.element.get("fontSize", 12))) font.setPointSizeF(float(self.element.get("fontSize", 12)))
painter.setFont(font) painter.setFont(font)
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))) painter.drawText(
rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))
)
if self.isSelected(): if self.isSelected():
painter.setBrush(Qt.BrushStyle.NoBrush) painter.setBrush(Qt.BrushStyle.NoBrush)
painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine)) painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine))
@@ -182,23 +229,7 @@ class ShapeItem(QGraphicsObject):
def itemChange(self, change, value): # noqa: N802 def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128) value = self._bounded_position(value)
value = QPointF(
max(
bounds.left(),
min(
bounds.right() - float(self.element.get("width", 20)),
_snap(value.x()),
),
),
max(
bounds.top(),
min(
bounds.bottom() - float(self.element.get("height", 20)),
_snap(value.y()),
),
),
)
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
self.element["x"], self.element["y"] = value.x(), value.y() self.element["x"], self.element["y"] = value.x(), value.y()
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
@@ -207,10 +238,17 @@ class ShapeItem(QGraphicsObject):
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
self.setPos(self._bounded_position(self.pos()))
def _bounded_position(self, position: QPointF) -> QPointF:
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128) bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
self.setPos( width = float(self.element.get("width", 20))
max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))), height = float(self.element.get("height", 20))
max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))), minimum_x, maximum_x = min(0.0, width), max(0.0, width)
minimum_y, maximum_y = min(0.0, height), max(0.0, height)
return QPointF(
max(bounds.left() - minimum_x, min(bounds.right() - maximum_x, _snap(position.x()))),
max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))),
) )
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
@@ -219,11 +257,19 @@ class ShapeItem(QGraphicsObject):
delete = menu.addAction("Delete Shape") delete = menu.addAction("Delete Shape")
chosen = menu.exec(event.screenPos()) chosen = menu.exec(event.screenPos())
if chosen is options: if chosen is options:
dialog = ShapeOptionsDialog(self.element) options_element = deepcopy(self.element)
width_sign = -1 if float(options_element.get("width", 20)) < 0 else 1
height_sign = -1 if float(options_element.get("height", 20)) < 0 else 1
options_element["width"] = abs(float(options_element.get("width", 20)))
options_element["height"] = abs(float(options_element.get("height", 20)))
dialog = ShapeOptionsDialog(options_element)
if dialog.exec() == dialog.DialogCode.Accepted: if dialog.exec() == dialog.DialogCode.Accepted:
self.prepareGeometryChange() self.prepareGeometryChange()
self.element.clear() self.element.clear()
self.element.update(dialog.element) self.element.update(dialog.element)
if self.element.get("type") == "line":
self.element["width"] *= width_sign
self.element["height"] *= height_sign
self.resize_handle.setPos( self.resize_handle.setPos(
float(self.element.get("width", 20)), float(self.element.get("width", 20)),
float(self.element.get("height", 20)), float(self.element.get("height", 20)),
@@ -244,7 +290,11 @@ class PortHandle(QGraphicsEllipseItem):
self.setPen(QPen(QColor("#ffffff"), 1.5)) self.setPen(QPen(QColor("#ffffff"), 1.5))
self.setToolTip(f"{direction.title()}: {port.name} (drag to position)") self.setToolTip(f"{direction.title()}: {port.name} (drag to position)")
self.setZValue(10) self.setZValue(10)
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
def itemChange(self, change, value): # noqa: N802 def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
@@ -266,6 +316,81 @@ class PortHandle(QGraphicsEllipseItem):
) )
class IconDrawingScene(QGraphicsScene):
def __init__(self, editor: "IconEditorDialog", rect: QRectF) -> None:
super().__init__(rect, editor)
self.editor = editor
self.mode = "pointer"
self.start: QPointF | None = None
self.preview: QGraphicsPathItem | None = None
def set_mode(self, mode: str) -> None:
self.mode = mode
self._clear_preview()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.mode == "pointer":
super().mousePressEvent(event)
return
point = self._bounded(event.scenePos())
if self.mode == "text":
text, accepted = QInputDialog.getText(self.editor, "Add Text", "Text:", text="Text")
if accepted:
self.editor.create_shape("text", point, point + QPointF(55, 35), text)
event.accept()
return
self.start = point
self.preview = QGraphicsPathItem()
self.preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine))
self.preview.setZValue(100)
self.addItem(self.preview)
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.start is None or self.preview is None:
super().mouseMoveEvent(event)
return
end = self._bounded(event.scenePos())
path = QPainterPath()
rect = QRectF(self.start, end).normalized()
if self.mode == "line":
path.moveTo(self.start)
path.lineTo(end)
elif self.mode in {"circle", "ellipse"}:
if self.mode == "circle":
side = min(rect.width(), rect.height())
rect.setSize(QSizeF(side, side))
path.addEllipse(rect)
else:
path.addRect(rect)
self.preview.setPath(path)
event.accept()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.start is None:
super().mouseReleaseEvent(event)
return
end = self._bounded(event.scenePos())
start = self.start
self._clear_preview()
if start != end:
self.editor.create_shape(self.mode, start, end)
event.accept()
def _bounded(self, point: QPointF) -> QPointF:
rect = self.sceneRect()
return QPointF(
min(rect.right(), max(rect.left(), _snap(point.x()))),
min(rect.bottom(), max(rect.top(), _snap(point.y()))),
)
def _clear_preview(self) -> None:
if self.preview is not None:
self.removeItem(self.preview)
self.preview = None
self.start = None
class IconEditorDialog(QDialog): class IconEditorDialog(QDialog):
def __init__(self, component: Component, parent=None) -> None: def __init__(self, component: Component, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
@@ -275,6 +400,10 @@ class IconEditorDialog(QDialog):
self.icon = Icon.from_dict(component.icon.to_dict()) self.icon = Icon.from_dict(component.icon.to_dict())
self.inputs = deepcopy(component.inputs) self.inputs = deepcopy(component.inputs)
self.outputs = deepcopy(component.outputs) self.outputs = deepcopy(component.outputs)
self.tool_group = QButtonGroup(self)
self.tool_group.setExclusive(True)
self.tool_group.addButton(self.ui.pointerButton)
self.ui.pointerButton.clicked.connect(lambda: self.set_draw_mode("pointer"))
for button, kind in ( for button, kind in (
(self.ui.addRectangleButton, "rectangle"), (self.ui.addRectangleButton, "rectangle"),
(self.ui.addCircleButton, "circle"), (self.ui.addCircleButton, "circle"),
@@ -283,43 +412,70 @@ class IconEditorDialog(QDialog):
(self.ui.addTriangleButton, "triangle"), (self.ui.addTriangleButton, "triangle"),
(self.ui.addTextButton, "text"), (self.ui.addTextButton, "text"),
): ):
button.clicked.connect( self.tool_group.addButton(button)
lambda _checked=False, value=kind: self.add_shape(value) button.clicked.connect(lambda _checked=False, value=kind: self.set_draw_mode(value))
)
self.ui.deleteSelectedButton.clicked.connect(self.delete_selected) self.ui.deleteSelectedButton.clicked.connect(self.delete_selected)
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self) self.scene = IconDrawingScene(self, QRectF(0, 0, self.icon.width, self.icon.height))
self.ui.iconView.setScene(self.scene) self.ui.iconView.setScene(self.scene)
self.view = self.ui.iconView self.view = self.ui.iconView
self.view.setRenderHint(QPainter.RenderHint.Antialiasing) self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
self.ui.zoomInButton.clicked.connect(self.view.zoom_in)
self.ui.zoomOutButton.clicked.connect(self.view.zoom_out)
self.ui.centerButton.clicked.connect(self.view.center_icon)
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100) self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
for element in self.icon.elements: for element in self.icon.elements:
self.scene.addItem(ShapeItem(element)) self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0) self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width) self._add_ports(self.outputs, "output", self.icon.width)
self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio) self.view.center_icon()
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None: def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
spacing = self.icon.height / (len(ports) + 1) spacing = self.icon.height / (len(ports) + 1)
for index, port in enumerate(ports, 1): for index, port in enumerate(ports, 1):
saved = port.properties.get("iconPosition", {}) saved = port.properties.get("iconPosition", {})
position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index))) position = QPointF(
float(saved.get("x", default_x)), float(saved.get("y", spacing * index))
)
self.scene.addItem(PortHandle(port, direction, position)) self.scene.addItem(PortHandle(port, direction, position))
def add_shape(self, kind: str) -> None: def set_draw_mode(self, mode: str) -> None:
count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)]) self.scene.set_mode(mode)
x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25 self.view.setDragMode(
element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"} self.view.DragMode.RubberBandDrag if mode == "pointer" else self.view.DragMode.NoDrag
)
def create_shape(self, kind: str, start: QPointF, end: QPointF, text: str = "Text") -> None:
left, right = sorted((start.x(), end.x()))
top, bottom = sorted((start.y(), end.y()))
element = {
"type": kind,
"x": left,
"y": top,
"width": max(_icon_grid_size(), right - left),
"height": max(_icon_grid_size(), bottom - top),
"fill": "#dbeafe",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
}
if kind == "circle": if kind == "circle":
element["width"] = element["height"] = 35 element["width"] = element["height"] = min(element["width"], element["height"])
if kind == "line": if kind == "line":
element["x"], element["y"] = start.x(), start.y()
element["width"], element["height"] = end.x() - start.x(), end.y() - start.y()
element["fill"] = "none" element["fill"] = "none"
if kind == "rectangle": if kind == "rectangle":
element["cornerRadius"] = 0 element["cornerRadius"] = 0
if kind == "text": if kind == "text":
text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text") element.update(
if not accepted: {
return "text": text,
element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"}) "fontSize": 12,
"color": "#202020",
"fill": "none",
"lineStyle": "none",
}
)
self.icon.elements.append(element) self.icon.elements.append(element)
item = ShapeItem(element) item = ShapeItem(element)
self.scene.addItem(item) self.scene.addItem(item)
@@ -332,5 +488,7 @@ class IconEditorDialog(QDialog):
item.element["_deleted"] = True item.element["_deleted"] = True
def accept(self) -> None: def accept(self) -> None:
self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)] self.icon.elements = [
element for element in self.icon.elements if not element.pop("_deleted", False)
]
super().accept() super().accept()

View File

@@ -13,14 +13,14 @@ def icon_bounds(icon: Icon) -> QRectF:
rect = QRectF( rect = QRectF(
float(element.get("x", 0)), float(element.get("x", 0)),
float(element.get("y", 0)), float(element.get("y", 0)),
max(0.0, float(element.get("width", 0))), float(element.get("width", 0)),
max(0.0, float(element.get("height", 0))), float(element.get("height", 0)),
) ).normalized()
bounds = rect if bounds.isNull() else bounds.united(rect) bounds = rect if bounds.isNull() else bounds.united(rect)
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64) return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
def _pen(element: dict) -> QPen: def shape_pen(element: dict) -> QPen:
styles = { styles = {
"solid": Qt.PenStyle.SolidLine, "solid": Qt.PenStyle.SolidLine,
"dash": Qt.PenStyle.DashLine, "dash": Qt.PenStyle.DashLine,
@@ -51,12 +51,16 @@ def paint_icon(
for element in icon.elements: for element in icon.elements:
kind = element.get("type", "rectangle") kind = element.get("type", "rectangle")
rect = QRectF( rect = QRectF(
float(element.get("x", 0)), float(element.get("y", 0)), float(element.get("x", 0)),
float(element.get("width", 20)), float(element.get("height", 20)), float(element.get("y", 0)),
float(element.get("width", 20)),
float(element.get("height", 20)),
) )
painter.setPen(_pen(element)) painter.setPen(shape_pen(element))
fill = element.get("fill", "#ffffff") fill = element.get("fill", "#ffffff")
painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)) painter.setBrush(
Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)
)
if kind == "rectangle": if kind == "rectangle":
radius = float(element.get("cornerRadius", 0)) radius = float(element.get("cornerRadius", 0))
painter.drawRoundedRect(rect, radius, radius) painter.drawRoundedRect(rect, radius, radius)
@@ -65,7 +69,11 @@ def paint_icon(
elif kind == "line": elif kind == "line":
painter.drawLine(rect.topLeft(), rect.bottomRight()) painter.drawLine(rect.topLeft(), rect.bottomRight())
elif kind == "triangle": elif kind == "triangle":
painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()])) painter.drawPolygon(
QPolygonF(
[QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()]
)
)
elif kind == "text": elif kind == "text":
painter.setPen(QColor(element.get("color", element.get("stroke", "#202020")))) painter.setPen(QColor(element.get("color", element.get("stroke", "#202020"))))
font = QFont() font = QFont()

View File

@@ -24,15 +24,18 @@ from PySide6.QtWidgets import (
QGraphicsView, QGraphicsView,
QApplication, QApplication,
QMenu, QMenu,
QInputDialog,
QStyleOptionGraphicsItem, QStyleOptionGraphicsItem,
QToolTip, QToolTip,
QWidget, 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.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.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.graphics.connection_styles import ConnectionStyle, connection_style
from bedit.gui.preferences import application_settings from bedit.gui.preferences import application_settings
@@ -94,7 +97,9 @@ class ComponentGraphicsItem(QGraphicsObject):
position = port.properties.get("iconPosition", {}) position = port.properties.get("iconPosition", {})
item.setPos( item.setPos(
float(position.get("x", x)) * self.WIDTH / self.component.icon.width, 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 result[port.id] = item
return result return result
@@ -212,6 +217,9 @@ class InterfaceTerminalItem(QGraphicsObject):
super().mousePressEvent(event) super().mousePressEvent(event)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if self.index < 0:
event.accept()
return
menu = QMenu() menu = QMenu()
options_action = menu.addAction(f"{self.direction.title()} Options…") options_action = menu.addAction(f"{self.direction.title()} Options…")
if menu.exec(event.screenPos()) is options_action: if menu.exec(event.screenPos()) is options_action:
@@ -253,13 +261,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.end_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(0)
self.setToolTip(name or "Connection") self.setToolTip(name or "Connection")
self.handles: list[WaypointHandle] = []
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
menu = QMenu() menu = QMenu()
add_node_action = menu.addAction("Add Node")
menu.addSeparator()
options_action = menu.addAction("Connection Options…") 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() scene = self.scene()
if isinstance(scene, GraphScene): if isinstance(scene, GraphScene):
scene.connectionOptionsRequested.emit(self.connection_id) scene.connectionOptionsRequested.emit(self.connection_id)
@@ -269,6 +288,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
result = super().itemChange(change, value) result = super().itemChange(change, value)
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
self._update_pen() self._update_pen()
self._update_handles()
return result return result
def _update_pen(self) -> None: def _update_pen(self) -> None:
@@ -291,6 +311,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.start_direction = points[1] - points[0] self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2] 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 @staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF: def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5) 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: if self.style.arrow_at_target:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size)) painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
if self.style.arrow_at_source: 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): class GraphScene(QGraphicsScene):
@@ -322,17 +564,21 @@ class GraphScene(QGraphicsScene):
self.input_items: dict[str, InterfaceTerminalItem] = {} self.input_items: dict[str, InterfaceTerminalItem] = {}
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.annotation_items: dict[str, QGraphicsItem] = {}
self.pending_source: ConnectionPortItem | None = None self.pending_source: ConnectionPortItem | None = None
self.pending_waypoints: list[QPointF] = [] self.pending_waypoints: list[QPointF] = []
self.pending_preview: QGraphicsPathItem | None = None self.pending_preview: QGraphicsPathItem | None = None
self.interaction_mode = "pointer" 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) self.setSceneRect(-2000, -2000, 4000, 4000)
controller.documentReset.connect(self.rebuild) controller.documentReset.connect(self.rebuild)
controller.activeGraphChanged.connect(self.rebuild) controller.activeGraphChanged.connect(self.rebuild)
controller.componentMoved.connect(self.set_component_position) controller.componentMoved.connect(self.set_component_position)
controller.componentRotated.connect(self.set_component_rotation) controller.componentRotated.connect(self.set_component_rotation)
controller.graphItemChanged.connect(self.refresh_graph_item)
self.rebuild() self.rebuild()
def rebuild(self) -> None: def rebuild(self) -> None:
@@ -341,6 +587,7 @@ class GraphScene(QGraphicsScene):
self.input_items.clear() self.input_items.clear()
self.output_items.clear() self.output_items.clear()
self.connection_items.clear() self.connection_items.clear()
self.annotation_items.clear()
self.pending_source = None self.pending_source = None
self.pending_waypoints.clear() self.pending_waypoints.clear()
self.pending_preview = None self.pending_preview = None
@@ -371,6 +618,16 @@ class GraphScene(QGraphicsScene):
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)
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: def set_component_position(self, component_id: str, position: QPointF) -> None:
item = self.component_items.get(component_id) item = self.component_items.get(component_id)
@@ -383,6 +640,52 @@ class GraphScene(QGraphicsScene):
item.setRotation(rotation) item.setRotation(rotation)
self.update_connections_for_block(component_id) 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: def update_connections_for_block(self, component_id: str) -> None:
for connection in self.controller.active_graph.connections.values(): for connection in self.controller.active_graph.connections.values():
if component_id in (connection.source.block, connection.target.block): 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: if source is None or target is None:
return return
start, end = source.scenePos(), target.scenePos() start, end = source.scenePos(), target.scenePos()
routing = str(connection.properties.get("routing", "spline")) routing = str(connection.properties.get("routing", "angled"))
waypoints = [ waypoints = [
QPointF(float(point["x"]), float(point["y"])) QPointF(float(point["x"]), float(point["y"]))
for point in connection.properties.get("waypoints", []) for point in connection.properties.get("waypoints", [])
@@ -426,6 +729,35 @@ class GraphScene(QGraphicsScene):
] ]
path, direction_points = self._route_path(start, end, routing, waypoints) path, direction_points = self._route_path(start, end, routing, waypoints)
graphics.set_connection_path(path, direction_points) 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 @staticmethod
def _route_path( def _route_path(
@@ -437,16 +769,23 @@ class GraphScene(QGraphicsScene):
waypoints = waypoints or [] waypoints = waypoints or []
path = QPainterPath(start) path = QPainterPath(start)
if routing == "angled": if routing == "angled":
points = [start, *waypoints, end] points = GraphScene._orthogonal_points(start, end, waypoints)
for point in points[1:]: for point in points[1:]:
path.lineTo(point) path.lineTo(point)
return path, points return path, points
if routing == "direct": if routing == "direct":
path.lineTo(end) path.lineTo(end)
return path, [start, 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) distance = max(50.0, abs(end.x() - start.x()) * 0.5)
first_control = start + QPointF(distance, 0) first_control, second_control = start + QPointF(distance, 0), end - QPointF(distance, 0)
second_control = end - QPointF(distance, 0)
path.cubicTo(first_control, second_control, end) path.cubicTo(first_control, second_control, end)
return path, [start, first_control, second_control, end] return path, [start, first_control, second_control, end]
@@ -454,12 +793,16 @@ class GraphScene(QGraphicsScene):
self.interaction_mode = mode self.interaction_mode = mode
if mode != "connect": if mode != "connect":
self._clear_pending_source() self._clear_pending_source()
self._clear_drawing()
def set_connection_routing(self, routing: str) -> None: def set_connection_routing(self, routing: str) -> None:
self.connection_routing = routing self.connection_routing = routing
self._clear_pending_source() self._clear_pending_source()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 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": if self.interaction_mode != "connect":
self._clear_pending_source() self._clear_pending_source()
super().mousePressEvent(event) super().mousePressEvent(event)
@@ -483,7 +826,15 @@ class GraphScene(QGraphicsScene):
self.pending_source.endpoint, self.pending_source.endpoint,
item.endpoint, item.endpoint,
routing=self.connection_routing, 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: except ValueError as error:
QToolTip.showText(event.screenPos(), str(error)) QToolTip.showText(event.screenPos(), str(error))
@@ -500,8 +851,222 @@ class GraphScene(QGraphicsScene):
self._update_preview(event.scenePos()) self._update_preview(event.scenePos())
event.accept() event.accept()
return 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) 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: def _create_preview(self) -> None:
self.pending_preview = QGraphicsPathItem() self.pending_preview = QGraphicsPathItem()
self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine)) 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: def _update_preview(self, cursor: QPointF) -> None:
if self.pending_source is None or self.pending_preview is None: if self.pending_source is None or self.pending_preview is None:
return return
preview_cursor = _snapped(cursor) if self.connection_routing == "angled" else cursor
path, _points = self._route_path( path, _points = self._route_path(
self.pending_source.scenePos(), self.pending_source.scenePos(),
cursor, preview_cursor,
self.connection_routing, self.connection_routing,
self.pending_waypoints, self.pending_waypoints,
) )
@@ -622,6 +1188,7 @@ class GraphWorkspaceView(QGraphicsView):
connections: set[str] = set() connections: set[str] = set()
inputs: set[str] = set() inputs: set[str] = set()
outputs: set[str] = set() outputs: set[str] = set()
annotations: set[str] = set()
for item in self.scene().selectedItems(): for item in self.scene().selectedItems():
if isinstance(item, ComponentGraphicsItem): if isinstance(item, ComponentGraphicsItem):
blocks.add(item.component_id) blocks.add(item.component_id)
@@ -629,32 +1196,29 @@ class GraphWorkspaceView(QGraphicsView):
connections.add(item.connection_id) connections.add(item.connection_id)
elif isinstance(item, InterfaceTerminalItem): elif isinstance(item, InterfaceTerminalItem):
(inputs if item.direction == "input" else outputs).add(item.port.id) (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_selection(blocks, connections, inputs, outputs)
self.controller.delete_annotations(annotations)
def has_selected_components(self) -> bool: def has_selected_components(self) -> bool:
scene = self.scene() scene = self.scene()
return bool( return bool(
scene scene 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: def has_single_selected_component(self) -> bool:
scene = self.scene() scene = self.scene()
return bool( return bool(
scene scene
and sum( and sum(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) == 1
isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()
)
== 1
) )
def open_selected_component(self) -> bool: def open_selected_component(self) -> bool:
if self.controller is None or self.scene() is None: if self.controller is None or self.scene() is None:
return False return False
selected = [ selected = [
item item for item in self.scene().selectedItems() if isinstance(item, ComponentGraphicsItem)
for item in self.scene().selectedItems()
if isinstance(item, ComponentGraphicsItem)
] ]
if len(selected) != 1: if len(selected) != 1:
return False return False

View File

@@ -4,7 +4,14 @@ 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 QButtonGroup, QFileDialog, QMainWindow, QMenu, QMessageBox from PySide6.QtWidgets import (
QButtonGroup,
QFileDialog,
QMainWindow,
QMenu,
QMessageBox,
QTabWidget,
)
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
@@ -51,11 +58,11 @@ class MainWindow(QMainWindow):
self.ui.leftDockHost.show() self.ui.leftDockHost.show()
self.ui.panel_libraries.show() self.ui.panel_libraries.show()
self.ui.panel_document.show() self.ui.panel_document.show()
self.ui.leftDockHost.splitDockWidget( self.ui.leftDockHost.setTabPosition(
self.ui.panel_document, Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North
self.ui.panel_libraries,
Qt.Orientation.Vertical,
) )
self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries)
self.ui.panel_document.raise_()
self.ui.workspaceSplitter.setSizes([280, 720]) self.ui.workspaceSplitter.setSizes([280, 720])
self.reload_libraries() self.reload_libraries()
self._active_graph_changed() self._active_graph_changed()
@@ -67,22 +74,14 @@ class MainWindow(QMainWindow):
self.ui.treeView.setDragEnabled(True) self.ui.treeView.setDragEnabled(True)
self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly) self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly)
self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.treeView.customContextMenuRequested.connect( self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu)
self.show_external_library_context_menu
)
self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll) self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll)
self.ui.documentTreeView.setModel(self.document_tree_model) self.ui.documentTreeView.setModel(self.document_tree_model)
self.ui.documentTreeView.setHeaderHidden(True) self.ui.documentTreeView.setHeaderHidden(True)
self.ui.documentTreeView.setDragEnabled(True) self.ui.documentTreeView.setDragEnabled(True)
self.ui.documentTreeView.setDragDropMode( self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly)
self.ui.documentTreeView.DragDropMode.DragOnly self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
) self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
)
self.ui.documentTreeView.customContextMenuRequested.connect(
self.show_library_context_menu
)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component) self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll) self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
self.ui.graphView.set_model(self.document_controller) self.ui.graphView.set_model(self.document_controller)
@@ -97,6 +96,9 @@ class MainWindow(QMainWindow):
self.mode_button_group.setExclusive(True) self.mode_button_group.setExclusive(True)
self.mode_button_group.addButton(self.ui.pointerToolButton) self.mode_button_group.addButton(self.ui.pointerToolButton)
self.mode_button_group.addButton(self.ui.connectToolButton) self.mode_button_group.addButton(self.ui.connectToolButton)
self.mode_button_group.addButton(self.ui.boxToolButton)
self.mode_button_group.addButton(self.ui.lineToolButton)
self.mode_button_group.addButton(self.ui.textToolButton)
self.routing_button_group = QButtonGroup(self) self.routing_button_group = QButtonGroup(self)
self.routing_button_group.setExclusive(True) self.routing_button_group.setExclusive(True)
for button in ( for button in (
@@ -109,15 +111,13 @@ class MainWindow(QMainWindow):
self.ui.navigateDownButton.clicked.connect(self.navigate_down) 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.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect"))
self.ui.directRoutingButton.clicked.connect( self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box"))
lambda: self.set_connection_routing("direct") self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
) self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
self.ui.angledRoutingButton.clicked.connect( self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
lambda: self.set_connection_routing("angled") 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( self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline"))
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)
@@ -160,7 +160,6 @@ class MainWindow(QMainWindow):
for toolbar in ( for toolbar in (
self.ui.fileToolbar, self.ui.fileToolbar,
self.ui.editToolbar, self.ui.editToolbar,
self.ui.transformToolbar,
self.ui.cameraToolbar, self.ui.cameraToolbar,
): ):
self.ui.menuToolbars.addAction(toolbar.toggleViewAction()) self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
@@ -184,7 +183,11 @@ class MainWindow(QMainWindow):
if self.document_controller.document is None: if self.document_controller.document is None:
self.setWindowTitle("BEdit") self.setWindowTitle("BEdit")
return return
name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled" name = (
self.document_controller.file_path.name
if self.document_controller.file_path
else "Untitled"
)
modified = "*" if not self.document_controller.undo_stack.isClean() else "" modified = "*" if not self.document_controller.undo_stack.isClean() else ""
self.setWindowTitle(f"{modified}{name} — BEdit") self.setWindowTitle(f"{modified}{name} — BEdit")
@@ -217,12 +220,17 @@ class MainWindow(QMainWindow):
def _update_edit_actions(self) -> None: def _update_edit_actions(self) -> None:
component = self.document_controller.active_component component = self.document_controller.active_component
is_graph = component is not None and component.implementation_kind == "graph" is_graph = component is not None and component.implementation_kind == "graph"
has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()) has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
)
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete): for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection) action.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled( self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components() is_graph and self.ui.graphView.has_selected_components()
) )
self.ui.rotateToolButton.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
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( self.ui.navigateDownButton.setEnabled(
@@ -243,6 +251,10 @@ class MainWindow(QMainWindow):
for widget in ( for widget in (
self.ui.pointerToolButton, self.ui.pointerToolButton,
self.ui.connectToolButton, self.ui.connectToolButton,
self.ui.boxToolButton,
self.ui.lineToolButton,
self.ui.textToolButton,
self.ui.rotateToolButton,
self.ui.routingLabel, self.ui.routingLabel,
self.ui.directRoutingButton, self.ui.directRoutingButton,
self.ui.angledRoutingButton, self.ui.angledRoutingButton,
@@ -252,7 +264,13 @@ class MainWindow(QMainWindow):
def set_graph_tool(self, mode: str) -> None: def set_graph_tool(self, mode: str) -> None:
self.ui.graphView.set_tool_mode(mode) self.ui.graphView.set_tool_mode(mode)
(self.ui.pointerToolButton if mode == "pointer" else self.ui.connectToolButton).setChecked(True) {
"pointer": self.ui.pointerToolButton,
"connect": self.ui.connectToolButton,
"box": self.ui.boxToolButton,
"line": self.ui.lineToolButton,
"text": self.ui.textToolButton,
}[mode].setChecked(True)
def set_connection_routing(self, routing: str) -> None: def set_connection_routing(self, routing: str) -> None:
self.ui.graphView.set_connection_routing(routing) self.ui.graphView.set_connection_routing(routing)
@@ -312,9 +330,7 @@ class MainWindow(QMainWindow):
raise ValueError("'source' must be an object") raise ValueError("'source' must be an object")
inputs = [Port.from_dict(item) for item in data["inputs"]] inputs = [Port.from_dict(item) for item in data["inputs"]]
outputs = [Port.from_dict(item) for item in data["outputs"]] outputs = [Port.from_dict(item) for item in data["outputs"]]
self.document_controller.replace_active_text_definition( self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
inputs, outputs, data["source"]
)
except (TypeError, ValueError, json.JSONDecodeError) as error: except (TypeError, ValueError, json.JSONDecodeError) as error:
QMessageBox.critical(self, "Invalid text component JSON", str(error)) QMessageBox.critical(self, "Invalid text component JSON", str(error))
return False return False
@@ -497,9 +513,7 @@ class MainWindow(QMainWindow):
try: try:
if library is not None: if library is not None:
library.document.validate() library.document.validate()
JsonDocumentSerializer.save( JsonDocumentSerializer.save(library.document, Path(library.source_path))
library.document, Path(library.source_path)
)
except (OSError, ValueError) as error: except (OSError, ValueError) as error:
component.inputs, component.outputs = old_inputs, old_outputs component.inputs, component.outputs = old_inputs, old_outputs
QMessageBox.warning(self, "Cannot change library ports", str(error)) QMessageBox.warning(self, "Cannot change library ports", str(error))
@@ -559,9 +573,7 @@ class MainWindow(QMainWindow):
connection = owner.graph.connections.get(connection_id) connection = owner.graph.connections.get(connection_id)
if connection is None: if connection is None:
return return
dialog = ItemOptionsDialog( dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
"Connection Options", connection.name, self, name_required=False
)
if dialog.exec() == dialog.DialogCode.Accepted: if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.rename_connection(connection_id, dialog.name) self.document_controller.rename_connection(connection_id, dialog.name)

View File

@@ -7,14 +7,18 @@
<layout class="QVBoxLayout" name="dialogLayout"> <layout class="QVBoxLayout" name="dialogLayout">
<item> <item>
<layout class="QHBoxLayout" name="shapeToolbarLayout"> <layout class="QHBoxLayout" name="shapeToolbarLayout">
<item><widget class="QToolButton" name="pointerButton"><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="QLabel" name="addShapeLabel"><property name="text"><string>Add:</string></property></widget></item> <item><widget class="QLabel" name="addShapeLabel"><property name="text"><string>Add:</string></property></widget></item>
<item><widget class="QToolButton" name="addRectangleButton"><property name="text"><string>Rectangle</string></property></widget></item> <item><widget class="QToolButton" name="addRectangleButton"><property name="text"><string>Rectangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addCircleButton"><property name="text"><string>Circle</string></property></widget></item> <item><widget class="QToolButton" name="addCircleButton"><property name="text"><string>Circle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addEllipseButton"><property name="text"><string>Ellipse</string></property></widget></item> <item><widget class="QToolButton" name="addEllipseButton"><property name="text"><string>Ellipse</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addLineButton"><property name="text"><string>Line</string></property></widget></item> <item><widget class="QToolButton" name="addLineButton"><property name="text"><string>Line</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addTriangleButton"><property name="text"><string>Triangle</string></property></widget></item> <item><widget class="QToolButton" name="addTriangleButton"><property name="text"><string>Triangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addTextButton"><property name="text"><string>Text</string></property></widget></item> <item><widget class="QToolButton" name="addTextButton"><property name="text"><string>Text</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><spacer name="toolbarSpacer"><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="toolbarSpacer"><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="QToolButton" name="zoomInButton"><property name="text"><string>+</string></property><property name="toolTip"><string>Zoom in</string></property></widget></item>
<item><widget class="QToolButton" name="zoomOutButton"><property name="text"><string></string></property><property name="toolTip"><string>Zoom out</string></property></widget></item>
<item><widget class="QToolButton" name="centerButton"><property name="text"><string>Fit</string></property><property name="toolTip"><string>Fit and center the icon canvas</string></property></widget></item>
<item><widget class="QPushButton" name="deleteSelectedButton"><property name="text"><string>Delete selected</string></property></widget></item> <item><widget class="QPushButton" name="deleteSelectedButton"><property name="text"><string>Delete selected</string></property></widget></item>
</layout> </layout>
</item> </item>

View File

@@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1000</width> <width>1209</width>
<height>700</height> <height>777</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -83,11 +83,18 @@
</property> </property>
<item> <item>
<widget class="QTreeView" name="treeView"> <widget class="QTreeView" name="treeView">
<property name="iconSize"><size><width>28</width><height>28</height></size></property> <property name="styleSheet">
<property name="styleSheet"><string notr="true">QTreeView::item { height: 32px; }</string></property> <string notr="true">QTreeView::item { height: 32px; }</string>
</property>
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="iconSize">
<size>
<width>28</width>
<height>28</height>
</size>
</property>
<property name="uniformRowHeights"> <property name="uniformRowHeights">
<bool>true</bool> <bool>true</bool>
</property> </property>
@@ -98,22 +105,48 @@
</widget> </widget>
<widget class="QDockWidget" name="panel_document"> <widget class="QDockWidget" name="panel_document">
<property name="minimumSize"> <property name="minimumSize">
<size><width>220</width><height>91</height></size> <size>
<width>220</width>
<height>91</height>
</size>
</property> </property>
<property name="windowTitle"><string>Document</string></property> <property name="windowTitle">
<attribute name="dockWidgetArea"><number>1</number></attribute> <string>Document</string>
</property>
<attribute name="dockWidgetArea">
<number>1</number>
</attribute>
<widget class="QWidget" name="documentDockContents"> <widget class="QWidget" name="documentDockContents">
<layout class="QVBoxLayout" name="documentPanelLayout"> <layout class="QVBoxLayout" name="documentPanelLayout">
<property name="spacing"><number>0</number></property> <property name="spacing">
<property name="leftMargin"><number>0</number></property> <number>0</number>
<property name="topMargin"><number>0</number></property> </property>
<property name="rightMargin"><number>0</number></property> <property name="leftMargin">
<property name="bottomMargin"><number>0</number></property> <number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QTreeView" name="documentTreeView"> <widget class="QTreeView" name="documentTreeView">
<property name="iconSize"><size><width>16</width><height>16</height></size></property> <property name="alternatingRowColors">
<property name="alternatingRowColors"><bool>true</bool></property> <bool>true</bool>
<property name="uniformRowHeights"><bool>true</bool></property> </property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>
@@ -128,59 +161,320 @@
</sizepolicy> </sizepolicy>
</property> </property>
<layout class="QVBoxLayout" name="workspaceEditorLayout"> <layout class="QVBoxLayout" name="workspaceEditorLayout">
<property name="spacing"><number>0</number></property> <property name="spacing">
<property name="leftMargin"><number>0</number></property> <number>0</number>
<property name="topMargin"><number>0</number></property> </property>
<property name="rightMargin"><number>0</number></property> <property name="leftMargin">
<property name="bottomMargin"><number>0</number></property> <number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item> <item>
<widget class="QFrame" name="workspaceHeader"> <widget class="QFrame" name="workspaceHeader">
<property name="minimumSize"><size><width>0</width><height>34</height></size></property> <property name="minimumSize">
<property name="maximumSize"><size><width>16777215</width><height>34</height></size></property> <size>
<property name="frameShape"><enum>QFrame::Shape::StyledPanel</enum></property> <width>0</width>
<height>34</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>34</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<layout class="QHBoxLayout" name="workspaceHeaderLayout"> <layout class="QHBoxLayout" name="workspaceHeaderLayout">
<property name="leftMargin"><number>6</number></property> <property name="leftMargin">
<property name="topMargin"><number>2</number></property> <number>6</number>
<property name="rightMargin"><number>6</number></property> </property>
<property name="bottomMargin"><number>2</number></property> <property name="topMargin">
<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> <number>2</number>
<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> </property>
<item><widget class="QLabel" name="graphBreadcrumbLabel"><property name="text"><string>Untitled</string></property></widget></item> <property name="rightMargin">
<item><widget class="QLabel" name="workspaceModeLabel"><property name="text"><string>Graph</string></property></widget></item> <number>6</number>
<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> </property>
<item><widget class="QPushButton" name="applyJsonButton"><property name="text"><string>Apply JSON</string></property><property name="visible"><bool>false</bool></property></widget></item> <property name="bottomMargin">
<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> <number>2</number>
<item><widget class="QToolButton" name="connectToolButton"><property name="text"><string>Connect</string></property><property name="checkable"><bool>true</bool></property></widget></item> </property>
<item><widget class="QLabel" name="routingLabel"><property name="text"><string>Line:</string></property></widget></item> <item>
<item><widget class="QToolButton" name="directRoutingButton"><property name="text"><string>Direct</string></property><property name="checkable"><bool>true</bool></property></widget></item> <widget class="QToolButton" name="navigateUpButton">
<item><widget class="QToolButton" name="angledRoutingButton"><property name="text"><string>Angled</string></property><property name="checkable"><bool>true</bool></property></widget></item> <property name="toolTip">
<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> <string>Open the containing graph</string>
</property>
<property name="text">
<string>Up</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/arrow-up.png</normaloff>:/icons/icons/arrow-up.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="navigateDownButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Open the selected block</string>
</property>
<property name="text">
<string>Down</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/arrow-down.png</normaloff>:/icons/icons/arrow-down.png</iconset>
</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>
<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="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Apply JSON</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="pointerToolButton">
<property name="text">
<string>Pointer</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/edit-select.png</normaloff>:/icons/icons/edit-select.png</iconset>
</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="QToolButton" name="boxToolButton">
<property name="toolTip">
<string>Draw a box annotation</string>
</property>
<property name="text">
<string>Box</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-rectangle.png</normaloff>:/icons/icons/draw-rectangle.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="lineToolButton">
<property name="toolTip">
<string>Draw a line annotation</string>
</property>
<property name="text">
<string>Line</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-bezier-curves.png</normaloff>:/icons/icons/draw-bezier-curves.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="textToolButton">
<property name="toolTip">
<string>Add a text annotation</string>
</property>
<property name="text">
<string>Text</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-text.png</normaloff>:/icons/icons/draw-text.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="rotateToolButton">
<property name="toolTip">
<string>Rotate selected blocks clockwise</string>
</property>
<property name="text">
<string>Rotate</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
</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>
<property name="checked">
<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>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QStackedWidget" name="workspaceStack"> <widget class="QStackedWidget" name="workspaceStack">
<property name="currentIndex"><number>0</number></property> <property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="graphPage"> <widget class="QWidget" name="graphPage">
<layout class="QVBoxLayout" name="graphPageLayout"> <layout class="QVBoxLayout" name="graphPageLayout">
<property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property> <property name="leftMargin">
<item><widget class="GraphWorkspaceView" name="graphView"/></item> <number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="GraphWorkspaceView" name="graphView"/>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="jsonPage"> <widget class="QWidget" name="jsonPage">
<layout class="QVBoxLayout" name="jsonPageLayout"> <layout class="QVBoxLayout" name="jsonPageLayout">
<property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property> <property name="leftMargin">
<item><widget class="QPlainTextEdit" name="jsonEditor"><property name="lineWrapMode"><enum>QPlainTextEdit::LineWrapMode::NoWrap</enum></property><property name="placeholderText"><string>Component JSON</string></property></widget></item> <number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPlainTextEdit" name="jsonEditor">
<property name="lineWrapMode">
<enum>QPlainTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="placeholderText">
<string>Component JSON</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="emptyPage"> <widget class="QWidget" name="emptyPage">
<property name="styleSheet"><string notr="true">background-color: #9a9a9a;</string></property> <property name="styleSheet">
<string notr="true">background-color: #9a9a9a;</string>
</property>
<layout class="QVBoxLayout" name="emptyPageLayout"> <layout class="QVBoxLayout" name="emptyPageLayout">
<item> <item>
<widget class="QLabel" name="emptyWorkspaceLabel"> <widget class="QLabel" name="emptyWorkspaceLabel">
<property name="styleSheet"><string notr="true">background: transparent; color: #202020;</string></property> <property name="styleSheet">
<property name="text"><string>No document open</string></property> <string notr="true">background: transparent; color: #202020;</string>
<property name="alignment"><set>Qt::AlignmentFlag::AlignCenter</set></property> </property>
<property name="text">
<string>No document open</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>
@@ -198,7 +492,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1000</width> <width>1209</width>
<height>24</height> <height>24</height>
</rect> </rect>
</property> </property>
@@ -314,16 +608,16 @@
<addaction name="actionCut"/> <addaction name="actionCut"/>
<addaction name="actionPaste"/> <addaction name="actionPaste"/>
</widget> </widget>
<widget class="QToolBar" name="transformToolbar">
<property name="windowTitle"><string>Transform</string></property>
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute>
<attribute name="toolBarBreak"><bool>false</bool></attribute>
<addaction name="actionRotateClockwise"/>
</widget>
<widget class="QToolBar" name="cameraToolbar"> <widget class="QToolBar" name="cameraToolbar">
<property name="windowTitle"><string>Camera</string></property> <property name="windowTitle">
<attribute name="toolBarArea"><enum>TopToolBarArea</enum></attribute> <string>Camera</string>
<attribute name="toolBarBreak"><bool>false</bool></attribute> </property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionZoomIn"/> <addaction name="actionZoomIn"/>
<addaction name="actionZoomOut"/> <addaction name="actionZoomOut"/>
<addaction name="actionCenterView"/> <addaction name="actionCenterView"/>
@@ -348,13 +642,52 @@
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset> <normaloff>:/icons/icons/transform-rotate.png</normaloff>:/icons/icons/transform-rotate.png</iconset>
</property> </property>
<property name="text"><string>Rotate Clockwise</string></property> <property name="text">
<property name="toolTip"><string>Rotate selected blocks clockwise by 90 degrees</string></property> <string>Rotate Clockwise</string>
<property name="shortcut"><string>Ctrl+R</string></property> </property>
<property name="toolTip">
<string>Rotate selected blocks clockwise by 90 degrees</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
<action name="actionZoomIn">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-in.png</normaloff>:/icons/icons/zoom-in.png</iconset>
</property>
<property name="text">
<string>Zoom In</string>
</property>
<property name="shortcut">
<string>Ctrl++</string>
</property>
</action>
<action name="actionZoomOut">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-out.png</normaloff>:/icons/icons/zoom-out.png</iconset>
</property>
<property name="text">
<string>Zoom Out</string>
</property>
<property name="shortcut">
<string>Ctrl+-</string>
</property>
</action>
<action name="actionCenterView">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-original.png</normaloff>:/icons/icons/zoom-original.png</iconset>
</property>
<property name="text">
<string>Center</string>
</property>
<property name="shortcut">
<string>Ctrl+0</string>
</property>
</action> </action>
<action name="actionZoomIn"><property name="text"><string>Zoom In</string></property><property name="shortcut"><string>Ctrl++</string></property></action>
<action name="actionZoomOut"><property name="text"><string>Zoom Out</string></property><property name="shortcut"><string>Ctrl+-</string></property></action>
<action name="actionCenterView"><property name="text"><string>Center</string></property><property name="shortcut"><string>Ctrl+0</string></property></action>
<action name="actionOpen"> <action name="actionOpen">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
@@ -406,8 +739,12 @@
</property> </property>
</action> </action>
<action name="actionClose"> <action name="actionClose">
<property name="text"><string>&amp;Close Document</string></property> <property name="text">
<property name="shortcut"><string>Ctrl+W</string></property> <string>&amp;Close Document</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action> </action>
<action name="actionUndo"> <action name="actionUndo">
<property name="icon"> <property name="icon">
@@ -478,8 +815,12 @@
</property> </property>
</action> </action>
<action name="actionDelete"> <action name="actionDelete">
<property name="text"><string>&amp;Delete</string></property> <property name="text">
<property name="shortcut"><string>Del</string></property> <string>&amp;Delete</string>
</property>
<property name="shortcut">
<string>Del</string>
</property>
</action> </action>
<action name="actionAbout"> <action name="actionAbout">
<property name="text"> <property name="text">

355
BEdit/untitled.bedit.json Normal file
View File

@@ -0,0 +1,355 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Untitled"
},
"roots": [
{
"id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c",
"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": 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": "#202020",
"fontSize": 12.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [
{
"id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
"name": "A",
"position": {
"x": -192.0,
"y": -192.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-5c5d4695",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-de109124",
"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": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#303030",
"fill": "#ffffff",
"fontSize": 12.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"text": "A",
"type": "text",
"width": 48.0,
"x": 40.0,
"y": 40.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": [],
"parameters": {}
}
}
},
{
"id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
"name": "B",
"position": {
"x": 128.0,
"y": -128.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-4f732b3e",
"name": "Port 1",
"position": {
"x": -176.0,
"y": -144.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 48.0
}
},
"type": "signal"
},
{
"id": "port-ef7c4218",
"name": "Port 2",
"position": {
"x": -176.0,
"y": -16.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 80.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-b68679b2",
"name": "Port 3",
"position": {
"x": 128.0,
"y": -80.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": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#303030",
"fill": "#ffffff",
"fontSize": 12.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"text": "B",
"type": "text",
"width": 48.0,
"x": 40.0,
"y": 40.0
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": [],
"annotations": []
}
}
}
],
"connections": [
{
"id": "c59eac1f-5f24-45de-90ee-bfd039894ed9",
"source": {
"block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
"port": "port-de109124"
},
"target": {
"block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
"port": "port-4f732b3e"
},
"name": "",
"properties": {
"routing": "angled",
"waypoints": [
{
"x": -64.0,
"y": -128.0
},
{
"x": -64.0,
"y": 64.0
},
{
"x": 64.0,
"y": 64.0
},
{
"x": 64.0,
"y": -80.0
}
]
}
}
],
"annotations": [
{
"id": "bfe7d48b-fe54-4378-ab1b-f9b46c2efa87",
"kind": "box",
"position": {
"x": 48.0,
"y": -328.0
},
"size": {
"width": 160.0,
"height": 112.0
},
"text": "",
"layer": -1,
"properties": {
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"fill": "#dbeafe",
"cornerRadius": 0.0,
"routing": "angled",
"waypoints": []
}
},
{
"id": "aa69f7b3-f5af-426b-9eb3-c63a268346b5",
"kind": "text",
"position": {
"x": -272.0,
"y": -216.0
},
"size": {
"width": 400.0,
"height": 48.0
},
"text": "This is an annotation",
"layer": -1,
"properties": {
"stroke": "#023010",
"lineWidth": 3.0,
"lineStyle": "dash",
"fill": "none",
"fontSize": 18.0,
"color": "#023010",
"routing": "angled",
"waypoints": []
}
}
]
}
}
}
]
}