diff --git a/src/bedit_gui/commands/port_commands.py b/src/bedit_gui/commands/port_commands.py
new file mode 100644
index 0000000..b57fb0c
--- /dev/null
+++ b/src/bedit_gui/commands/port_commands.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+from copy import deepcopy
+
+from PySide6.QtGui import QUndoCommand
+
+from bedit_core.models import Component, Port, PortID
+
+
+class AddPortCommand(QUndoCommand):
+ def __init__(
+ self,
+ document: object,
+ component: Component,
+ port_id: PortID,
+ port: Port,
+ ) -> None:
+ super().__init__("Add port")
+ self.document = document
+ self.component = component
+ self.port_id = port_id
+ self.port = deepcopy(port)
+
+ def redo(self) -> None:
+ self.component.interface.ports[self.port_id] = deepcopy(self.port)
+ self.document.model_changed.emit(self.document.model)
+
+ def undo(self) -> None:
+ del self.component.interface.ports[self.port_id]
+ self.document.model_changed.emit(self.document.model)
+
+
+class RemovePortCommand(QUndoCommand):
+ def __init__(
+ self,
+ document: object,
+ component: Component,
+ port_id: PortID,
+ ) -> None:
+ super().__init__("Remove port")
+ self.document = document
+ self.component = component
+ self.port_id = port_id
+ self.port = deepcopy(component.interface.ports[port_id])
+
+ def redo(self) -> None:
+ del self.component.interface.ports[self.port_id]
+ self.document.model_changed.emit(self.document.model)
+
+ def undo(self) -> None:
+ self.component.interface.ports[self.port_id] = deepcopy(self.port)
+ self.document.model_changed.emit(self.document.model)
+
+
+class ChangePortCommand(QUndoCommand):
+ def __init__(
+ self,
+ document: object,
+ component: Component,
+ port_id: PortID,
+ port: Port,
+ ) -> None:
+ super().__init__("Change port")
+ self.document = document
+ self.component = component
+ self.port_id = port_id
+ self.old_port = deepcopy(component.interface.ports[port_id])
+ self.new_port = deepcopy(port)
+
+ def redo(self) -> None:
+ self.component.interface.ports[self.port_id] = deepcopy(self.new_port)
+ self.document.model_changed.emit(self.document.model)
+
+ def undo(self) -> None:
+ self.component.interface.ports[self.port_id] = deepcopy(self.old_port)
+ self.document.model_changed.emit(self.document.model)
diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py
index 157b2f7..2073b5a 100644
--- a/src/bedit_gui/controllers/document_tree_controller.py
+++ b/src/bedit_gui/controllers/document_tree_controller.py
@@ -1,18 +1,44 @@
-from PySide6.QtCore import QObject
+from collections.abc import Callable
+from typing import Protocol
+from PySide6.QtCore import QObject, QPoint, Qt
+from PySide6.QtWidgets import QDialog, QMenu
+
+from bedit_core.models import Component, Port, PortID
from bedit_core.models import Document as CoreDocument
from bedit_gui.documents import Document
+from bedit_gui.views.dialogs.interface_editor_dialog import (
+ InterfaceEditorDialog,
+)
from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
+class InterfaceEditorLike(Protocol):
+ def exec(self) -> int: ...
+
+ def ports(self) -> dict[PortID, Port]: ...
+
+
+InterfaceEditorFactory = Callable[
+ [dict[PortID, Port], MainWindow],
+ InterfaceEditorLike,
+]
+
+
class DocumentTreeController(QObject):
- def __init__(self,document: Document,window: MainWindow) -> None:
+ def __init__(
+ self,
+ document: Document,
+ window: MainWindow,
+ interface_editor_factory: InterfaceEditorFactory = InterfaceEditorDialog,
+ ) -> None:
super().__init__(window)
self.document = document
self.window = window
self.model = DocumentTreeModel()
+ self.interface_editor_factory = interface_editor_factory
window.ui.documentTree.setModel(self.model)
document.model_changed.connect(self._on_document_changed)
@@ -20,6 +46,12 @@ class DocumentTreeController(QObject):
self.model.rename_component_requested.connect(self.document.rename_component)
window.ui.documentTree.setHeaderHidden(True)
+ window.ui.documentTree.setContextMenuPolicy(
+ Qt.ContextMenuPolicy.CustomContextMenu
+ )
+ window.ui.documentTree.customContextMenuRequested.connect(
+ self._show_context_menu
+ )
self._on_document_changed(document.model)
@@ -30,3 +62,25 @@ class DocumentTreeController(QObject):
# Optional presentation behavior. Later, you could instead remember
# expanded component IDs and restore only those nodes.
self.window.ui.documentTree.expandAll()
+
+ def _show_context_menu(self, position: QPoint) -> None:
+ index = self.window.ui.documentTree.indexAt(position)
+ component = self.model.value(index)
+ if not isinstance(component, Component):
+ return
+
+ menu = QMenu(self.window.ui.documentTree)
+ edit_interface = menu.addAction("Edit Interface")
+ selected = menu.exec(
+ self.window.ui.documentTree.viewport().mapToGlobal(position)
+ )
+ if selected is edit_interface:
+ self._edit_interface(component)
+
+ def _edit_interface(self, component: Component) -> None:
+ dialog = self.interface_editor_factory(
+ component.interface.ports,
+ self.window,
+ )
+ if dialog.exec() == QDialog.DialogCode.Accepted:
+ self.document.update_component_ports(component, dialog.ports())
diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py
index f1060be..49a8011 100644
--- a/src/bedit_gui/documents/document.py
+++ b/src/bedit_gui/documents/document.py
@@ -5,11 +5,12 @@ from pathlib import Path
from PySide6.QtCore import QObject, Signal
from PySide6.QtGui import QUndoStack
-from bedit_core.models import ID
-from bedit_core.models import Document as CoreDocument, Component
-from bedit_gui.services import document_files
-from bedit_gui.commands.rename_document_command import RenameDocumentCommand
+from bedit_core.models import ID, Component, Port, PortID
+from bedit_core.models import Document as CoreDocument
+from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
from bedit_gui.commands.rename_component_command import RenameComponentCommand
+from bedit_gui.commands.rename_document_command import RenameDocumentCommand
+from bedit_gui.services import document_files
class Document(QObject):
@@ -84,5 +85,33 @@ class Document(QObject):
def rename(self, name: str) -> None:
self.undo_stack.push(RenameDocumentCommand(self, name))
- def rename_component(self, component: Component, name:str) -> None:
+ def rename_component(self, component: Component, name: str) -> None:
self.undo_stack.push(RenameComponentCommand(self, component, name))
+
+ def update_component_ports(
+ self,
+ component: Component,
+ ports: dict[PortID, Port],
+ ) -> None:
+ current = component.interface.ports
+ removed = [
+ RemovePortCommand(self, component, port_id)
+ for port_id in current.keys() - ports.keys()
+ ]
+ added = [
+ AddPortCommand(self, component, port_id, ports[port_id])
+ for port_id in ports.keys() - current.keys()
+ ]
+ changed = [
+ ChangePortCommand(self, component, port_id, ports[port_id])
+ for port_id in current.keys() & ports.keys()
+ if current[port_id] != ports[port_id]
+ ]
+ commands = [*removed, *added, *changed]
+ if not commands:
+ return
+
+ self.undo_stack.beginMacro("Edit interface")
+ for command in commands:
+ self.undo_stack.push(command)
+ self.undo_stack.endMacro()
diff --git a/src/bedit_gui/ui/forms/port_editor_widget.ui b/src/bedit_gui/ui/forms/port_editor_widget.ui
index e7677be..869d71f 100644
--- a/src/bedit_gui/ui/forms/port_editor_widget.ui
+++ b/src/bedit_gui/ui/forms/port_editor_widget.ui
@@ -7,7 +7,7 @@
0
0
541
- 593
+ 420
@@ -183,8 +183,31 @@
-
+ -
+
+
+ Causality:
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
@@ -194,6 +217,9 @@
-
+
+ QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow
+
-
@@ -204,7 +230,7 @@
-
-
+
0
0
@@ -215,6 +241,12 @@
20
+
+
+ 16777215
+ 60
+
+
diff --git a/src/bedit_gui/views/dialogs/interface_editor_dialog.py b/src/bedit_gui/views/dialogs/interface_editor_dialog.py
new file mode 100644
index 0000000..2877f10
--- /dev/null
+++ b/src/bedit_gui/views/dialogs/interface_editor_dialog.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from PySide6.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QVBoxLayout,
+ QWidget,
+)
+
+from bedit_core.models import Port, PortID
+from bedit_gui.views.port_editor_widget import PortEditorWidget
+
+
+class InterfaceEditorDialog(QDialog):
+ """Modal wrapper around the reusable port editor widget."""
+
+ def __init__(
+ self,
+ ports: dict[PortID, Port],
+ parent: QWidget | None = None,
+ ) -> None:
+ super().__init__(parent)
+ self.setWindowTitle("Edit Interface")
+ self.resize(700, 500)
+
+ layout = QVBoxLayout(self)
+ self.editor = PortEditorWidget(self)
+ self.editor.set_ports(ports)
+ layout.addWidget(self.editor)
+
+ buttons = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
+ self,
+ )
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addWidget(buttons)
+
+ def ports(self) -> dict[PortID, Port]:
+ return self.editor.ports()
diff --git a/src/bedit_gui/views/models/document_tree_model.py b/src/bedit_gui/views/models/document_tree_model.py
index d9b75cd..2c54869 100644
--- a/src/bedit_gui/views/models/document_tree_model.py
+++ b/src/bedit_gui/views/models/document_tree_model.py
@@ -4,7 +4,8 @@ from dataclasses import dataclass
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
-from bedit_core.models import Document as CoreDocument, ComponentID, Component, GraphImplementation
+from bedit_core.models import Component, ComponentID, GraphImplementation
+from bedit_core.models import Document as CoreDocument
@dataclass
@@ -36,7 +37,7 @@ class DocumentTreeModel(QAbstractItemModel):
def columnCount(self, _parent: QModelIndex | None = None) -> int:
return 1
- def index(self,row: int,column: int,parent: QModelIndex | None = None) -> QModelIndex:
+ def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex:
parent_node = self._node(parent)
if column != 0 or row < 0 or row >= len(parent_node.children):
return QModelIndex()
@@ -60,7 +61,7 @@ class DocumentTreeModel(QAbstractItemModel):
row = grandparent.children.index(parent_node)
return self.createIndex(row, 0, parent_node)
- def data(self,index: QModelIndex,role: int = Qt.ItemDataRole.DisplayRole) -> object | None:
+ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> object | None:
if not index.isValid():
return None
@@ -70,10 +71,10 @@ class DocumentTreeModel(QAbstractItemModel):
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
return node.name
-
+
return None
- def setData(self,index: QModelIndex,value: object,role: int = Qt.ItemDataRole.EditRole) -> bool:
+ def setData(self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole) -> bool:
if role != Qt.ItemDataRole.EditRole or not index.isValid():
return False
@@ -99,6 +100,12 @@ class DocumentTreeModel(QAbstractItemModel):
node = index.internalPointer()
return node if isinstance(node, DocumentTreeNode) else self._root
+ def value(self, index: QModelIndex) -> object | None:
+ if not index.isValid():
+ return None
+ node = index.internalPointer()
+ return node.value if isinstance(node, DocumentTreeNode) else None
+
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
flags = super().flags(index)
@@ -108,8 +115,7 @@ class DocumentTreeModel(QAbstractItemModel):
node = index.internalPointer()
# Make the document root node editable
- if (isinstance(node, DocumentTreeNode)
- and (isinstance(node.value, CoreDocument) or isinstance(node.value, Component))):
+ if isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
flags |= Qt.ItemFlag.ItemIsEditable
return flags
@@ -123,26 +129,17 @@ class DocumentTreeModel(QAbstractItemModel):
children=[],
)
# Add itself as a child so the document root is visible in the tree
- document_root = DocumentTreeNode(
- name = document.name,
- value = document,
- parent = root,
- children = []
- )
+ document_root = DocumentTreeNode(name=document.name, value=document, parent=root, children=[])
root.children.append(document_root)
def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None:
- for component_id, component in components.items():
- component_node = DocumentTreeNode(
- name = component.name,
- value = component,
- parent = root,
- children = []
- )
+ for component in components.values():
+ component_node = DocumentTreeNode(name=component.name, value=component, parent=root, children=[])
root.children.append(component_node)
if isinstance(component.implementation, GraphImplementation):
_list_children(component_node, component.implementation.graph.components)
+
_list_children(document_root, document.root)
return root
diff --git a/src/bedit_gui/views/port_editor_widget.py b/src/bedit_gui/views/port_editor_widget.py
new file mode 100644
index 0000000..90a64d7
--- /dev/null
+++ b/src/bedit_gui/views/port_editor_widget.py
@@ -0,0 +1,222 @@
+from __future__ import annotations
+
+from copy import deepcopy
+
+from PySide6.QtCore import QItemSelection, Signal
+from PySide6.QtGui import QStandardItem, QStandardItemModel
+from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
+
+from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType
+from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor
+
+
+class PortEditorWidget(QWidget):
+ """Reusable editor for a detached draft of an interface's ports."""
+
+ ports_changed = Signal()
+
+ def __init__(self, parent: QWidget | None = None) -> None:
+ super().__init__(parent)
+
+ self.ui = Ui_PortEditor()
+ self.ui.setupUi(self)
+ self._ports: dict[PortID, Port] = {}
+ self._port_ids: list[PortID] = []
+ self._loading = False
+
+ self._type_group = QButtonGroup(self)
+ self._type_group.addButton(self.ui.typeSignal)
+ self._type_group.addButton(self.ui.typeBond)
+ self._orientation_group = QButtonGroup(self)
+ self._orientation_group.addButton(self.ui.inputOrientation)
+ self._orientation_group.addButton(self.ui.outputOrientation)
+
+ self.ui.signalTypeComboBox.addItem("Real", ValueType.REAL)
+ self.ui.signalTypeComboBox.addItem("Integer", ValueType.INT)
+ self.ui.signalTypeComboBox.addItem("Boolean", ValueType.BOOL)
+ self.ui.domainComboBox.setEditable(True)
+ for causality in PortCausality:
+ self.ui.causalityComboBox.addItem(causality.value.replace("_", " ").title(), causality)
+
+ self._list_model = QStandardItemModel(self)
+ self.ui.portList.setModel(self._list_model)
+ self.ui.portList.selectionModel().selectionChanged.connect(
+ self._selection_changed
+ )
+ self.ui.addPort.clicked.connect(self._add_port)
+ self.ui.removePort.clicked.connect(self._remove_port)
+
+ self.ui.nameEdit.textEdited.connect(self._form_changed)
+ self.ui.typeSignal.toggled.connect(self._form_changed)
+ self.ui.typeBond.toggled.connect(self._form_changed)
+ self.ui.inputOrientation.toggled.connect(self._form_changed)
+ self.ui.outputOrientation.toggled.connect(self._form_changed)
+ self.ui.widthSize.valueChanged.connect(self._form_changed)
+ self.ui.heightSize.valueChanged.connect(self._form_changed)
+ self.ui.multiplicityCheckBox.toggled.connect(self._form_changed)
+ self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed)
+ self.ui.domainComboBox.currentTextChanged.connect(self._form_changed)
+ self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed)
+ self.ui.descriptionEdit.textChanged.connect(self._form_changed)
+
+ self._set_editor_enabled(False)
+ self._update_option_visibility()
+
+ def set_ports(self, ports: dict[PortID, Port]) -> None:
+ self._ports = deepcopy(ports)
+ self._port_ids = list(self._ports)
+ self._rebuild_list()
+
+ def ports(self) -> dict[PortID, Port]:
+ return deepcopy(self._ports)
+
+ def _rebuild_list(self, selected_id: PortID | None = None) -> None:
+ self._list_model.clear()
+ for port_id in self._port_ids:
+ self._list_model.appendRow(QStandardItem(self._ports[port_id].name))
+
+ if self._port_ids:
+ if selected_id not in self._ports:
+ selected_id = self._port_ids[0]
+ row = self._port_ids.index(selected_id)
+ index = self._list_model.index(row, 0)
+ self.ui.portList.setCurrentIndex(index)
+ else:
+ self._set_editor_enabled(False)
+ self.ui.removePort.setEnabled(False)
+
+ def _selection_changed(self,_selected: QItemSelection,_deselected: QItemSelection) -> None:
+ port_id = self._selected_port_id()
+ self.ui.removePort.setEnabled(port_id is not None)
+ self._set_editor_enabled(port_id is not None)
+ if port_id is not None:
+ self._load_port(self._ports[port_id])
+
+ def _selected_port_id(self) -> PortID | None:
+ index = self.ui.portList.currentIndex()
+ if not index.isValid() or index.row() >= len(self._port_ids):
+ return None
+ return self._port_ids[index.row()]
+
+ def _load_port(self, port: Port) -> None:
+ self._loading = True
+ self.ui.nameEdit.setText(port.name)
+ self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT)
+ self.ui.outputOrientation.setChecked(port.direction is SignalDirection.OUTPUT)
+ self.ui.widthSize.setValue(port.matrix_size[0])
+ self.ui.heightSize.setValue(port.matrix_size[1])
+ self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
+ self.ui.descriptionEdit.setPlainText(port.description or "")
+
+ if isinstance(port, SignalPort):
+ self.ui.typeSignal.setChecked(True)
+ self.ui.signalTypeComboBox.setCurrentIndex(
+ self.ui.signalTypeComboBox.findData(port.value_type)
+ )
+ else:
+ self.ui.typeBond.setChecked(True)
+ assert isinstance(port, BondPort)
+ if self.ui.domainComboBox.findText(port.domain) < 0:
+ self.ui.domainComboBox.addItem(port.domain)
+ self.ui.domainComboBox.setCurrentText(port.domain)
+ self.ui.causalityComboBox.setCurrentIndex(self.ui.causalityComboBox.findData(port.causality_preference))
+
+ self._loading = False
+ self._update_option_visibility()
+
+ def _form_changed(self, *_args: object) -> None:
+ if self._loading:
+ return
+
+ self._update_option_visibility()
+ port_id = self._selected_port_id()
+ if port_id is None:
+ return
+
+ old_port = self._ports[port_id]
+ self._ports[port_id] = self._port_from_form(old_port)
+ self._list_model.item(self._port_ids.index(port_id)).setText(
+ self._ports[port_id].name
+ )
+ self.ports_changed.emit()
+
+ def _port_from_form(self, old_port: Port) -> Port:
+ common = {
+ "name": self.ui.nameEdit.text().strip(),
+ "direction": (
+ SignalDirection.INPUT
+ if self.ui.inputOrientation.isChecked()
+ else SignalDirection.OUTPUT
+ ),
+ "multiplicity": self.ui.multiplicityCheckBox.isChecked(),
+ "matrix_size": [
+ self.ui.widthSize.value(),
+ self.ui.heightSize.value(),
+ ],
+ "description": self.ui.descriptionEdit.toPlainText() or None,
+ }
+ if self.ui.typeSignal.isChecked():
+ return SignalPort(
+ **common,
+ value_type=self.ui.signalTypeComboBox.currentData(),
+ quantity=(
+ old_port.quantity if isinstance(old_port, SignalPort) else None
+ ),
+ unit=old_port.unit if isinstance(old_port, SignalPort) else None,
+ )
+ return BondPort(
+ **common,
+ domain=self.ui.domainComboBox.currentText(),
+ causality_preference=self.ui.causalityComboBox.currentData(),
+ )
+
+ def _add_port(self) -> None:
+ port_id = PortID()
+ number = len(self._ports) + 1
+ port = SignalPort(
+ name=f"Port {number}",
+ direction=SignalDirection.INPUT,
+ )
+ self._ports[port_id] = port
+ self._port_ids.append(port_id)
+ self._rebuild_list(port_id)
+ self.ports_changed.emit()
+
+ def _remove_port(self) -> None:
+ port_id = self._selected_port_id()
+ if port_id is None:
+ return
+ row = self._port_ids.index(port_id)
+ del self._ports[port_id]
+ self._port_ids.remove(port_id)
+ selected = (
+ self._port_ids[min(row, len(self._port_ids) - 1)]
+ if self._port_ids
+ else None
+ )
+ self._rebuild_list(selected)
+ self.ports_changed.emit()
+
+ def _update_option_visibility(self) -> None:
+ signal = self.ui.typeSignal.isChecked()
+ self._set_layout_visible(self.ui.signalOptions, signal)
+ self._set_layout_visible(self.ui.bondOptions, not signal)
+
+ @staticmethod
+ def _set_layout_visible(layout: QLayout, visible: bool) -> None:
+ for index in range(layout.count()):
+ widget = layout.itemAt(index).widget()
+ if widget is not None:
+ widget.setVisible(visible)
+
+ def _set_editor_enabled(self, enabled: bool) -> None:
+ for layout in (
+ self.ui.basicForm,
+ self.ui.signalOptions,
+ self.ui.bondOptions,
+ self.ui.descriptionForm,
+ ):
+ for index in range(layout.count()):
+ widget = layout.itemAt(index).widget()
+ if widget is not None:
+ widget.setEnabled(enabled)
diff --git a/tests/unit/test_port_editor.py b/tests/unit/test_port_editor.py
new file mode 100644
index 0000000..e56f6fa
--- /dev/null
+++ b/tests/unit/test_port_editor.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from copy import deepcopy
+
+import pytest
+from PySide6.QtWidgets import QApplication
+
+from bedit_core import models
+from bedit_core.models import (
+ BondPort,
+ Component,
+ Interface,
+ PortID,
+ SignalDirection,
+ SignalPort,
+ ValueType,
+)
+from bedit_gui.documents import Document
+from bedit_gui.views.port_editor_widget import PortEditorWidget
+
+
+@pytest.fixture(scope="module")
+def qt_app() -> QApplication:
+ return QApplication.instance() or QApplication([])
+
+
+@pytest.mark.unit
+def test_port_editor_uses_a_detached_draft(qt_app: QApplication) -> None:
+ port_id = PortID("input")
+ original = {
+ port_id: SignalPort(
+ name="Input",
+ direction=SignalDirection.INPUT,
+ )
+ }
+ editor = PortEditorWidget()
+ editor.set_ports(original)
+
+ editor.ui.nameEdit.setText("Changed")
+ editor.ui.nameEdit.textEdited.emit("Changed")
+
+ assert original[port_id].name == "Input"
+ assert editor.ports()[port_id].name == "Changed"
+
+
+@pytest.mark.unit
+def test_port_type_controls_option_visibility(qt_app: QApplication) -> None:
+ editor = PortEditorWidget()
+ editor.set_ports(
+ {
+ PortID("port"): SignalPort(
+ name="Port",
+ direction=SignalDirection.INPUT,
+ value_type=ValueType.REAL,
+ )
+ }
+ )
+
+ assert not editor.ui.signalTypeComboBox.isHidden()
+ assert editor.ui.domainComboBox.isHidden()
+
+ editor.ui.typeBond.setChecked(True)
+
+ assert editor.ui.signalTypeComboBox.isHidden()
+ assert not editor.ui.domainComboBox.isHidden()
+
+
+@pytest.mark.unit
+def test_bond_causality_is_loaded_and_changed(qt_app: QApplication) -> None:
+ port_id = PortID("bond")
+ editor = PortEditorWidget()
+ editor.set_ports({port_id: BondPort("Bond", SignalDirection.INPUT, causality_preference=models.PortCausality.PREFERRED_FLOW_OUT)})
+
+ assert editor.ui.causalityComboBox.currentData() is models.PortCausality.PREFERRED_FLOW_OUT
+
+ index = editor.ui.causalityComboBox.findData(models.PortCausality.FIXED_EFFORT_OUT)
+ editor.ui.causalityComboBox.setCurrentIndex(index)
+
+ assert editor.ports()[port_id].causality_preference is models.PortCausality.FIXED_EFFORT_OUT
+
+
+@pytest.mark.unit
+def test_port_changes_are_one_undoable_operation(
+ qt_app: QApplication,
+ minimal_document,
+) -> None:
+ component: Component = next(iter(minimal_document.root.values()))
+ removed_id = PortID("removed")
+ changed_id = PortID("changed")
+ added_id = PortID("added")
+ component.interface = Interface(
+ {
+ removed_id: SignalPort("Remove", SignalDirection.INPUT),
+ changed_id: SignalPort("Before", SignalDirection.INPUT),
+ }
+ )
+ original = deepcopy(component.interface.ports)
+ updated = {
+ changed_id: BondPort(
+ "After",
+ SignalDirection.OUTPUT,
+ domain="electrical",
+ ),
+ added_id: SignalPort("Added", SignalDirection.INPUT),
+ }
+ document = Document(qt_app)
+
+ document.update_component_ports(component, updated)
+ assert component.interface.ports == updated
+
+ document.undo_stack.undo()
+ assert component.interface.ports == original
+
+ document.undo_stack.redo()
+ assert component.interface.ports == updated
diff --git a/untitled.bedit.json b/untitled.bedit.json
index 5f313f1..618a859 100644
--- a/untitled.bedit.json
+++ b/untitled.bedit.json
@@ -125,6 +125,20 @@
"description": "",
"domain": "power",
"causality_preference": "indifferent"
+ },
+ "d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": {
+ "port_type": "signal",
+ "name": "power",
+ "direction": "output",
+ "multiplicity": false,
+ "matrix_size": [
+ 1,
+ 1
+ ],
+ "description": "Dissipated power",
+ "value_type": "real",
+ "quantity": null,
+ "unit": null
}
}
},
@@ -147,7 +161,8 @@
"declarations": [],
"initial_equations": [],
"equations": [
- "p.e = r*p.f;"
+ "p.e = r*p.f;",
+ "power = p.e*p.f;"
]
}
},