Compare commits

..

3 Commits

11 changed files with 916 additions and 40 deletions

View File

@@ -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)

View File

@@ -0,0 +1,19 @@
from PySide6.QtGui import QUndoCommand
from bedit_core.models import Component
class RenameComponentCommand(QUndoCommand):
def __init__(self, document, component: Component, new_name: str) -> None:
super().__init__("Rename component")
self.document = document
self.component = component
self.old_name = component.name
self.new_name = new_name
def redo(self) -> None:
self.component.name = self.new_name
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
self.component.name = self.old_name
self.document.model_changed.emit(self.document.model)

View File

@@ -0,0 +1,18 @@
from PySide6.QtGui import QUndoCommand
class RenameDocumentCommand(QUndoCommand):
def __init__(self, document, new_name: str) -> None:
super().__init__("Rename document")
self.document = document
self.old_name = document.model.name
self.new_name = new_name
def redo(self) -> None:
self.document.model.name = self.new_name
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
self.document.model.name = self.old_name
self.document.model_changed.emit(self.document.model)

View File

@@ -1,27 +1,57 @@
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,
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)
self.model.rename_document_requested.connect(self.document.rename)
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)
@@ -32,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())

View File

@@ -5,8 +5,11 @@ 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 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
@@ -78,3 +81,37 @@ class Document(QObject):
name="Untitled",
root={},
)
def rename(self, name: str) -> None:
self.undo_stack.push(RenameDocumentCommand(self, name))
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()

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PortEditor</class>
<widget class="QWidget" name="PortEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>541</width>
<height>420</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QVBoxLayout" name="leftColumn">
<item>
<widget class="QListView" name="portList"/>
</item>
<item>
<layout class="QHBoxLayout" name="buttonRow">
<item>
<widget class="QPushButton" name="addPort">
<property name="text">
<string>Add Port</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removePort">
<property name="text">
<string>Remove Port</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="rightColumn">
<item>
<layout class="QFormLayout" name="basicForm">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nameEdit"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="typeRow">
<item>
<widget class="QRadioButton" name="typeSignal">
<property name="text">
<string>Signal</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="typeBond">
<property name="text">
<string>Power Bond</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="orientationLabel">
<property name="text">
<string>Orientation:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="orientationRow">
<item>
<widget class="QRadioButton" name="inputOrientation">
<property name="text">
<string>Input</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="outputOrientation">
<property name="text">
<string>Output</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="sizeLabel">
<property name="text">
<string>Size</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="sizeRow">
<item>
<widget class="QSpinBox" name="widthSize">
<property name="suffix">
<string> rows</string>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="heightSize">
<property name="suffix">
<string> columns</string>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="domainLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="multiplicityCheckBox">
<property name="text">
<string>Allow multiple connections</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="signalOptions">
<item row="0" column="0">
<widget class="QLabel" name="signalTypeLabel">
<property name="text">
<string>Signal Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="signalTypeComboBox"/>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="bondOptions">
<item row="0" column="0">
<widget class="QLabel" name="domainLabel_2">
<property name="text">
<string>Domain:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="domainComboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="causalityLabel">
<property name="text">
<string>Causality:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="causalityComboBox"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="descriptionForm">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPlainTextEdit" name="descriptionEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>60</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -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()

View File

@@ -2,9 +2,10 @@ from __future__ import annotations
from dataclasses import dataclass
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt
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
@@ -16,6 +17,9 @@ class DocumentTreeNode:
class DocumentTreeModel(QAbstractItemModel):
rename_document_requested = Signal(str)
rename_component_requested = Signal(Component, str)
def __init__(self) -> None:
super().__init__()
self._document: CoreDocument | None = None
@@ -33,18 +37,9 @@ 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)
):
if column != 0 or row < 0 or row >= len(parent_node.children):
return QModelIndex()
return self.createIndex(row, column, parent_node.children[row])
@@ -66,22 +61,38 @@ 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
node = index.internalPointer()
if (
role == Qt.ItemDataRole.DisplayRole
and isinstance(node, DocumentTreeNode)
):
if not isinstance(node, DocumentTreeNode):
return None
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:
if role != Qt.ItemDataRole.EditRole or not index.isValid():
return False
node = index.internalPointer()
if not isinstance(node, DocumentTreeNode) or not isinstance(node.value, (CoreDocument, Component)):
return False
name = str(value).strip()
if not name or name == node.name:
return False
if isinstance(node.value, CoreDocument):
self.rename_document_requested.emit(name)
elif isinstance(node.value, Component):
self.rename_component_requested.emit(node.value, name)
return True
def _node(self, index: QModelIndex | None) -> DocumentTreeNode:
if index is None or not index.isValid():
return self._root
@@ -89,6 +100,26 @@ 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)
if not index.isValid():
return flags
node = index.internalPointer()
# Make the document root node editable
if isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)):
flags |= Qt.ItemFlag.ItemIsEditable
return flags
def _build_tree(self, document: CoreDocument) -> DocumentTreeNode:
# QT's invisible root
root = DocumentTreeNode(
@@ -98,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_id, 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

View File

@@ -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)

View File

@@ -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

View File

@@ -2,10 +2,10 @@
"file_format_version": 1,
"format_version": 1,
"id": "3b6780c7-488b-471e-a784-392db7632090",
"name": "Current Document",
"name": "main_node",
"root": {
"50e6ef97-f686-4400-bc01-e5a352e8cc22": {
"name": "test",
"name": "some_bondgraph",
"interface": {
"ports": {}
},
@@ -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;"
]
}
},