Compare commits
2 Commits
09824195c9
...
8df5c2a8f9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8df5c2a8f9 | |||
| dbca41640f |
@@ -134,18 +134,18 @@ Every component owns its ports, declarative icon, properties, and child graph:
|
|||||||
"name": "My Component",
|
"name": "My Component",
|
||||||
"position": {"x": 0, "y": 0},
|
"position": {"x": 0, "y": 0},
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [{"id": "in", "name": "Input", "properties": {
|
"inputs": [{"id": "in", "name": "Input", "type": "signal", "properties": {
|
||||||
"iconPosition": {"x": 0, "y": 40}
|
"iconPosition": {"x": 0, "y": 40}
|
||||||
}}],
|
}}],
|
||||||
"outputs": [{"id": "out", "name": "Output", "properties": {
|
"outputs": [{"id": "out", "name": "Output", "type": "signal", "properties": {
|
||||||
"iconPosition": {"x": 120, "y": 40}
|
"iconPosition": {"x": 128, "y": 64}
|
||||||
}}]
|
}}]
|
||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"size": {"width": 120, "height": 80},
|
"size": {"width": 128, "height": 128},
|
||||||
"elements": [{
|
"elements": [{
|
||||||
"type": "rectangle", "x": 1, "y": 1,
|
"type": "rectangle", "x": 32, "y": 32,
|
||||||
"width": 118, "height": 78,
|
"width": 64, "height": 64,
|
||||||
"cornerRadius": 5,
|
"cornerRadius": 5,
|
||||||
"fill": "#dbeafe", "stroke": "#245c9c",
|
"fill": "#dbeafe", "stroke": "#245c9c",
|
||||||
"lineWidth": 1.5, "lineStyle": "solid"
|
"lineWidth": 1.5, "lineStyle": "solid"
|
||||||
@@ -168,6 +168,8 @@ text components use `"implementation": {"kind": "text", "source": ...}` and
|
|||||||
never own a graph. Vector icons can contain rectangles, circles, ellipses,
|
never own a graph. Vector icons can contain rectangles, circles, ellipses,
|
||||||
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
|
lines, triangles, and text. Each element owns its geometry, fill, stroke, and
|
||||||
line style; ports keep their icon anchor in `properties.iconPosition`. The
|
line style; ports keep their icon anchor in `properties.iconPosition`. The
|
||||||
|
port type registry controls which types may connect (currently `signal` only).
|
||||||
|
Graph and icon grid sizes are configured independently in Settings. The
|
||||||
recursive model is under `src/bedit/document/`, library loading
|
recursive model is under `src/bedit/document/`, library loading
|
||||||
and the live Current Document tree are under `src/bedit/library/`, and graphics
|
and the live Current Document tree are under `src/bedit/library/`, and graphics
|
||||||
are isolated under `src/bedit/workspace/`.
|
are isolated under `src/bedit/workspace/`.
|
||||||
|
|||||||
162
BEdit/data/libraries/test.bedit.json
Normal file
162
BEdit/data/libraries/test.bedit.json
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Untitled"
|
||||||
|
},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
|
||||||
|
"name": "New Graph Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0,
|
||||||
|
"width": 126.0,
|
||||||
|
"height": 126.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 8.0,
|
||||||
|
"y": 8.0,
|
||||||
|
"width": 112.0,
|
||||||
|
"height": 112.0,
|
||||||
|
"text": "Graph",
|
||||||
|
"color": "#202020",
|
||||||
|
"fontSize": 12.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
|
||||||
|
"name": "New Graph Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": -96.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-e5cb81a5",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-eb0034af",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0,
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "Graph",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ from bedit.document.model import (
|
|||||||
Port,
|
Port,
|
||||||
clone_component,
|
clone_component,
|
||||||
)
|
)
|
||||||
|
from bedit.document.port_types import PortTypeRegistry
|
||||||
from bedit.document.serializer import JsonDocumentSerializer
|
from bedit.document.serializer import JsonDocumentSerializer
|
||||||
|
|
||||||
|
|
||||||
@@ -227,12 +228,33 @@ class DocumentController(QObject):
|
|||||||
def connect(self, source: Endpoint, target: Endpoint) -> str:
|
def connect(self, source: Endpoint, target: Endpoint) -> str:
|
||||||
if self.active_component_id is None:
|
if self.active_component_id is None:
|
||||||
raise ValueError("There is no active graph")
|
raise ValueError("There is no active graph")
|
||||||
|
source_port = self._port_for_endpoint(source, "source")
|
||||||
|
target_port = self._port_for_endpoint(target, "target")
|
||||||
|
if source_port is None or target_port is None:
|
||||||
|
raise ValueError("A connection endpoint no longer exists")
|
||||||
|
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot connect {source_port.type!r} to {target_port.type!r}"
|
||||||
|
)
|
||||||
connection = Connection(str(uuid4()), source, target)
|
connection = Connection(str(uuid4()), source, target)
|
||||||
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 _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
||||||
|
owner = self.active_component
|
||||||
|
if owner is None:
|
||||||
|
return None
|
||||||
|
if endpoint.interface is not None:
|
||||||
|
ports = owner.inputs if role == "source" else owner.outputs
|
||||||
|
else:
|
||||||
|
component = owner.graph.blocks.get(endpoint.block or "")
|
||||||
|
if component is None:
|
||||||
|
return None
|
||||||
|
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)
|
||||||
|
|
||||||
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
@@ -354,6 +376,55 @@ class DocumentController(QObject):
|
|||||||
"outputs": [port.to_dict() for port in outputs],
|
"outputs": [port.to_dict() for port in outputs],
|
||||||
"show_subtree": show_subtree,
|
"show_subtree": show_subtree,
|
||||||
}
|
}
|
||||||
|
if old != new:
|
||||||
|
candidate = deepcopy(self.document)
|
||||||
|
candidate_component = candidate.find_component(component_id)
|
||||||
|
candidate_component.name = name
|
||||||
|
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||||
|
candidate_component.inputs = deepcopy(inputs)
|
||||||
|
candidate_component.outputs = deepcopy(outputs)
|
||||||
|
candidate.validate()
|
||||||
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
|
|
||||||
|
def edit_component_ports(
|
||||||
|
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
||||||
|
) -> None:
|
||||||
|
if self.document is None:
|
||||||
|
return
|
||||||
|
component = self.document.find_component(component_id)
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
input_ids = {port.id for port in inputs}
|
||||||
|
output_ids = {port.id for port in outputs}
|
||||||
|
parent = self.document.find_parent(component_id)
|
||||||
|
if parent is not None:
|
||||||
|
for connection in parent.graph.connections.values():
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
for connection in component.graph.connections.values():
|
||||||
|
if connection.source.interface and connection.source.interface not in input_ids:
|
||||||
|
raise ValueError("An interface input cannot be removed while connected")
|
||||||
|
if connection.target.interface and connection.target.interface not in output_ids:
|
||||||
|
raise ValueError("An interface output cannot be removed while connected")
|
||||||
|
candidate = deepcopy(self.document)
|
||||||
|
candidate_component = candidate.find_component(component_id)
|
||||||
|
candidate_component.inputs = deepcopy(inputs)
|
||||||
|
candidate_component.outputs = deepcopy(outputs)
|
||||||
|
candidate.validate()
|
||||||
|
old = {
|
||||||
|
"name": component.name,
|
||||||
|
"icon": component.icon.to_dict(),
|
||||||
|
"inputs": [port.to_dict() for port in component.inputs],
|
||||||
|
"outputs": [port.to_dict() for port in component.outputs],
|
||||||
|
"show_subtree": component.show_subtree_in_library,
|
||||||
|
}
|
||||||
|
new = {
|
||||||
|
**old,
|
||||||
|
"inputs": [port.to_dict() for port in inputs],
|
||||||
|
"outputs": [port.to_dict() for port in outputs],
|
||||||
|
}
|
||||||
if old != new:
|
if old != new:
|
||||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from bedit.document.port_types import PortTypeRegistry
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Port:
|
class Port:
|
||||||
@@ -13,6 +15,7 @@ class Port:
|
|||||||
x: float = 0.0
|
x: float = 0.0
|
||||||
y: float = 0.0
|
y: float = 0.0
|
||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
type: str = "signal"
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -20,6 +23,7 @@ class Port:
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"position": {"x": self.x, "y": self.y},
|
"position": {"x": self.x, "y": self.y},
|
||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
|
"type": self.type,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -31,6 +35,7 @@ class Port:
|
|||||||
x=float(position.get("x", 0.0)),
|
x=float(position.get("x", 0.0)),
|
||||||
y=float(position.get("y", 0.0)),
|
y=float(position.get("y", 0.0)),
|
||||||
properties=dict(data.get("properties", {})),
|
properties=dict(data.get("properties", {})),
|
||||||
|
type=str(data.get("type", "signal")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,22 +45,22 @@ class Icon:
|
|||||||
fill: str = "#f4f4f4"
|
fill: str = "#f4f4f4"
|
||||||
border: str = "#303030"
|
border: str = "#303030"
|
||||||
text: str = ""
|
text: str = ""
|
||||||
width: float = 120.0
|
width: float = 128.0
|
||||||
height: float = 80.0
|
height: float = 128.0
|
||||||
elements: list[dict[str, Any]] = field(default_factory=list)
|
elements: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
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",
|
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
|
||||||
"x": 1.0, "y": 1.0, "width": self.width - 2.0, "height": self.height - 2.0,
|
"x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0,
|
||||||
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
|
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
|
||||||
"lineStyle": "solid", "cornerRadius": 5.0,
|
"lineStyle": "solid", "cornerRadius": 5.0,
|
||||||
}]
|
}]
|
||||||
if self.text:
|
if self.text:
|
||||||
self.elements.append({
|
self.elements.append({
|
||||||
"type": "text", "x": 8.0, "y": 8.0,
|
"type": "text", "x": 40.0, "y": 40.0,
|
||||||
"width": self.width - 16.0, "height": self.height - 16.0,
|
"width": 48.0, "height": 48.0,
|
||||||
"text": self.text, "color": "#202020", "fontSize": 12.0,
|
"text": self.text, "color": "#202020", "fontSize": 12.0,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,14 +77,13 @@ class Icon:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
|
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
|
||||||
data = data or {}
|
data = data or {}
|
||||||
size = data.get("size", {})
|
|
||||||
icon = cls(
|
icon = cls(
|
||||||
shape=str(data.get("shape", "rectangle")),
|
shape=str(data.get("shape", "rectangle")),
|
||||||
fill=str(data.get("fill", "#f4f4f4")),
|
fill=str(data.get("fill", "#f4f4f4")),
|
||||||
border=str(data.get("border", "#303030")),
|
border=str(data.get("border", "#303030")),
|
||||||
text=str(data.get("text", "")),
|
text=str(data.get("text", "")),
|
||||||
width=float(size.get("width", 120.0)),
|
width=128.0,
|
||||||
height=float(size.get("height", 80.0)),
|
height=128.0,
|
||||||
elements=deepcopy(data.get("elements", [])),
|
elements=deepcopy(data.get("elements", [])),
|
||||||
)
|
)
|
||||||
return icon
|
return icon
|
||||||
@@ -293,21 +297,31 @@ class GraphDocument:
|
|||||||
def _validate_graph(owner: Component) -> None:
|
def _validate_graph(owner: Component) -> None:
|
||||||
input_ids = {port.id for port in owner.inputs}
|
input_ids = {port.id for port in owner.inputs}
|
||||||
output_ids = {port.id for port in owner.outputs}
|
output_ids = {port.id for port in owner.outputs}
|
||||||
|
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
|
||||||
|
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
|
||||||
|
for port in (*owner.inputs, *owner.outputs):
|
||||||
|
PortTypeRegistry.get(port.type)
|
||||||
for connection in owner.graph.connections.values():
|
for connection in owner.graph.connections.values():
|
||||||
if connection.source.interface is not None:
|
if connection.source.interface is not None:
|
||||||
if connection.source.interface not in input_ids:
|
if connection.source.interface not in input_ids:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
|
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
|
||||||
|
source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
|
||||||
else:
|
else:
|
||||||
source = owner.graph.blocks.get(connection.source.block or "")
|
source = owner.graph.blocks.get(connection.source.block or "")
|
||||||
if source is None or connection.source.port not in {p.id for p in source.outputs}:
|
if source is None or connection.source.port not in {p.id for p in source.outputs}:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown block output")
|
raise ValueError(f"Connection {connection.id} uses an unknown block output")
|
||||||
|
source_port = next(p for p in source.outputs if p.id == connection.source.port)
|
||||||
if connection.target.interface is not None:
|
if connection.target.interface is not None:
|
||||||
if connection.target.interface not in output_ids:
|
if connection.target.interface not in output_ids:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
|
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
|
||||||
|
target_port = next(p for p in owner.outputs if p.id == connection.target.interface)
|
||||||
else:
|
else:
|
||||||
target = owner.graph.blocks.get(connection.target.block or "")
|
target = owner.graph.blocks.get(connection.target.block or "")
|
||||||
if target is None or connection.target.port not in {p.id for p in target.inputs}:
|
if target is None or connection.target.port not in {p.id for p in target.inputs}:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown block input")
|
raise ValueError(f"Connection {connection.id} uses an unknown block input")
|
||||||
|
target_port = next(p for p in target.inputs if p.id == connection.target.port)
|
||||||
|
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||||
|
raise ValueError(f"Connection {connection.id} joins incompatible port types")
|
||||||
|
|
||||||
|
|
||||||
def clone_component(source: Component) -> Component:
|
def clone_component(source: Component) -> Component:
|
||||||
@@ -341,8 +355,8 @@ def clone_component(source: Component) -> Component:
|
|||||||
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)) for port in current.inputs],
|
inputs=[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)) for port in current.outputs],
|
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,
|
||||||
|
|||||||
32
BEdit/src/bedit/document/port_types.py
Normal file
32
BEdit/src/bedit/document/port_types.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortType:
|
||||||
|
id: str
|
||||||
|
display_name: str
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
def accepts(self, other: "PortType") -> bool:
|
||||||
|
return self.id == other.id
|
||||||
|
|
||||||
|
|
||||||
|
class PortTypeRegistry:
|
||||||
|
_types = {
|
||||||
|
"signal": PortType("signal", "Signal", "A scalar signal connection"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> tuple[PortType, ...]:
|
||||||
|
return tuple(cls._types.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, type_id: str) -> PortType:
|
||||||
|
try:
|
||||||
|
return cls._types[type_id]
|
||||||
|
except KeyError as error:
|
||||||
|
raise ValueError(f"Unknown port type: {type_id}") from error
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compatible(cls, first: str, second: str) -> bool:
|
||||||
|
return cls.get(first).accepts(cls.get(second))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
from PySide6.QtCore import QPointF, QRectF, QSettings, Qt
|
||||||
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QColorDialog,
|
QColorDialog,
|
||||||
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import (
|
|||||||
QGraphicsObject,
|
QGraphicsObject,
|
||||||
QGraphicsScene,
|
QGraphicsScene,
|
||||||
QGraphicsSceneContextMenuEvent,
|
QGraphicsSceneContextMenuEvent,
|
||||||
|
QGraphicsSceneMouseEvent,
|
||||||
QGraphicsView,
|
QGraphicsView,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QInputDialog,
|
QInputDialog,
|
||||||
@@ -30,6 +31,60 @@ from bedit.document.model import Component, Icon, Port
|
|||||||
from bedit.icon_renderer import _pen
|
from bedit.icon_renderer import _pen
|
||||||
|
|
||||||
|
|
||||||
|
def _icon_grid_size() -> int:
|
||||||
|
return QSettings().value("grid/iconSize", 8, type=int)
|
||||||
|
|
||||||
|
|
||||||
|
def _snap(value: float) -> float:
|
||||||
|
grid = _icon_grid_size()
|
||||||
|
return round(value / grid) * grid
|
||||||
|
|
||||||
|
|
||||||
|
class IconEditorView(QGraphicsView):
|
||||||
|
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||||
|
painter.fillRect(rect, QColor("#ffffff"))
|
||||||
|
grid = _icon_grid_size()
|
||||||
|
painter.setPen(QPen(QColor("#dbeafe"), 0))
|
||||||
|
left = int(rect.left()) - int(rect.left()) % grid
|
||||||
|
top = int(rect.top()) - int(rect.top()) % grid
|
||||||
|
for x in range(left, int(rect.right()) + grid, grid):
|
||||||
|
painter.drawLine(x, rect.top(), x, rect.bottom())
|
||||||
|
for y in range(top, int(rect.bottom()) + grid, grid):
|
||||||
|
painter.drawLine(rect.left(), y, rect.right(), y)
|
||||||
|
|
||||||
|
|
||||||
|
class ResizeHandle(QGraphicsEllipseItem):
|
||||||
|
def __init__(self, owner: "ShapeItem") -> None:
|
||||||
|
super().__init__(-4, -4, 8, 8, owner)
|
||||||
|
self.owner = owner
|
||||||
|
self.setBrush(QColor("#ffffff"))
|
||||||
|
self.setPen(QPen(QColor("#2563eb"), 1.5))
|
||||||
|
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||||
|
self.setZValue(20)
|
||||||
|
|
||||||
|
def itemChange(self, change, value): # noqa: N802
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
maximum = (
|
||||||
|
self.owner.scene().sceneRect().bottomRight() - self.owner.pos()
|
||||||
|
if self.owner.scene()
|
||||||
|
else QPointF(128, 128)
|
||||||
|
)
|
||||||
|
value = QPointF(
|
||||||
|
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
|
||||||
|
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
|
||||||
|
)
|
||||||
|
if self.owner.element.get("type") == "circle":
|
||||||
|
side = min(maximum.x(), maximum.y(), max(value.x(), value.y()))
|
||||||
|
value = QPointF(side, side)
|
||||||
|
self.owner.prepareGeometryChange()
|
||||||
|
self.owner.element["width"] = value.x()
|
||||||
|
self.owner.element["height"] = value.y()
|
||||||
|
self.owner.update()
|
||||||
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
|
||||||
class ColorButton(QPushButton):
|
class ColorButton(QPushButton):
|
||||||
def __init__(self, color: str, allow_none: bool = False, parent=None) -> None:
|
def __init__(self, color: str, allow_none: bool = False, parent=None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -128,6 +183,9 @@ class ShapeItem(QGraphicsObject):
|
|||||||
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.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
|
||||||
|
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)))
|
||||||
@@ -161,10 +219,38 @@ class ShapeItem(QGraphicsObject):
|
|||||||
painter.drawRect(rect)
|
painter.drawRect(rect)
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
def itemChange(self, change, value): # noqa: N802
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
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:
|
||||||
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:
|
||||||
|
self.resize_handle.setVisible(bool(value))
|
||||||
return super().itemChange(change, value)
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
self.setPos(
|
||||||
|
max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))),
|
||||||
|
max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))),
|
||||||
|
)
|
||||||
|
|
||||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
options = menu.addAction("Shape Options…")
|
options = menu.addAction("Shape Options…")
|
||||||
@@ -176,6 +262,10 @@ class ShapeItem(QGraphicsObject):
|
|||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
self.element.clear()
|
self.element.clear()
|
||||||
self.element.update(dialog.element)
|
self.element.update(dialog.element)
|
||||||
|
self.resize_handle.setPos(
|
||||||
|
float(self.element.get("width", 20)),
|
||||||
|
float(self.element.get("height", 20)),
|
||||||
|
)
|
||||||
self.update()
|
self.update()
|
||||||
elif chosen is delete and self.scene() is not None:
|
elif chosen is delete and self.scene() is not None:
|
||||||
self.scene().removeItem(self)
|
self.scene().removeItem(self)
|
||||||
@@ -195,10 +285,24 @@ class PortHandle(QGraphicsEllipseItem):
|
|||||||
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.ItemPositionHasChanged:
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
value = QPointF(
|
||||||
|
min(bounds.right(), max(bounds.left(), _snap(value.x()))),
|
||||||
|
min(bounds.bottom(), max(bounds.top(), _snap(value.y()))),
|
||||||
|
)
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
self.port.properties["iconPosition"] = {"x": value.x(), "y": value.y()}
|
||||||
return super().itemChange(change, value)
|
return super().itemChange(change, value)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
|
||||||
|
self.setPos(
|
||||||
|
min(bounds.right(), max(bounds.left(), _snap(self.pos().x()))),
|
||||||
|
min(bounds.bottom(), max(bounds.top(), _snap(self.pos().y()))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IconEditorDialog(QDialog):
|
class IconEditorDialog(QDialog):
|
||||||
def __init__(self, component: Component, parent=None) -> None:
|
def __init__(self, component: Component, parent=None) -> None:
|
||||||
@@ -222,11 +326,10 @@ class IconEditorDialog(QDialog):
|
|||||||
toolbar.addWidget(delete)
|
toolbar.addWidget(delete)
|
||||||
layout.addLayout(toolbar)
|
layout.addLayout(toolbar)
|
||||||
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self)
|
||||||
self.view = QGraphicsView(self.scene)
|
self.view = IconEditorView(self.scene)
|
||||||
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
self.view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
self.view.setBackgroundBrush(QColor("#f8fafc"))
|
|
||||||
self.view.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
self.view.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#94a3b8"), 0), QColor("#ffffff"))
|
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||||
layout.addWidget(self.view, 1)
|
layout.addWidget(self.view, 1)
|
||||||
layout.addWidget(QLabel("Green points are inputs; red points are outputs. Drag them to place connection anchors."))
|
layout.addWidget(QLabel("Green points are inputs; red points are outputs. Drag them to place connection anchors."))
|
||||||
for element in self.icon.elements:
|
for element in self.icon.elements:
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QPolygo
|
|||||||
from bedit.document.model import Icon
|
from bedit.document.model import Icon
|
||||||
|
|
||||||
|
|
||||||
|
def icon_bounds(icon: Icon) -> QRectF:
|
||||||
|
"""Return the automatic hitbox of all visible vector elements."""
|
||||||
|
bounds = QRectF()
|
||||||
|
for element in icon.elements:
|
||||||
|
if element.get("_deleted"):
|
||||||
|
continue
|
||||||
|
rect = QRectF(
|
||||||
|
float(element.get("x", 0)),
|
||||||
|
float(element.get("y", 0)),
|
||||||
|
max(0.0, float(element.get("width", 0))),
|
||||||
|
max(0.0, float(element.get("height", 0))),
|
||||||
|
)
|
||||||
|
bounds = rect if bounds.isNull() else bounds.united(rect)
|
||||||
|
return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64)
|
||||||
|
|
||||||
|
|
||||||
def _pen(element: dict) -> QPen:
|
def _pen(element: dict) -> QPen:
|
||||||
styles = {
|
styles = {
|
||||||
"solid": Qt.PenStyle.SolidLine,
|
"solid": Qt.PenStyle.SolidLine,
|
||||||
@@ -19,11 +35,19 @@ def _pen(element: dict) -> QPen:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def paint_icon(painter: QPainter, icon: Icon, target: QRectF) -> None:
|
def paint_icon(
|
||||||
|
painter: QPainter,
|
||||||
|
icon: Icon,
|
||||||
|
target: QRectF,
|
||||||
|
source: QRectF | None = None,
|
||||||
|
) -> None:
|
||||||
painter.save()
|
painter.save()
|
||||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
if source is None:
|
||||||
|
source = QRectF(0, 0, icon.width, icon.height)
|
||||||
painter.translate(target.topLeft())
|
painter.translate(target.topLeft())
|
||||||
painter.scale(target.width() / icon.width, target.height() / icon.height)
|
painter.scale(target.width() / source.width(), target.height() / source.height())
|
||||||
|
painter.translate(-source.left(), -source.top())
|
||||||
for element in icon.elements:
|
for element in icon.elements:
|
||||||
kind = element.get("type", "rectangle")
|
kind = element.get("type", "rectangle")
|
||||||
rect = QRectF(
|
rect = QRectF(
|
||||||
@@ -55,12 +79,20 @@ def icon_pixmap(icon: Icon, size: int = 16) -> QPixmap:
|
|||||||
pixmap = QPixmap(size, size)
|
pixmap = QPixmap(size, size)
|
||||||
pixmap.fill(Qt.GlobalColor.transparent)
|
pixmap.fill(Qt.GlobalColor.transparent)
|
||||||
painter = QPainter(pixmap)
|
painter = QPainter(pixmap)
|
||||||
ratio = min(size / icon.width, size / icon.height)
|
bounds = icon_bounds(icon)
|
||||||
width, height = icon.width * ratio, icon.height * ratio
|
ratio = min(size / bounds.width(), size / bounds.height())
|
||||||
paint_icon(painter, icon, QRectF((size - width) / 2, (size - height) / 2, width, height))
|
width, height = bounds.width() * ratio, bounds.height() * ratio
|
||||||
|
paint_icon(
|
||||||
|
painter,
|
||||||
|
icon,
|
||||||
|
QRectF((size - width) / 2, (size - height) / 2, width, height),
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
painter.end()
|
painter.end()
|
||||||
return pixmap
|
return pixmap
|
||||||
|
|
||||||
|
|
||||||
def library_icon(icon: Icon) -> QIcon:
|
def library_icon(icon: Icon) -> QIcon:
|
||||||
return QIcon(icon_pixmap(icon, 16))
|
# Render large enough for the tall Libraries rows. icon_pixmap crops to the
|
||||||
|
# vector hitbox first, so a 32x32 drawing is shown as large as a 128x128 one.
|
||||||
|
return QIcon(icon_pixmap(icon, 28))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from bedit.library.repository import LibraryRepository
|
|||||||
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1
|
COMPONENT_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||||
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole + 2
|
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole + 2
|
||||||
ITEM_KIND_ROLE = Qt.ItemDataRole.UserRole + 3
|
ITEM_KIND_ROLE = Qt.ItemDataRole.UserRole + 3
|
||||||
|
COMPONENT_INSTANCE_ROLE = Qt.ItemDataRole.UserRole + 4
|
||||||
COMPONENT_MIME_TYPE = "application/x-bedit-component"
|
COMPONENT_MIME_TYPE = "application/x-bedit-component"
|
||||||
|
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ class LibraryTreeModel(QStandardItemModel):
|
|||||||
item.setData(component.to_dict(), COMPONENT_ROLE)
|
item.setData(component.to_dict(), COMPONENT_ROLE)
|
||||||
item.setData(component.id, COMPONENT_ID_ROLE)
|
item.setData(component.id, COMPONENT_ID_ROLE)
|
||||||
item.setData("current-component" if current else "library-component", ITEM_KIND_ROLE)
|
item.setData("current-component" if current else "library-component", ITEM_KIND_ROLE)
|
||||||
|
item.setData(component, COMPONENT_INSTANCE_ROLE)
|
||||||
if component.show_subtree_in_library:
|
if component.show_subtree_in_library:
|
||||||
for child in component.graph.blocks.values():
|
for child in component.graph.blocks.values():
|
||||||
item.appendRow(self._component_item(child, current=current))
|
item.appendRow(self._component_item(child, current=current))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, QSize, Qt, Slot
|
from PySide6.QtCore import QSettings, QSize, Qt, Slot
|
||||||
@@ -7,16 +8,19 @@ from PySide6.QtWidgets import QFileDialog, QMainWindow, QMenu, QMessageBox, QToo
|
|||||||
|
|
||||||
from bedit.component_options_dialog import ComponentOptionsDialog
|
from bedit.component_options_dialog import ComponentOptionsDialog
|
||||||
from bedit.document.controller import DocumentController
|
from bedit.document.controller import DocumentController
|
||||||
from bedit.document.model import Port
|
from bedit.document.model import Component, Port
|
||||||
|
from bedit.document.serializer import JsonDocumentSerializer
|
||||||
from bedit.item_options_dialog import ItemOptionsDialog
|
from bedit.item_options_dialog import ItemOptionsDialog
|
||||||
from bedit.library.repository import LibraryRepository
|
from bedit.library.repository import LibraryRepository
|
||||||
from bedit.library.tree_model import (
|
from bedit.library.tree_model import (
|
||||||
COMPONENT_ID_ROLE,
|
COMPONENT_ID_ROLE,
|
||||||
|
COMPONENT_INSTANCE_ROLE,
|
||||||
ITEM_KIND_ROLE,
|
ITEM_KIND_ROLE,
|
||||||
DocumentTreeModel,
|
DocumentTreeModel,
|
||||||
LibraryTreeModel,
|
LibraryTreeModel,
|
||||||
)
|
)
|
||||||
from bedit.settings_dialog import SettingsDialog
|
from bedit.settings_dialog import SettingsDialog
|
||||||
|
from bedit.port_options_dialog import PortOptionsDialog
|
||||||
from bedit.ui_main_window import Ui_MainWindow
|
from bedit.ui_main_window import Ui_MainWindow
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +31,10 @@ class MainWindow(QMainWindow):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.ui = Ui_MainWindow()
|
self.ui = Ui_MainWindow()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
self.ui.emptyPage.setStyleSheet("background-color: #9a9a9a;")
|
||||||
|
self.ui.emptyWorkspaceLabel.setStyleSheet(
|
||||||
|
"background: transparent; color: #202020;"
|
||||||
|
)
|
||||||
self._create_camera_toolbar()
|
self._create_camera_toolbar()
|
||||||
self.settings = QSettings()
|
self.settings = QSettings()
|
||||||
|
|
||||||
@@ -59,10 +67,15 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _configure_models(self) -> None:
|
def _configure_models(self) -> None:
|
||||||
self.ui.treeView.setModel(self.library_tree_model)
|
self.ui.treeView.setModel(self.library_tree_model)
|
||||||
self.ui.treeView.setIconSize(QSize(16, 16))
|
self.ui.treeView.setIconSize(QSize(28, 28))
|
||||||
|
self.ui.treeView.setStyleSheet("QTreeView::item { height: 32px; }")
|
||||||
self.ui.treeView.setHeaderHidden(True)
|
self.ui.treeView.setHeaderHidden(True)
|
||||||
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.customContextMenuRequested.connect(
|
||||||
|
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.setIconSize(QSize(16, 16))
|
self.ui.documentTreeView.setIconSize(QSize(16, 16))
|
||||||
@@ -81,6 +94,7 @@ class MainWindow(QMainWindow):
|
|||||||
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)
|
||||||
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
|
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
|
||||||
|
self.ui.graphView.componentPortOptionsRequested.connect(self.show_component_port_options)
|
||||||
self.ui.graphView.portOptionsRequested.connect(self.show_port_options)
|
self.ui.graphView.portOptionsRequested.connect(self.show_port_options)
|
||||||
self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options)
|
self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options)
|
||||||
self.ui.graphView.selectionAvailabilityChanged.connect(
|
self.ui.graphView.selectionAvailabilityChanged.connect(
|
||||||
@@ -88,9 +102,8 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
|
||||||
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
|
||||||
self.ui.inputToolButton.clicked.connect(lambda: self.set_graph_tool("input"))
|
self.ui.inputToolButton.hide()
|
||||||
self.ui.outputToolButton.clicked.connect(lambda: self.set_graph_tool("output"))
|
self.ui.outputToolButton.hide()
|
||||||
self.ui.graphView.toolUsed.connect(lambda: self.set_graph_tool("pointer"))
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -199,12 +212,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
||||||
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
||||||
self.ui.applyJsonButton.setVisible(not is_graph)
|
self.ui.applyJsonButton.setVisible(not is_graph)
|
||||||
for button in (
|
self.ui.pointerToolButton.setVisible(is_graph)
|
||||||
self.ui.pointerToolButton,
|
self.ui.inputToolButton.hide()
|
||||||
self.ui.inputToolButton,
|
self.ui.outputToolButton.hide()
|
||||||
self.ui.outputToolButton,
|
|
||||||
):
|
|
||||||
button.setVisible(is_graph)
|
|
||||||
if is_graph:
|
if is_graph:
|
||||||
self.set_graph_tool("pointer")
|
self.set_graph_tool("pointer")
|
||||||
else:
|
else:
|
||||||
@@ -229,13 +239,8 @@ class MainWindow(QMainWindow):
|
|||||||
self.document_controller.navigate_up()
|
self.document_controller.navigate_up()
|
||||||
|
|
||||||
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("pointer")
|
||||||
buttons = {
|
self.ui.pointerToolButton.setChecked(True)
|
||||||
"pointer": self.ui.pointerToolButton,
|
|
||||||
"input": self.ui.inputToolButton,
|
|
||||||
"output": self.ui.outputToolButton,
|
|
||||||
}
|
|
||||||
buttons[mode].setChecked(True)
|
|
||||||
|
|
||||||
def _load_source_json(self) -> None:
|
def _load_source_json(self) -> None:
|
||||||
component = self.document_controller.active_component
|
component = self.document_controller.active_component
|
||||||
@@ -372,8 +377,15 @@ class MainWindow(QMainWindow):
|
|||||||
def show_settings(self) -> None:
|
def show_settings(self) -> None:
|
||||||
dialog = SettingsDialog(self)
|
dialog = SettingsDialog(self)
|
||||||
dialog.settingsChanged.connect(self.reload_libraries)
|
dialog.settingsChanged.connect(self.reload_libraries)
|
||||||
|
dialog.settingsChanged.connect(self.refresh_editor_settings)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
|
def refresh_editor_settings(self) -> None:
|
||||||
|
scene = self.ui.graphView.scene()
|
||||||
|
if scene is not None:
|
||||||
|
scene.update()
|
||||||
|
self.ui.graphView.viewport().update()
|
||||||
|
|
||||||
def _document_opened_changed(self, opened: bool) -> None:
|
def _document_opened_changed(self, opened: bool) -> None:
|
||||||
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):
|
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):
|
||||||
action.setEnabled(opened)
|
action.setEnabled(opened)
|
||||||
@@ -406,6 +418,7 @@ class MainWindow(QMainWindow):
|
|||||||
text_action = menu.addAction("New Text Block")
|
text_action = menu.addAction("New Text Block")
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
options_action = menu.addAction("Component Options…")
|
options_action = menu.addAction("Component Options…")
|
||||||
|
ports_action = menu.addAction("Port Options…")
|
||||||
delete_action = menu.addAction("Delete")
|
delete_action = menu.addAction("Delete")
|
||||||
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||||
if selected is graph_action:
|
if selected is graph_action:
|
||||||
@@ -414,6 +427,8 @@ class MainWindow(QMainWindow):
|
|||||||
self.document_controller.add_child(component_id, "text")
|
self.document_controller.add_child(component_id, "text")
|
||||||
elif selected is options_action:
|
elif selected is options_action:
|
||||||
self.show_component_options(component_id)
|
self.show_component_options(component_id)
|
||||||
|
elif selected is ports_action:
|
||||||
|
self.show_component_port_options(component_id)
|
||||||
elif selected is delete_action:
|
elif selected is delete_action:
|
||||||
answer = QMessageBox.question(
|
answer = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
@@ -435,6 +450,56 @@ class MainWindow(QMainWindow):
|
|||||||
elif selected is text_action:
|
elif selected is text_action:
|
||||||
self.document_controller.add_root("text")
|
self.document_controller.add_root("text")
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def show_external_library_context_menu(self, position) -> None:
|
||||||
|
tree = self.ui.treeView
|
||||||
|
index = tree.indexAt(position)
|
||||||
|
component = index.data(COMPONENT_INSTANCE_ROLE)
|
||||||
|
if not isinstance(component, Component):
|
||||||
|
return
|
||||||
|
menu = QMenu(self)
|
||||||
|
ports_action = menu.addAction("Port Options…")
|
||||||
|
if menu.exec(tree.viewport().mapToGlobal(position)) is ports_action:
|
||||||
|
dialog = PortOptionsDialog(component, self)
|
||||||
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
|
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
|
||||||
|
component.inputs = dialog.inputs
|
||||||
|
component.outputs = dialog.outputs
|
||||||
|
library = next(
|
||||||
|
(
|
||||||
|
library
|
||||||
|
for library in self.libraries.libraries
|
||||||
|
if any(item is component for item in library.document.all_components())
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if library is not None:
|
||||||
|
library.document.validate()
|
||||||
|
JsonDocumentSerializer.save(
|
||||||
|
library.document, Path(library.source_path)
|
||||||
|
)
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
component.inputs, component.outputs = old_inputs, old_outputs
|
||||||
|
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||||
|
self.library_tree_model.rebuild()
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_component_port_options(self, component_id: str) -> None:
|
||||||
|
document = self.document_controller.document
|
||||||
|
component = document.find_component(component_id) if document else None
|
||||||
|
if component is None:
|
||||||
|
return
|
||||||
|
dialog = PortOptionsDialog(component, self)
|
||||||
|
if dialog.exec() != dialog.DialogCode.Accepted:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.document_controller.edit_component_ports(
|
||||||
|
component_id, dialog.inputs, dialog.outputs
|
||||||
|
)
|
||||||
|
except ValueError as error:
|
||||||
|
QMessageBox.warning(self, "Cannot change ports", str(error))
|
||||||
|
|
||||||
@Slot(str)
|
@Slot(str)
|
||||||
def show_component_options(self, component_id: str) -> None:
|
def show_component_options(self, component_id: str) -> None:
|
||||||
document = self.document_controller.document
|
document = self.document_controller.document
|
||||||
|
|||||||
162
BEdit/src/bedit/port_options_dialog.py
Normal file
162
BEdit/src/bedit/port_options_dialog.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QSplitter,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bedit.document.model import Component, Port
|
||||||
|
from bedit.document.port_types import PortTypeRegistry
|
||||||
|
|
||||||
|
|
||||||
|
PORT_ROLE = Qt.ItemDataRole.UserRole
|
||||||
|
|
||||||
|
|
||||||
|
class PortOptionsDialog(QDialog):
|
||||||
|
"""Unified editor for a component's typed, oriented ports."""
|
||||||
|
|
||||||
|
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(f"Port Options — {component.name}")
|
||||||
|
self.resize(620, 380)
|
||||||
|
self.ports: list[tuple[Port, str]] = [
|
||||||
|
*((deepcopy(port), "input") for port in component.inputs),
|
||||||
|
*((deepcopy(port), "output") for port in component.outputs),
|
||||||
|
]
|
||||||
|
self._loading = False
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
splitter = QSplitter()
|
||||||
|
left = QWidget()
|
||||||
|
left_layout = QVBoxLayout(left)
|
||||||
|
self.list = QListWidget()
|
||||||
|
self.list.currentRowChanged.connect(self._load_current)
|
||||||
|
left_layout.addWidget(self.list)
|
||||||
|
port_buttons = QHBoxLayout()
|
||||||
|
self.add_button = QPushButton("Add Port")
|
||||||
|
self.remove_button = QPushButton("Remove Port")
|
||||||
|
self.add_button.clicked.connect(self.add_port)
|
||||||
|
self.remove_button.clicked.connect(self.remove_port)
|
||||||
|
port_buttons.addWidget(self.add_button)
|
||||||
|
port_buttons.addWidget(self.remove_button)
|
||||||
|
left_layout.addLayout(port_buttons)
|
||||||
|
right = QWidget()
|
||||||
|
form = QFormLayout(right)
|
||||||
|
self.name_edit = QLineEdit()
|
||||||
|
self.type_combo = QComboBox()
|
||||||
|
for port_type in PortTypeRegistry.all():
|
||||||
|
self.type_combo.addItem(port_type.display_name, port_type.id)
|
||||||
|
self.orientation_combo = QComboBox()
|
||||||
|
self.orientation_combo.addItem("Input", "input")
|
||||||
|
self.orientation_combo.addItem("Output", "output")
|
||||||
|
form.addRow("Name:", self.name_edit)
|
||||||
|
form.addRow("Type:", self.type_combo)
|
||||||
|
form.addRow("Orientation:", self.orientation_combo)
|
||||||
|
form.addRow("", QLabel("New ports start at (0, 0) in the icon editor."))
|
||||||
|
self.name_edit.textEdited.connect(self._store_current)
|
||||||
|
self.type_combo.currentIndexChanged.connect(self._store_current)
|
||||||
|
self.orientation_combo.currentIndexChanged.connect(self._store_current)
|
||||||
|
splitter.addWidget(left)
|
||||||
|
splitter.addWidget(right)
|
||||||
|
splitter.setSizes([250, 370])
|
||||||
|
layout.addWidget(splitter)
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
if read_only:
|
||||||
|
self.add_button.setEnabled(False)
|
||||||
|
self.remove_button.setEnabled(False)
|
||||||
|
self.name_edit.setReadOnly(True)
|
||||||
|
self.type_combo.setEnabled(False)
|
||||||
|
self.orientation_combo.setEnabled(False)
|
||||||
|
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
|
||||||
|
buttons.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||||
|
self._rebuild_list(0 if self.ports else -1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inputs(self) -> list[Port]:
|
||||||
|
return [port for port, orientation in self.ports if orientation == "input"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outputs(self) -> list[Port]:
|
||||||
|
return [port for port, orientation in self.ports if orientation == "output"]
|
||||||
|
|
||||||
|
def _rebuild_list(self, row: int = -1) -> None:
|
||||||
|
self.list.clear()
|
||||||
|
for port, orientation in self.ports:
|
||||||
|
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
||||||
|
item.setData(PORT_ROLE, port.id)
|
||||||
|
self.list.addItem(item)
|
||||||
|
self.list.setCurrentRow(min(row, len(self.ports) - 1))
|
||||||
|
self._update_enabled()
|
||||||
|
|
||||||
|
def _load_current(self, row: int) -> None:
|
||||||
|
self._loading = True
|
||||||
|
enabled = 0 <= row < len(self.ports)
|
||||||
|
if enabled:
|
||||||
|
port, orientation = self.ports[row]
|
||||||
|
self.name_edit.setText(port.name)
|
||||||
|
self.type_combo.setCurrentIndex(self.type_combo.findData(port.type))
|
||||||
|
self.orientation_combo.setCurrentIndex(self.orientation_combo.findData(orientation))
|
||||||
|
else:
|
||||||
|
self.name_edit.clear()
|
||||||
|
self._loading = False
|
||||||
|
self._update_enabled()
|
||||||
|
|
||||||
|
def _update_enabled(self) -> None:
|
||||||
|
enabled = self.list.currentRow() >= 0
|
||||||
|
self.remove_button.setEnabled(enabled)
|
||||||
|
self.name_edit.setEnabled(enabled)
|
||||||
|
self.type_combo.setEnabled(enabled)
|
||||||
|
self.orientation_combo.setEnabled(enabled)
|
||||||
|
|
||||||
|
def _store_current(self) -> None:
|
||||||
|
row = self.list.currentRow()
|
||||||
|
if self._loading or not (0 <= row < len(self.ports)):
|
||||||
|
return
|
||||||
|
port, _orientation = self.ports[row]
|
||||||
|
port.name = self.name_edit.text()
|
||||||
|
port.type = self.type_combo.currentData()
|
||||||
|
self.ports[row] = (port, self.orientation_combo.currentData())
|
||||||
|
self.list.item(row).setText(
|
||||||
|
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_port(self) -> None:
|
||||||
|
port = Port(
|
||||||
|
id=f"port-{uuid4().hex[:8]}",
|
||||||
|
name=f"Port {len(self.ports) + 1}",
|
||||||
|
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||||
|
type="signal",
|
||||||
|
)
|
||||||
|
self.ports.append((port, "input"))
|
||||||
|
self._rebuild_list(len(self.ports) - 1)
|
||||||
|
self.name_edit.selectAll()
|
||||||
|
self.name_edit.setFocus()
|
||||||
|
|
||||||
|
def remove_port(self) -> None:
|
||||||
|
row = self.list.currentRow()
|
||||||
|
if row >= 0:
|
||||||
|
self.ports.pop(row)
|
||||||
|
self._rebuild_list(min(row, len(self.ports) - 1))
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
self._store_current()
|
||||||
|
if any(not port.name.strip() for port, _orientation in self.ports):
|
||||||
|
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
||||||
|
return
|
||||||
|
super().accept()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QSettings, Signal
|
from PySide6.QtCore import QSettings, Signal
|
||||||
from PySide6.QtWidgets import QDialog, QFileDialog
|
from PySide6.QtWidgets import QDialog, QFileDialog, QFormLayout, QGroupBox, QSpinBox
|
||||||
|
|
||||||
from bedit.library.repository import default_library_paths
|
from bedit.library.repository import default_library_paths
|
||||||
from bedit.ui_settings_dialog import Ui_SettingsDialog
|
from bedit.ui_settings_dialog import Ui_SettingsDialog
|
||||||
@@ -16,6 +16,17 @@ class SettingsDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ui = Ui_SettingsDialog()
|
self.ui = Ui_SettingsDialog()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
|
self.grid_group = QGroupBox("Editor grids", self.ui.generalTab)
|
||||||
|
grid_form = QFormLayout(self.grid_group)
|
||||||
|
self.graph_grid_spin = QSpinBox()
|
||||||
|
self.graph_grid_spin.setRange(2, 256)
|
||||||
|
self.graph_grid_spin.setSuffix(" units")
|
||||||
|
self.icon_grid_spin = QSpinBox()
|
||||||
|
self.icon_grid_spin.setRange(1, 64)
|
||||||
|
self.icon_grid_spin.setSuffix(" units")
|
||||||
|
grid_form.addRow("Graph grid size:", self.graph_grid_spin)
|
||||||
|
grid_form.addRow("Icon grid size:", self.icon_grid_spin)
|
||||||
|
self.ui.generalLayout.insertWidget(1, self.grid_group)
|
||||||
self.settings = QSettings()
|
self.settings = QSettings()
|
||||||
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
|
||||||
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
|
||||||
@@ -32,6 +43,8 @@ class SettingsDialog(QDialog):
|
|||||||
)
|
)
|
||||||
self.ui.libraryPathsList.clear()
|
self.ui.libraryPathsList.clear()
|
||||||
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
self.ui.libraryPathsList.addItems(self.library_paths(self.settings))
|
||||||
|
self.graph_grid_spin.setValue(self.graph_grid_size(self.settings))
|
||||||
|
self.icon_grid_spin.setValue(self.icon_grid_size(self.settings))
|
||||||
self._update_remove_button()
|
self._update_remove_button()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -42,6 +55,14 @@ class SettingsDialog(QDialog):
|
|||||||
return [value]
|
return [value]
|
||||||
return [str(path) for path in value]
|
return [str(path) for path in value]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def graph_grid_size(settings: QSettings | None = None) -> int:
|
||||||
|
return (settings or QSettings()).value("grid/graphSize", 32, type=int)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def icon_grid_size(settings: QSettings | None = None) -> int:
|
||||||
|
return (settings or QSettings()).value("grid/iconSize", 8, type=int)
|
||||||
|
|
||||||
def _add_library_file(self) -> None:
|
def _add_library_file(self) -> None:
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
self,
|
self,
|
||||||
@@ -83,6 +104,8 @@ class SettingsDialog(QDialog):
|
|||||||
for row in range(self.ui.libraryPathsList.count())
|
for row in range(self.ui.libraryPathsList.count())
|
||||||
]
|
]
|
||||||
self.settings.setValue("libraries/paths", paths)
|
self.settings.setValue("libraries/paths", paths)
|
||||||
|
self.settings.setValue("grid/graphSize", self.graph_grid_spin.value())
|
||||||
|
self.settings.setValue("grid/iconSize", self.icon_grid_spin.value())
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.settingsChanged.emit()
|
self.settingsChanged.emit()
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from PySide6.QtCore import QMimeData, QPointF, QRectF, Qt, Signal
|
from PySide6.QtCore import QMimeData, QPointF, QRectF, QSettings, Qt, Signal
|
||||||
from PySide6.QtGui import (
|
from PySide6.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
QDragEnterEvent,
|
QDragEnterEvent,
|
||||||
@@ -24,18 +24,28 @@ from PySide6.QtWidgets import (
|
|||||||
QApplication,
|
QApplication,
|
||||||
QMenu,
|
QMenu,
|
||||||
QStyleOptionGraphicsItem,
|
QStyleOptionGraphicsItem,
|
||||||
|
QToolTip,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bedit.document.controller import DocumentController
|
from bedit.document.controller import DocumentController
|
||||||
from bedit.document.model import Component, Connection, Endpoint, Port
|
from bedit.document.model import Component, Connection, Endpoint, Port
|
||||||
from bedit.library.tree_model import COMPONENT_MIME_TYPE
|
from bedit.library.tree_model import COMPONENT_MIME_TYPE
|
||||||
from bedit.icon_renderer import paint_icon
|
from bedit.icon_renderer import icon_bounds, paint_icon
|
||||||
|
|
||||||
|
|
||||||
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
SELECTION_MIME_TYPE = "application/x-bedit-selection"
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_grid_size() -> int:
|
||||||
|
return QSettings().value("grid/graphSize", 32, type=int)
|
||||||
|
|
||||||
|
|
||||||
|
def _snapped(position: QPointF) -> QPointF:
|
||||||
|
grid = _graph_grid_size()
|
||||||
|
return QPointF(round(position.x() / grid) * grid, round(position.y() / grid) * grid)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionPortItem(QGraphicsEllipseItem):
|
class ConnectionPortItem(QGraphicsEllipseItem):
|
||||||
def __init__(self, endpoint: Endpoint, role: str, label: str, parent: QGraphicsItem) -> None:
|
def __init__(self, endpoint: Endpoint, role: str, label: str, parent: QGraphicsItem) -> None:
|
||||||
super().__init__(-6, -6, 12, 12, parent)
|
super().__init__(-6, -6, 12, 12, parent)
|
||||||
@@ -48,8 +58,8 @@ class ConnectionPortItem(QGraphicsEllipseItem):
|
|||||||
|
|
||||||
|
|
||||||
class ComponentGraphicsItem(QGraphicsObject):
|
class ComponentGraphicsItem(QGraphicsObject):
|
||||||
WIDTH = 120.0
|
WIDTH = 128.0
|
||||||
HEIGHT = 72.0
|
HEIGHT = 128.0
|
||||||
|
|
||||||
def __init__(self, component: Component, controller: DocumentController) -> None:
|
def __init__(self, component: Component, controller: DocumentController) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -57,6 +67,7 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
self.component = component
|
self.component = component
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.drag_start = QPointF()
|
self.drag_start = QPointF()
|
||||||
|
self.hitbox = icon_bounds(component.icon)
|
||||||
self.setFlags(
|
self.setFlags(
|
||||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
@@ -64,7 +75,7 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
)
|
)
|
||||||
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
||||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
||||||
self.setTransformOriginPoint(self.WIDTH / 2, self.HEIGHT / 2)
|
self.setTransformOriginPoint(self.hitbox.center())
|
||||||
self.setRotation(component.rotation)
|
self.setRotation(component.rotation)
|
||||||
|
|
||||||
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
||||||
@@ -82,7 +93,7 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def boundingRect(self) -> QRectF: # noqa: N802
|
def boundingRect(self) -> QRectF: # noqa: N802
|
||||||
return QRectF(0, 0, self.WIDTH, self.HEIGHT)
|
return self.hitbox
|
||||||
|
|
||||||
def paint(
|
def paint(
|
||||||
self,
|
self,
|
||||||
@@ -91,7 +102,11 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
widget: QWidget | None = None,
|
widget: QWidget | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
del option, widget
|
del option, widget
|
||||||
paint_icon(painter, self.component.icon, self.boundingRect())
|
paint_icon(
|
||||||
|
painter,
|
||||||
|
self.component.icon,
|
||||||
|
QRectF(0, 0, self.WIDTH, self.HEIGHT),
|
||||||
|
)
|
||||||
if self.isSelected():
|
if self.isSelected():
|
||||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||||
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
painter.setPen(QPen(QColor("#2563eb"), 2, Qt.PenStyle.DashLine))
|
||||||
@@ -109,10 +124,16 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
self.setSelected(True)
|
self.setSelected(True)
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
options_action = menu.addAction("Component Options…")
|
options_action = menu.addAction("Component Options…")
|
||||||
if menu.exec(event.screenPos()) is options_action:
|
ports_action = menu.addAction("Port Options…")
|
||||||
|
selected = menu.exec(event.screenPos())
|
||||||
|
if selected is options_action:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.componentOptionsRequested.emit(self.component_id)
|
scene.componentOptionsRequested.emit(self.component_id)
|
||||||
|
elif selected is ports_action:
|
||||||
|
scene = self.scene()
|
||||||
|
if isinstance(scene, GraphScene):
|
||||||
|
scene.componentPortOptionsRequested.emit(self.component_id)
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
@@ -121,10 +142,14 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
|
|
||||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
self.controller.move_component(self.component_id, self.drag_start, self.pos())
|
snapped = _snapped(self.pos())
|
||||||
|
self.setPos(snapped)
|
||||||
|
self.controller.move_component(self.component_id, self.drag_start, snapped)
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
def itemChange(self, change, value): # noqa: N802
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
value = _snapped(value)
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.update_connections_for_block(self.component_id)
|
scene.update_connections_for_block(self.component_id)
|
||||||
@@ -190,10 +215,14 @@ class InterfaceTerminalItem(QGraphicsObject):
|
|||||||
|
|
||||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
self.controller.move_interface_port(self.port.id, self.drag_start, self.pos())
|
snapped = _snapped(self.pos())
|
||||||
|
self.setPos(snapped)
|
||||||
|
self.controller.move_interface_port(self.port.id, self.drag_start, snapped)
|
||||||
|
|
||||||
def itemChange(self, change, value): # noqa: N802
|
def itemChange(self, change, value): # noqa: N802
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||||
|
value = _snapped(value)
|
||||||
|
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.update_connections_for_interface(self.port.id)
|
scene.update_connections_for_interface(self.port.id)
|
||||||
@@ -236,6 +265,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
|
|
||||||
class GraphScene(QGraphicsScene):
|
class GraphScene(QGraphicsScene):
|
||||||
componentOptionsRequested = Signal(str)
|
componentOptionsRequested = Signal(str)
|
||||||
|
componentPortOptionsRequested = Signal(str)
|
||||||
portOptionsRequested = Signal(str, str)
|
portOptionsRequested = Signal(str, str)
|
||||||
connectionOptionsRequested = Signal(str)
|
connectionOptionsRequested = Signal(str)
|
||||||
|
|
||||||
@@ -308,16 +338,8 @@ class GraphScene(QGraphicsScene):
|
|||||||
self.update_connection(connection.id)
|
self.update_connection(connection.id)
|
||||||
|
|
||||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||||
painter.fillRect(rect, QColor("#e4e4e4"))
|
# The view paints the grid so it always covers the complete viewport.
|
||||||
if self.controller.document is None or self.controller.active_component is None:
|
del painter, rect
|
||||||
return
|
|
||||||
spacing = 32
|
|
||||||
left = int(rect.left()) - (int(rect.left()) % spacing)
|
|
||||||
top = int(rect.top()) - (int(rect.top()) % spacing)
|
|
||||||
painter.setPen(QPen(QColor("#b8b8b8"), 1))
|
|
||||||
for x in range(left, int(rect.right()) + spacing, spacing):
|
|
||||||
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
|
||||||
painter.drawPoint(x, y)
|
|
||||||
|
|
||||||
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
||||||
if endpoint.interface is not None:
|
if endpoint.interface is not None:
|
||||||
@@ -354,7 +376,10 @@ class GraphScene(QGraphicsScene):
|
|||||||
item.setBrush(QColor("#f5b642"))
|
item.setBrush(QColor("#f5b642"))
|
||||||
elif self.pending_source is not None:
|
elif self.pending_source is not None:
|
||||||
if self.pending_source.endpoint != item.endpoint:
|
if self.pending_source.endpoint != item.endpoint:
|
||||||
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
try:
|
||||||
|
self.controller.connect(self.pending_source.endpoint, item.endpoint)
|
||||||
|
except ValueError as error:
|
||||||
|
QToolTip.showText(event.screenPos(), str(error))
|
||||||
self._clear_pending_source()
|
self._clear_pending_source()
|
||||||
event.accept()
|
event.accept()
|
||||||
return
|
return
|
||||||
@@ -370,6 +395,7 @@ class GraphScene(QGraphicsScene):
|
|||||||
class GraphWorkspaceView(QGraphicsView):
|
class GraphWorkspaceView(QGraphicsView):
|
||||||
toolUsed = Signal()
|
toolUsed = Signal()
|
||||||
componentOptionsRequested = Signal(str)
|
componentOptionsRequested = Signal(str)
|
||||||
|
componentPortOptionsRequested = Signal(str)
|
||||||
portOptionsRequested = Signal(str, str)
|
portOptionsRequested = Signal(str, str)
|
||||||
connectionOptionsRequested = Signal(str)
|
connectionOptionsRequested = Signal(str)
|
||||||
selectionAvailabilityChanged = Signal(bool)
|
selectionAvailabilityChanged = Signal(bool)
|
||||||
@@ -382,10 +408,28 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
||||||
self.setBackgroundBrush(QColor("#9a9a9a"))
|
self.setBackgroundBrush(QColor("#f7f7f7"))
|
||||||
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||||
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
||||||
|
|
||||||
|
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||||
|
"""Paint the visible graph viewport in scene coordinates."""
|
||||||
|
painter.fillRect(rect, QColor("#f7f7f7"))
|
||||||
|
if (
|
||||||
|
self.controller is None
|
||||||
|
or self.controller.document is None
|
||||||
|
or self.controller.active_component is None
|
||||||
|
):
|
||||||
|
return
|
||||||
|
spacing = _graph_grid_size()
|
||||||
|
left = int(rect.left()) - (int(rect.left()) % spacing)
|
||||||
|
top = int(rect.top()) - (int(rect.top()) % spacing)
|
||||||
|
painter.setPen(QPen(QColor("#c5cbd1"), 0))
|
||||||
|
for x in range(left, int(rect.right()) + spacing, spacing):
|
||||||
|
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||||
|
for y in range(top, int(rect.bottom()) + spacing, spacing):
|
||||||
|
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||||
|
|
||||||
def _zoom(self, factor: float) -> None:
|
def _zoom(self, factor: float) -> None:
|
||||||
current = self.transform().m11()
|
current = self.transform().m11()
|
||||||
target = current * factor
|
target = current * factor
|
||||||
@@ -417,6 +461,7 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
scene = GraphScene(controller, self)
|
scene = GraphScene(controller, self)
|
||||||
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
|
||||||
|
scene.componentPortOptionsRequested.connect(self.componentPortOptionsRequested)
|
||||||
scene.portOptionsRequested.connect(self.portOptionsRequested)
|
scene.portOptionsRequested.connect(self.portOptionsRequested)
|
||||||
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
|
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
|
||||||
scene.selectionChanged.connect(
|
scene.selectionChanged.connect(
|
||||||
@@ -530,15 +575,6 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||||
if (
|
|
||||||
self.controller is not None
|
|
||||||
and self.tool_mode in {"input", "output"}
|
|
||||||
and event.button() == Qt.MouseButton.LeftButton
|
|
||||||
):
|
|
||||||
self.controller.add_interface_port(self.tool_mode, self.mapToScene(event.position().toPoint()))
|
|
||||||
self.toolUsed.emit()
|
|
||||||
event.accept()
|
|
||||||
return
|
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
def dragEnterEvent(self, event: QDragEnterEvent) -> None: # noqa: N802
|
||||||
@@ -569,5 +605,7 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
return
|
return
|
||||||
data = json.loads(bytes(event.mimeData().data(COMPONENT_MIME_TYPE)).decode("utf-8"))
|
data = json.loads(bytes(event.mimeData().data(COMPONENT_MIME_TYPE)).decode("utf-8"))
|
||||||
source = Component.from_dict(data)
|
source = Component.from_dict(data)
|
||||||
self.controller.add_component_copy(source, self.mapToScene(event.position().toPoint()))
|
self.controller.add_component_copy(
|
||||||
|
source, _snapped(self.mapToScene(event.position().toPoint()))
|
||||||
|
)
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
|
|||||||
162
BEdit/test.bedit.json
Normal file
162
BEdit/test.bedit.json
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{
|
||||||
|
"format": "bedit-document",
|
||||||
|
"version": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Untitled"
|
||||||
|
},
|
||||||
|
"roots": [
|
||||||
|
{
|
||||||
|
"id": "5ee742db-b25c-4d86-b4ca-cc0e61c4002a",
|
||||||
|
"name": "New Graph Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 1.0,
|
||||||
|
"width": 126.0,
|
||||||
|
"height": 126.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 8.0,
|
||||||
|
"y": 8.0,
|
||||||
|
"width": 112.0,
|
||||||
|
"height": 112.0,
|
||||||
|
"text": "Graph",
|
||||||
|
"color": "#202020",
|
||||||
|
"fontSize": 12.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "89944479-e73e-446a-8d58-ebf35ac4144b",
|
||||||
|
"name": "New Graph Block 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": -96.0
|
||||||
|
},
|
||||||
|
"rotation": 0.0,
|
||||||
|
"interface": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "port-e5cb81a5",
|
||||||
|
"name": "Port 1",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "port-eb0034af",
|
||||||
|
"name": "Port 2",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 0.0
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"iconPosition": {
|
||||||
|
"x": 96.0,
|
||||||
|
"y": 64.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"shape": "rectangle",
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"border": "#303030",
|
||||||
|
"text": "Graph",
|
||||||
|
"size": {
|
||||||
|
"width": 128.0,
|
||||||
|
"height": 128.0
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "rectangle",
|
||||||
|
"x": 32.0,
|
||||||
|
"y": 32.0,
|
||||||
|
"width": 64.0,
|
||||||
|
"height": 64.0,
|
||||||
|
"fill": "#f4f4f4",
|
||||||
|
"stroke": "#303030",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"cornerRadius": 5.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0,
|
||||||
|
"width": 48.0,
|
||||||
|
"height": 48.0,
|
||||||
|
"text": "Graph",
|
||||||
|
"color": "#303030",
|
||||||
|
"fontSize": 12.0,
|
||||||
|
"lineStyle": "solid",
|
||||||
|
"lineWidth": 1.5,
|
||||||
|
"stroke": "#303030",
|
||||||
|
"fill": "#ffffff"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {},
|
||||||
|
"library": {
|
||||||
|
"showSubtree": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"kind": "graph",
|
||||||
|
"graph": {
|
||||||
|
"blocks": [],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user