diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 460fa21..64240e4 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -121,6 +121,9 @@ pyside6-uic --from-imports ui/shape_options_dialog.ui \ pyside6-uic --from-imports ui/icon_editor_dialog.ui \ -o src/bedit/gui/generated/ui_icon_editor_dialog.py + +pyside6-uic --from-imports ui/text_definition_editor.ui \ + -o src/bedit/gui/generated/ui_text_definition_editor.py ``` When adding a promoted/custom widget in Designer, its header must use the real diff --git a/BEdit/src/bedit/core/libraries.py b/BEdit/src/bedit/core/libraries.py index 8da3b21..0d3d51d 100644 --- a/BEdit/src/bedit/core/libraries.py +++ b/BEdit/src/bedit/core/libraries.py @@ -13,7 +13,7 @@ class LibraryDocument: def bundled_library_path() -> Path: - return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json" + return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.json" def default_library_paths() -> list[str]: diff --git a/BEdit/src/bedit/core/model.py b/BEdit/src/bedit/core/model.py index b918133..52da2f4 100644 --- a/BEdit/src/bedit/core/model.py +++ b/BEdit/src/bedit/core/model.py @@ -152,6 +152,30 @@ class Connection: ) +@dataclass +class Parameter: + id: str + name: str + type: str = "real" + value: str = "0" + + def to_dict(self) -> dict[str, str]: + return {"id": self.id, "name": self.name, "type": self.type, "value": self.value} + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "Parameter": + if not isinstance(data, dict): + raise ValueError("Each text component parameter must be an object") + if "id" not in data: + raise ValueError("Each text component parameter must have an ID") + return cls( + id=str(data["id"]), + name=str(data.get("name", "")), + type=str(data.get("type", "real")), + value=str(data.get("value", "0")), + ) + + @dataclass class Annotation: id: str @@ -271,6 +295,19 @@ class Component: kind = str(implementation.get("kind", "graph")) if kind not in {"graph", "text"}: raise ValueError(f"Unknown component implementation kind: {kind}") + source: dict[str, Any] = {} + if kind == "text": + raw_source = implementation.get("source", {}) + equations = raw_source.get("equations", "") + parameters = raw_source.get("parameters", []) + if not isinstance(equations, str): + raise ValueError("Text component equations must be a string") + if not isinstance(parameters, list): + raise ValueError("Text component parameters must be a list") + source = { + "equations": equations, + "parameters": [Parameter.from_dict(item).to_dict() for item in parameters], + } return cls( id=str(data["id"]), name=str(data.get("name", "Unnamed")), @@ -284,7 +321,7 @@ class Component: show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)), implementation_kind=kind, graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(), - source=dict(implementation.get("source", {})) if kind == "text" else {}, + source=source, ) @@ -348,6 +385,9 @@ class GraphDocument: def validate(self) -> None: seen: set[str] = set() + root_names = [component.name for component in self.roots.values()] + if len(set(root_names)) != len(root_names): + raise ValueError("Root component names must be unique") for component in self.all_components(): if component.id in seen: raise ValueError(f"Duplicate component ID: {component.id}") @@ -358,6 +398,11 @@ class GraphDocument: @staticmethod def _validate_graph(owner: Component) -> None: + child_names = [component.name for component in owner.graph.blocks.values()] + if len(set(child_names)) != len(child_names): + raise ValueError( + f"Component names inside {owner.name!r} must be unique" + ) input_ids = {port.id for port in owner.inputs} output_ids = {port.id for port in owner.outputs} if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs): diff --git a/BEdit/src/bedit/data/libraries/example.json b/BEdit/src/bedit/data/libraries/default.json similarity index 66% rename from BEdit/src/bedit/data/libraries/example.json rename to BEdit/src/bedit/data/libraries/default.json index 2b788b4..bcf84c9 100644 --- a/BEdit/src/bedit/data/libraries/example.json +++ b/BEdit/src/bedit/data/libraries/default.json @@ -2,12 +2,12 @@ "format": "bedit-document", "version": 1, "metadata": { - "name": "Example" + "name": "Untitled" }, "roots": [ { - "id": "bf714517-e7b2-4f4b-8b53-55642e3beb28", - "name": "A", + "id": "7ad651ef-6f76-4064-81d3-3fd6dd4d2918", + "name": "gain", "position": { "x": 0.0, "y": 0.0 @@ -16,15 +16,15 @@ "interface": { "inputs": [ { - "id": "port-5c5d4695", - "name": "Port 1", + "id": "port-df34ce84", + "name": "in", "position": { "x": 0.0, "y": 0.0 }, "properties": { "iconPosition": { - "x": 32.0, + "x": 64.0, "y": 64.0 } }, @@ -33,16 +33,16 @@ ], "outputs": [ { - "id": "port-de109124", - "name": "Port 2", + "id": "port-fe9e6486", + "name": "out", "position": { "x": 0.0, "y": 0.0 }, "properties": { "iconPosition": { - "x": 96.0, - "y": 64.0 + "x": 88.0, + "y": 40.0 } }, "type": "signal" @@ -77,31 +77,40 @@ "y": 40.0, "width": 48.0, "height": 48.0, - "text": "A", - "color": "#303030", - "fontSize": 12.0, + "text": "K", + "color": "#00007f", + "fontSize": 24.0, "lineStyle": "solid", "lineWidth": 1.5, - "stroke": "#303030", + "stroke": "#00007f", "fill": "#ffffff" } ] }, - "properties": {}, + "properties": { + "showName": false + }, "library": { "showSubtree": true }, "implementation": { "kind": "text", "source": { - "equations": [], - "parameters": {} + "equations": "out = k*in;", + "parameters": [ + { + "id": "parameter-61a43a86", + "name": "k", + "type": "real", + "value": "1" + } + ] } } }, { - "id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f", - "name": "B", + "id": "12b21b11-8828-4a90-a561-19491dc9632e", + "name": "differentiate", "position": { "x": 0.0, "y": 0.0 @@ -110,31 +119,16 @@ "interface": { "inputs": [ { - "id": "port-4f732b3e", - "name": "Port 1", + "id": "port-1cbabc8f", + "name": "in", "position": { - "x": -176.0, - "y": -144.0 + "x": 0.0, + "y": 0.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 + "x": 64.0, + "y": 64.0 } }, "type": "signal" @@ -142,16 +136,16 @@ ], "outputs": [ { - "id": "port-b68679b2", - "name": "Port 3", + "id": "port-4eeea4e7", + "name": "out", "position": { - "x": 128.0, - "y": -80.0 + "x": 0.0, + "y": 0.0 }, "properties": { "iconPosition": { - "x": 96.0, - "y": 64.0 + "x": 88.0, + "y": 40.0 } }, "type": "signal" @@ -162,7 +156,7 @@ "shape": "rectangle", "fill": "#f4f4f4", "border": "#303030", - "text": "Graph", + "text": "Text", "size": { "width": 128.0, "height": 128.0 @@ -186,25 +180,34 @@ "y": 40.0, "width": 48.0, "height": 48.0, - "text": "B", - "color": "#303030", - "fontSize": 12.0, + "text": "d/dt", + "color": "#00007f", + "fontSize": 18.0, "lineStyle": "solid", "lineWidth": 1.5, - "stroke": "#303030", + "stroke": "#00007f", "fill": "#ffffff" } ] }, - "properties": {}, + "properties": { + "showName": false + }, "library": { "showSubtree": true }, "implementation": { - "kind": "graph", - "graph": { - "blocks": [], - "connections": [] + "kind": "text", + "source": { + "equations": "initial out = initial;\nout = der(in);", + "parameters": [ + { + "id": "parameter-331e38be", + "name": "initial", + "type": "real", + "value": "0" + } + ] } } } diff --git a/BEdit/src/bedit/gui/controllers/commands.py b/BEdit/src/bedit/gui/controllers/commands.py index 82b4a76..eda5285 100644 --- a/BEdit/src/bedit/gui/controllers/commands.py +++ b/BEdit/src/bedit/gui/controllers/commands.py @@ -322,3 +322,14 @@ class EditTextDefinitionCommand(QUndoCommand): def undo(self) -> None: self.controller._set_text_definition(self.component_id, self.old) + + def id(self) -> int: + return 1001 + + def mergeWith(self, other: QUndoCommand) -> bool: # noqa: N802 + if not isinstance(other, EditTextDefinitionCommand): + return False + if other.component_id != self.component_id: + return False + self.new = other.new + return True diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index 777149b..87a11c2 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -31,6 +31,7 @@ from bedit.core.model import ( Endpoint, GraphDocument, Icon, + Parameter, Port, clone_component, ) @@ -47,6 +48,7 @@ class DocumentController(QObject): componentMoved = Signal(str, QPointF) componentRotated = Signal(str, float) componentPropertiesChanged = Signal(str) + textDefinitionChanged = Signal(str) connectionAdded = Signal(str) connectionRemoved = Signal(str) graphItemChanged = Signal(str, str) @@ -153,12 +155,13 @@ class DocumentController(QObject): if self.document is None: raise ValueError("Open or create a document first") number = len(self.document.roots) + 1 + base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block " component = Component( id=str(uuid4()), - name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}", + name=self._available_component_name(base_name, self.document.roots.values(), number), implementation_kind=kind, icon=Icon(text="Graph" if kind == "graph" else "Text"), - source={"equations": [], "parameters": {}} if kind == "text" else {}, + source={"equations": "", "parameters": []} if kind == "text" else {}, ) self.undo_stack.push(AddComponentCommand(self, None, component)) self.activate_component(component.id) @@ -171,12 +174,13 @@ class DocumentController(QObject): if owner is None or owner.implementation_kind != "graph": raise ValueError("Children can only be added to graph components") number = len(owner.graph.blocks) + 1 + base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block " component = Component( id=str(uuid4()), - name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}", + name=self._available_component_name(base_name, owner.graph.blocks.values(), number), implementation_kind=kind, icon=Icon(text="Graph" if kind == "graph" else "Text"), - source={"equations": [], "parameters": {}} if kind == "text" else {}, + source={"equations": "", "parameters": []} if kind == "text" else {}, ) self.undo_stack.push(AddComponentCommand(self, owner_id, component)) return component.id @@ -211,6 +215,9 @@ class DocumentController(QObject): if self.active_component is None or self.active_component.implementation_kind != "graph": raise ValueError("Open a graph component before placing components") component = clone_component(source) + component.name = self._available_component_name( + source.name, self.active_component.graph.blocks.values(), 0 + ) component.x, component.y = position.x(), position.y() self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component)) return component.id @@ -470,7 +477,8 @@ class DocumentController(QObject): self, inputs: list[Port], outputs: list[Port], - source: dict, + equations: str, + parameters: list[Parameter], ) -> None: component = self.active_component if component is None or component.implementation_kind != "text": @@ -479,6 +487,13 @@ class DocumentController(QObject): output_ids = [port.id for port in outputs] if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids): raise ValueError("Input and output IDs must be unique") + if any(not port.name.strip() for port in (*inputs, *outputs)): + raise ValueError("Every port must have a name") + parameter_ids = [parameter.id for parameter in parameters] + if len(set(parameter_ids)) != len(parameter_ids): + raise ValueError("Parameter IDs must be unique") + if any(not parameter.name.strip() for parameter in parameters): + raise ValueError("Every parameter must have a name") if self.document is not None: parent = self.document.find_parent(component.id) if parent is not None: @@ -505,7 +520,10 @@ class DocumentController(QObject): new = { "inputs": [port.to_dict() for port in inputs], "outputs": [port.to_dict() for port in outputs], - "source": deepcopy(source), + "source": { + "equations": equations, + "parameters": [parameter.to_dict() for parameter in parameters], + }, } if old != new: self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new)) @@ -525,6 +543,9 @@ class DocumentController(QObject): component = self.document.find_component(component_id) if component is None: return + siblings = self._component_siblings(component_id) + if any(item.id != component_id and item.name == name for item in siblings): + raise ValueError(f"A component named {name!r} already exists at this level") old = { "name": component.name, "icon": component.icon.to_dict(), @@ -726,10 +747,13 @@ class DocumentController(QObject): pairs = [(source, clone_component(source)) for source in source_components] id_map = {source.id: clone.id for source, clone in pairs} blocks = {} - for _source, clone in pairs: + used = list(owner.graph.blocks.values()) + for source, clone in pairs: + clone.name = self._available_component_name(source.name, used, 0) clone.x += offset.x() clone.y += offset.y() blocks[clone.id] = clone + used.append(clone) connections = {} for source in source_connections: if source.source.block not in id_map or source.target.block not in id_map: @@ -759,6 +783,26 @@ class DocumentController(QObject): raise ValueError("The containing component is no longer in the document") return owner.graph + @staticmethod + def _available_component_name( + base: str, components, start: int = 0 + ) -> str: + used = {component.name for component in components} + number = start + while f"{base}{number}" in used: + number += 1 + return f"{base}{number}" + + def _component_siblings(self, component_id: str): + if self.document is None: + return () + parent = self.document.find_parent(component_id) + return ( + parent.graph.blocks.values() + if parent is not None + else self.document.roots.values() + ) + def _insert_component(self, owner_id: str | None, component: Component) -> None: if self.document is None: raise ValueError("There is no open document") @@ -1039,5 +1083,4 @@ class DocumentController(QObject): component.source = deepcopy(values["source"]) self.interfaceChanged.emit() self.documentReset.emit() - if component_id == self.active_component_id: - self.activeGraphChanged.emit() + self.textDefinitionChanged.emit(component_id) diff --git a/BEdit/src/bedit/gui/editors/__init__.py b/BEdit/src/bedit/gui/editors/__init__.py new file mode 100644 index 0000000..c0cb643 --- /dev/null +++ b/BEdit/src/bedit/gui/editors/__init__.py @@ -0,0 +1 @@ +"""Reusable editing widgets.""" diff --git a/BEdit/src/bedit/gui/editors/text_definition.py b/BEdit/src/bedit/gui/editors/text_definition.py new file mode 100644 index 0000000..df2d0cc --- /dev/null +++ b/BEdit/src/bedit/gui/editors/text_definition.py @@ -0,0 +1,193 @@ +from copy import deepcopy +from uuid import uuid4 + +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import QComboBox, QHeaderView, QTableWidgetItem, QWidget + +from bedit.core.model import Parameter, Port +from bedit.core.port_types import PortTypeRegistry +from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor + + +ID_ROLE = Qt.ItemDataRole.UserRole +PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1 + + +class TextDefinitionEditor(QWidget): + """Editor for a text component's equations, ports, and parameters.""" + + modifiedChanged = Signal(bool) + definitionEdited = Signal() + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.ui = Ui_TextDefinitionEditor() + self.ui.setupUi(self) + self._modified = False + self._loading = False + self.ui.portsTable.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + self.ui.parametersTable.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Stretch + ) + self.ui.columnSplitter.setSizes([560, 340]) + self.ui.definitionSplitter.setSizes([300, 300]) + self.ui.equationsEdit.textChanged.connect(self._mark_modified) + self.ui.portsTable.cellChanged.connect(self._mark_modified) + self.ui.parametersTable.cellChanged.connect(self._mark_modified) + self.ui.addPortButton.clicked.connect(self.add_port) + self.ui.removePortButton.clicked.connect(self.remove_port) + self.ui.addParameterButton.clicked.connect(self.add_parameter) + self.ui.removeParameterButton.clicked.connect(self.remove_parameter) + self.ui.portsTable.itemSelectionChanged.connect(self._update_buttons) + self.ui.parametersTable.itemSelectionChanged.connect(self._update_buttons) + self._update_buttons() + + @property + def is_modified(self) -> bool: + return self._modified + + @property + def equations(self) -> str: + return self.ui.equationsEdit.toPlainText() + + @property + def ports(self) -> tuple[list[Port], list[Port]]: + inputs: list[Port] = [] + outputs: list[Port] = [] + for row in range(self.ui.portsTable.rowCount()): + name_item = self.ui.portsTable.item(row, 0) + type_combo = self.ui.portsTable.cellWidget(row, 1) + orientation_combo = self.ui.portsTable.cellWidget(row, 2) + port = Port( + id=name_item.data(ID_ROLE), + name=name_item.text().strip(), + type=type_combo.currentData(), + properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}), + ) + target = inputs if orientation_combo.currentData() == "input" else outputs + target.append(port) + return inputs, outputs + + @property + def parameters(self) -> list[Parameter]: + table = self.ui.parametersTable + return [ + Parameter( + id=table.item(row, 0).data(ID_ROLE), + name=table.item(row, 0).text().strip(), + type=table.item(row, 1).text().strip(), + value=table.item(row, 2).text(), + ) + for row in range(table.rowCount()) + ] + + def set_definition( + self, + equations: str, + inputs: list[Port], + outputs: list[Port], + parameters: list[Parameter], + ) -> None: + self._loading = True + self.ui.equationsEdit.setPlainText(equations) + self.ui.portsTable.setRowCount(0) + for port in inputs: + self._append_port(port, "input") + for port in outputs: + self._append_port(port, "output") + self.ui.parametersTable.setRowCount(0) + for parameter in parameters: + self._append_parameter(parameter) + self._loading = False + self.set_modified(False) + self._update_buttons() + + def set_modified(self, modified: bool) -> None: + if self._modified != modified: + self._modified = modified + self.modifiedChanged.emit(modified) + + def _mark_modified(self, *_args) -> None: + if not self._loading: + self.set_modified(True) + self.definitionEdited.emit() + + def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox: + combo = QComboBox(self) + for label, value in values: + combo.addItem(label, value) + combo.setCurrentIndex(max(0, combo.findData(current))) + combo.currentIndexChanged.connect(self._mark_modified) + return combo + + def _append_port(self, port: Port, orientation: str) -> None: + table = self.ui.portsTable + row = table.rowCount() + table.insertRow(row) + name = QTableWidgetItem(port.name) + name.setData(ID_ROLE, port.id) + name.setData(PROPERTIES_ROLE, deepcopy(port.properties)) + table.setItem(row, 0, name) + types = [(item.display_name, item.id) for item in PortTypeRegistry.all()] + table.setCellWidget(row, 1, self._new_combo(types, port.type)) + table.setCellWidget( + row, + 2, + self._new_combo([("Input", "input"), ("Output", "output")], orientation), + ) + + def _append_parameter(self, parameter: Parameter) -> None: + table = self.ui.parametersTable + row = table.rowCount() + table.insertRow(row) + name = QTableWidgetItem(parameter.name) + name.setData(ID_ROLE, parameter.id) + table.setItem(row, 0, name) + table.setItem(row, 1, QTableWidgetItem(parameter.type)) + table.setItem(row, 2, QTableWidgetItem(parameter.value)) + + def add_port(self) -> None: + port = Port( + id=f"port-{uuid4().hex[:8]}", + name=f"Port {self.ui.portsTable.rowCount() + 1}", + type="signal", + properties={"iconPosition": {"x": 0.0, "y": 0.0}}, + ) + self._loading = True + self._append_port(port, "input") + self._loading = False + self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1) + self._mark_modified() + + def remove_port(self) -> None: + row = self.ui.portsTable.currentRow() + if row >= 0: + self.ui.portsTable.removeRow(row) + self._mark_modified() + self._update_buttons() + + def add_parameter(self) -> None: + parameter = Parameter( + id=f"parameter-{uuid4().hex[:8]}", + name=f"Parameter {self.ui.parametersTable.rowCount() + 1}", + ) + self._loading = True + self._append_parameter(parameter) + self._loading = False + self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1) + self._mark_modified() + + def remove_parameter(self) -> None: + row = self.ui.parametersTable.currentRow() + if row >= 0: + self.ui.parametersTable.removeRow(row) + self._mark_modified() + self._update_buttons() + + def _update_buttons(self) -> None: + self.ui.removePortButton.setEnabled(self.ui.portsTable.currentRow() >= 0) + self.ui.removeParameterButton.setEnabled( + self.ui.parametersTable.currentRow() >= 0 + ) diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 10c11df..24f45c0 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -18,10 +18,11 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, QTransform) from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout, QHeaderView, QLabel, QMainWindow, QMenu, - QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy, - QSpacerItem, QSplitter, QStackedWidget, QToolBar, - QToolButton, QTreeView, QVBoxLayout, QWidget) + QMenuBar, QSizePolicy, QSpacerItem, QSplitter, + QStackedWidget, QToolBar, QToolButton, QTreeView, + QVBoxLayout, QWidget) +from bedit.gui.editors.text_definition import TextDefinitionEditor from bedit.gui.graphics.workspace import GraphWorkspaceView from . import resources_rc @@ -60,6 +61,8 @@ class Ui_MainWindow(object): icon5 = QIcon() icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.actionOpen.setIcon(icon5) + self.actionReloadLibraries = QAction(MainWindow) + self.actionReloadLibraries.setObjectName(u"actionReloadLibraries") self.actionSave = QAction(MainWindow) self.actionSave.setObjectName(u"actionSave") icon6 = QIcon() @@ -217,12 +220,6 @@ class Ui_MainWindow(object): self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer) - self.applyJsonButton = QPushButton(self.workspaceHeader) - self.applyJsonButton.setObjectName(u"applyJsonButton") - self.applyJsonButton.setVisible(False) - - self.workspaceHeaderLayout.addWidget(self.applyJsonButton) - self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton.setObjectName(u"pointerToolButton") icon15 = QIcon() @@ -291,18 +288,17 @@ class Ui_MainWindow(object): self.graphPageLayout.addWidget(self.graphView) self.workspaceStack.addWidget(self.graphPage) - self.jsonPage = QWidget() - self.jsonPage.setObjectName(u"jsonPage") - self.jsonPageLayout = QVBoxLayout(self.jsonPage) - self.jsonPageLayout.setObjectName(u"jsonPageLayout") - self.jsonPageLayout.setContentsMargins(0, 0, 0, 0) - self.jsonEditor = QPlainTextEdit(self.jsonPage) - self.jsonEditor.setObjectName(u"jsonEditor") - self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + self.textPage = QWidget() + self.textPage.setObjectName(u"textPage") + self.textPageLayout = QVBoxLayout(self.textPage) + self.textPageLayout.setObjectName(u"textPageLayout") + self.textPageLayout.setContentsMargins(0, 0, 0, 0) + self.textDefinitionEditor = TextDefinitionEditor(self.textPage) + self.textDefinitionEditor.setObjectName(u"textDefinitionEditor") - self.jsonPageLayout.addWidget(self.jsonEditor) + self.textPageLayout.addWidget(self.textDefinitionEditor) - self.workspaceStack.addWidget(self.jsonPage) + self.workspaceStack.addWidget(self.textPage) self.emptyPage = QWidget() self.emptyPage.setObjectName(u"emptyPage") self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;") @@ -364,6 +360,7 @@ class Ui_MainWindow(object): self.menubar.addAction(self.menuHelp.menuAction()) self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionOpen) + self.menuFile.addAction(self.actionReloadLibraries) self.menuFile.addSeparator() self.menuFile.addAction(self.actionSave) self.menuFile.addAction(self.actionSaveAs) @@ -439,6 +436,13 @@ class Ui_MainWindow(object): #endif // QT_CONFIG(statustip) #if QT_CONFIG(shortcut) self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) +#endif // QT_CONFIG(shortcut) + self.actionReloadLibraries.setText(QCoreApplication.translate("MainWindow", u"Reload &Libraries", None)) +#if QT_CONFIG(statustip) + self.actionReloadLibraries.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload configured library files from disk", None)) +#endif // QT_CONFIG(statustip) +#if QT_CONFIG(shortcut) + self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None)) #endif // QT_CONFIG(shortcut) self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None)) #if QT_CONFIG(statustip) @@ -505,7 +509,6 @@ class Ui_MainWindow(object): self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None)) self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None)) self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None)) - self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None)) self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None)) self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None)) #if QT_CONFIG(tooltip) @@ -524,7 +527,6 @@ class Ui_MainWindow(object): 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.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None)) self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None)) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None)) self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None)) diff --git a/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py new file mode 100644 index 0000000..2feed68 --- /dev/null +++ b/BEdit/src/bedit/gui/generated/ui_text_definition_editor.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'text_definition_editor.ui' +## +## Created by: Qt User Interface Compiler version 6.11.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout, + QHeaderView, QPlainTextEdit, QPushButton, QSizePolicy, + QSpacerItem, QSplitter, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) + +class Ui_TextDefinitionEditor(object): + def setupUi(self, TextDefinitionEditor): + if not TextDefinitionEditor.objectName(): + TextDefinitionEditor.setObjectName(u"TextDefinitionEditor") + TextDefinitionEditor.resize(900, 600) + self.editorLayout = QHBoxLayout(TextDefinitionEditor) + self.editorLayout.setObjectName(u"editorLayout") + self.editorLayout.setContentsMargins(6, 6, 6, 6) + self.columnSplitter = QSplitter(TextDefinitionEditor) + self.columnSplitter.setObjectName(u"columnSplitter") + self.columnSplitter.setOrientation(Qt.Orientation.Horizontal) + self.columnSplitter.setChildrenCollapsible(False) + self.equationsGroup = QGroupBox(self.columnSplitter) + self.equationsGroup.setObjectName(u"equationsGroup") + self.equationsLayout = QVBoxLayout(self.equationsGroup) + self.equationsLayout.setObjectName(u"equationsLayout") + self.equationsEdit = QPlainTextEdit(self.equationsGroup) + self.equationsEdit.setObjectName(u"equationsEdit") + self.equationsEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + + self.equationsLayout.addWidget(self.equationsEdit) + + self.columnSplitter.addWidget(self.equationsGroup) + self.definitionSplitter = QSplitter(self.columnSplitter) + self.definitionSplitter.setObjectName(u"definitionSplitter") + self.definitionSplitter.setOrientation(Qt.Orientation.Vertical) + self.definitionSplitter.setChildrenCollapsible(False) + self.portsGroup = QGroupBox(self.definitionSplitter) + self.portsGroup.setObjectName(u"portsGroup") + self.portsLayout = QVBoxLayout(self.portsGroup) + self.portsLayout.setObjectName(u"portsLayout") + self.portsTable = QTableWidget(self.portsGroup) + if (self.portsTable.columnCount() < 3): + self.portsTable.setColumnCount(3) + __qtablewidgetitem = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem) + __qtablewidgetitem1 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1) + __qtablewidgetitem2 = QTableWidgetItem() + self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2) + self.portsTable.setObjectName(u"portsTable") + self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.portsTable.setColumnCount(3) + + self.portsLayout.addWidget(self.portsTable) + + self.portButtonsLayout = QHBoxLayout() + self.portButtonsLayout.setObjectName(u"portButtonsLayout") + self.addPortButton = QPushButton(self.portsGroup) + self.addPortButton.setObjectName(u"addPortButton") + + self.portButtonsLayout.addWidget(self.addPortButton) + + self.removePortButton = QPushButton(self.portsGroup) + self.removePortButton.setObjectName(u"removePortButton") + + self.portButtonsLayout.addWidget(self.removePortButton) + + self.portButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.portButtonsLayout.addItem(self.portButtonSpacer) + + + self.portsLayout.addLayout(self.portButtonsLayout) + + self.definitionSplitter.addWidget(self.portsGroup) + self.parametersGroup = QGroupBox(self.definitionSplitter) + self.parametersGroup.setObjectName(u"parametersGroup") + self.parametersLayout = QVBoxLayout(self.parametersGroup) + self.parametersLayout.setObjectName(u"parametersLayout") + self.parametersTable = QTableWidget(self.parametersGroup) + if (self.parametersTable.columnCount() < 3): + self.parametersTable.setColumnCount(3) + __qtablewidgetitem3 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem3) + __qtablewidgetitem4 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem4) + __qtablewidgetitem5 = QTableWidgetItem() + self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem5) + self.parametersTable.setObjectName(u"parametersTable") + self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.parametersTable.setColumnCount(3) + + self.parametersLayout.addWidget(self.parametersTable) + + self.parameterButtonsLayout = QHBoxLayout() + self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout") + self.addParameterButton = QPushButton(self.parametersGroup) + self.addParameterButton.setObjectName(u"addParameterButton") + + self.parameterButtonsLayout.addWidget(self.addParameterButton) + + self.removeParameterButton = QPushButton(self.parametersGroup) + self.removeParameterButton.setObjectName(u"removeParameterButton") + + self.parameterButtonsLayout.addWidget(self.removeParameterButton) + + self.parameterButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.parameterButtonsLayout.addItem(self.parameterButtonSpacer) + + + self.parametersLayout.addLayout(self.parameterButtonsLayout) + + self.definitionSplitter.addWidget(self.parametersGroup) + self.columnSplitter.addWidget(self.definitionSplitter) + + self.editorLayout.addWidget(self.columnSplitter) + + + self.retranslateUi(TextDefinitionEditor) + + QMetaObject.connectSlotsByName(TextDefinitionEditor) + # setupUi + + def retranslateUi(self, TextDefinitionEditor): + self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None)) + self.equationsEdit.setPlaceholderText(QCoreApplication.translate("TextDefinitionEditor", u"Enter equations here\u2026", None)) + self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None)) + ___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0) + ___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None)) + ___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1) + ___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None)) + ___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2) + ___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None)) + self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None)) + self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None)) + self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None)) + ___qtablewidgetitem3 = self.parametersTable.horizontalHeaderItem(0) + ___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None)) + ___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(1) + ___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None)) + ___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(2) + ___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None)) + self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None)) + self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None)) + pass + # retranslateUi + diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index c3dd287..1e35e36 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -1,4 +1,3 @@ -import json from copy import deepcopy from pathlib import Path @@ -13,7 +12,7 @@ from PySide6.QtWidgets import ( QTabWidget, ) -from bedit.core.model import Component, Port +from bedit.core.model import Component, Parameter from bedit.core.serializer import JsonDocumentSerializer from bedit.gui.controllers.document import DocumentController from bedit.gui.dialogs.component_options import ComponentOptionsDialog @@ -40,6 +39,7 @@ class MainWindow(QMainWindow): self.ui = Ui_MainWindow() self.ui.setupUi(self) self.settings = application_settings() + self._applying_text_definition = False self.libraries = LibraryRepository(self) self.document_controller = DocumentController(self) @@ -108,12 +108,18 @@ class MainWindow(QMainWindow): self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line")) self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text")) self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected) - self.ui.applyJsonButton.clicked.connect(self.apply_json) + self.ui.textDefinitionEditor.definitionEdited.connect( + self.apply_text_definition + ) self.document_controller.activeGraphChanged.connect(self._active_graph_changed) + self.document_controller.textDefinitionChanged.connect( + self._text_definition_changed + ) def _connect_actions(self) -> None: self.ui.actionNew.triggered.connect(self.new_document) self.ui.actionOpen.triggered.connect(self.open_document) + self.ui.actionReloadLibraries.triggered.connect(self.reload_libraries) self.ui.actionSave.triggered.connect(self.save_document) self.ui.actionSaveAs.triggered.connect(self.save_document_as) self.ui.actionClose.triggered.connect(self.close_document) @@ -189,7 +195,6 @@ class MainWindow(QMainWindow): self.ui.navigateUpButton.setEnabled(False) self.ui.workspaceModeLabel.setText("") self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage) - self.ui.applyJsonButton.setVisible(False) self._set_graph_controls_visible(False) self._update_edit_actions() return @@ -199,13 +204,14 @@ class MainWindow(QMainWindow): ) is_graph = component.implementation_kind == "graph" 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.applyJsonButton.setVisible(not is_graph) + self.ui.workspaceStack.setCurrentWidget( + self.ui.graphPage if is_graph else self.ui.textPage + ) self._set_graph_controls_visible(is_graph) if is_graph: self.set_graph_tool("pointer") else: - self._load_source_json() + self._load_text_definition() self._update_edit_actions() def _update_edit_actions(self) -> None: @@ -259,62 +265,70 @@ class MainWindow(QMainWindow): "text": self.ui.textToolButton, }[mode].setChecked(True) - def _load_source_json(self) -> None: + def _load_text_definition(self) -> None: component = self.document_controller.active_component if component is None: return - text = json.dumps( - { - "inputs": [port.to_dict() for port in component.inputs], - "outputs": [port.to_dict() for port in component.outputs], - "source": component.source, - }, - indent=2, + self.ui.textDefinitionEditor.set_definition( + component.source.get("equations", ""), + component.inputs, + component.outputs, + [ + Parameter.from_dict(parameter) + for parameter in component.source.get("parameters", []) + ], ) - self.ui.jsonEditor.setPlainText(text) - self.ui.jsonEditor.document().setModified(False) def _resolve_source_edits(self) -> bool: component = self.document_controller.active_component if ( component is None or component.implementation_kind != "text" - or not self.ui.jsonEditor.document().isModified() + or not self.ui.textDefinitionEditor.is_modified ): return True answer = QMessageBox.question( self, "Apply text component changes?", - "The text component has unapplied input, output, or source changes.", + "The text component has unapplied equation, port, or parameter changes.", QMessageBox.StandardButton.Apply | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, ) if answer == QMessageBox.StandardButton.Apply: - return self.apply_json() + return self.apply_text_definition() return answer == QMessageBox.StandardButton.Discard @Slot() - def apply_json(self) -> bool: + def apply_text_definition(self) -> bool: try: - data = json.loads(self.ui.jsonEditor.toPlainText()) - if not isinstance(data, dict): - raise ValueError("The text component JSON must be an object") - if not isinstance(data.get("inputs"), list): - raise ValueError("'inputs' must be a list") - if not isinstance(data.get("outputs"), list): - raise ValueError("'outputs' must be a list") - if not isinstance(data.get("source"), dict): - raise ValueError("'source' must be an object") - inputs = [Port.from_dict(item) for item in data["inputs"]] - outputs = [Port.from_dict(item) for item in data["outputs"]] - self.document_controller.replace_active_text_definition(inputs, outputs, data["source"]) - except (TypeError, ValueError, json.JSONDecodeError) as error: - QMessageBox.critical(self, "Invalid text component JSON", str(error)) + inputs, outputs = self.ui.textDefinitionEditor.ports + self._applying_text_definition = True + try: + self.document_controller.replace_active_text_definition( + inputs, + outputs, + self.ui.textDefinitionEditor.equations, + self.ui.textDefinitionEditor.parameters, + ) + finally: + self._applying_text_definition = False + except (TypeError, ValueError) as error: + QMessageBox.critical(self, "Invalid text component", str(error)) return False - self._load_source_json() + self.ui.textDefinitionEditor.set_modified(False) return True + @Slot(str) + def _text_definition_changed(self, component_id: str) -> None: + component = self.document_controller.active_component + if ( + component is not None + and component.id == component_id + and not self._applying_text_definition + ): + self._load_text_definition() + def _maybe_save(self) -> bool: if self.document_controller.document is None: return True @@ -436,9 +450,9 @@ class MainWindow(QMainWindow): ports_action = menu.addAction("Port Options…") delete_action = menu.addAction("Delete") selected = menu.exec(tree_view.viewport().mapToGlobal(position)) - if selected is graph_action: + if graph_action is not None and selected is graph_action: self.document_controller.add_child(component_id, "graph") - elif selected is text_action: + elif text_action is not None and selected is text_action: self.document_controller.add_child(component_id, "text") elif selected is options_action: self.show_component_options(component_id) @@ -521,15 +535,18 @@ class MainWindow(QMainWindow): return dialog = ComponentOptionsDialog(component, self) if dialog.exec() == dialog.DialogCode.Accepted: - self.document_controller.edit_component_appearance( - component_id, - dialog.ui.nameEdit.text().strip(), - dialog.edited_icon, - dialog.edited_inputs, - dialog.edited_outputs, - dialog.ui.showSubtreeCheckBox.isChecked(), - dialog.ui.showNameCheckBox.isChecked(), - ) + try: + self.document_controller.edit_component_appearance( + component_id, + dialog.ui.nameEdit.text().strip(), + dialog.edited_icon, + dialog.edited_inputs, + dialog.edited_outputs, + dialog.ui.showSubtreeCheckBox.isChecked(), + dialog.ui.showNameCheckBox.isChecked(), + ) + except ValueError as error: + QMessageBox.warning(self, "Cannot rename component", str(error)) @Slot(str, str) def show_port_options(self, port_id: str, direction: str) -> None: diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index 28b604f..3abb5a1 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -264,16 +264,6 @@ - - - - false - - - Apply JSON - - - @@ -397,8 +387,8 @@ - - + + 0 @@ -412,14 +402,7 @@ 0 - - - QPlainTextEdit::LineWrapMode::NoWrap - - - Component JSON - - + @@ -466,6 +449,7 @@ + @@ -667,6 +651,17 @@ Ctrl+O + + + Reload &Libraries + + + Reload configured library files from disk + + + F5 + + @@ -806,6 +801,12 @@ + + TextDefinitionEditor + QWidget +
bedit.gui.editors.text_definition
+ 1 +
GraphWorkspaceView QGraphicsView diff --git a/BEdit/ui/text_definition_editor.ui b/BEdit/ui/text_definition_editor.ui new file mode 100644 index 0000000..20f0cb5 --- /dev/null +++ b/BEdit/ui/text_definition_editor.ui @@ -0,0 +1,45 @@ + + + TextDefinitionEditor + + 00900600 + + 6 + 6 + 6 + 6 + + + Qt::Orientation::Horizontal + false + + Equations + + QPlainTextEdit::LineWrapMode::NoWrapEnter equations here… + + + + Qt::Orientation::Vertical + false + + Ports + + QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection3NameTypeOrientation + Add PortRemove PortQt::Orientation::Horizontal4020 + + + + Parameters + + QAbstractItemView::SelectionBehavior::SelectRowsQAbstractItemView::SelectionMode::SingleSelection3NameTypeValue + Add ParameterRemove ParameterQt::Orientation::Horizontal4020 + + + + + + + + + + diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json index de3fae5..1fe0870 100644 --- a/BEdit/untitled.bedit.json +++ b/BEdit/untitled.bedit.json @@ -6,7 +6,7 @@ }, "roots": [ { - "id": "fd375563-d8cb-4c7b-b686-cd6f1edbb654", + "id": "b53c8186-0926-46b1-80ec-51a22ac5e0c6", "name": "New Graph Block 1", "position": { "x": 0.0, @@ -58,242 +58,8 @@ "implementation": { "kind": "graph", "graph": { - "blocks": [ - { - "id": "ffca7e20-ca0f-4872-985a-715dcd985052", - "name": "test A", - "position": { - "x": -224.0, - "y": -96.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": { - "showName": true, - "nameLabelPosition": { - "x": 48.0, - "y": 104.0 - } - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "text", - "source": { - "equations": [], - "parameters": {} - } - } - }, - { - "id": "571fd107-61ad-496a-8270-014e5697fcd1", - "name": "test B", - "position": { - "x": 96.0, - "y": -160.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": { - "showName": true - }, - "library": { - "showSubtree": true - }, - "implementation": { - "kind": "graph", - "graph": { - "blocks": [], - "connections": [], - "annotations": [] - } - } - } - ], - "connections": [ - { - "id": "df5ecb33-5451-4295-b68d-20886d86f0f4", - "source": { - "block": "ffca7e20-ca0f-4872-985a-715dcd985052", - "port": "port-de109124" - }, - "target": { - "block": "571fd107-61ad-496a-8270-014e5697fcd1", - "port": "port-4f732b3e" - }, - "name": "conn", - "properties": { - "waypoints": [], - "showName": true, - "nameLabelPosition": { - "x": -8.0, - "y": -56.0 - } - } - } - ], + "blocks": [], + "connections": [], "annotations": [] } }