diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py index 7f46ea2..0af36db 100644 --- a/src/bedit_gui/controllers/document_tree_controller.py +++ b/src/bedit_gui/controllers/document_tree_controller.py @@ -2,7 +2,8 @@ from collections.abc import Callable from functools import partial from typing import Protocol -from PySide6.QtCore import QObject, QPoint, QSize, Qt +from PySide6.QtCore import QEvent, QObject, QPoint, QSize, Qt +from PySide6.QtGui import QMouseEvent from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu from bedit_core.models import Component, ComponentID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID @@ -79,15 +80,20 @@ class DocumentTreeController(QObject): window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) window.ui.documentTree.setColumnWidth(1, 56) - window.ui.documentTree.setContextMenuPolicy( - Qt.ContextMenuPolicy.CustomContextMenu - ) - window.ui.documentTree.customContextMenuRequested.connect( - self._show_context_menu - ) + self._tree_viewport = window.ui.documentTree.viewport() + self._tree_viewport.installEventFilter(self) self._on_document_changed(document.model) + def eventFilter(self, watched: QObject, event: QEvent) -> bool: + if watched is self._tree_viewport and event.type() in (QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease): + assert isinstance(event, QMouseEvent) + if event.button() == Qt.MouseButton.RightButton: + if event.type() == QEvent.Type.MouseButtonRelease: + self._show_context_menu(event.position().toPoint()) + return True + return super().eventFilter(watched, event) + def _on_document_changed(self, model: CoreDocument) -> None: """Rebuild the tree whenever New/Open replaces the core document.""" self._show_equation_component(None) diff --git a/src/bedit_gui/models.py b/src/bedit_gui/models.py index 2e4e610..eac6e22 100644 --- a/src/bedit_gui/models.py +++ b/src/bedit_gui/models.py @@ -1,4 +1,3 @@ -"""GUI document data stored inside the core document metadata field.""" from __future__ import annotations from collections.abc import Mapping @@ -12,6 +11,9 @@ from bedit_core.models import ComponentID, ID, PortID class ShapeID(ID): pass +class SimulationID(ID): + pass + @dataclass class Shape: layer: int @@ -32,7 +34,6 @@ class Shape: def to_data(self) -> dict[str, Any]: return {"layer": self.layer, "type": self.type, "pos": list(self.pos)} - class LineType(Enum): NONE = "none" SOLID = "solid" @@ -40,7 +41,6 @@ class LineType(Enum): DOTTED = "dotted" DASH_DOT = "dash_dot" - @dataclass class Line(Shape): type: str = field(init=False, default="line") @@ -58,7 +58,6 @@ class Line(Shape): def to_data(self) -> dict[str, Any]: return {**super().to_data(), "end": list(self.end), "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color} - @dataclass class Rectangle(Shape): type: str = field(init=False, default="rectangle") @@ -111,7 +110,6 @@ class Icon: def to_data(self) -> dict[str, Any]: return {"shapes": {str(key): shape.to_data() for key, shape in self.shapes.items()}, "port_positions": {str(key): list(position) for key, position in self.port_positions.items()}} - @dataclass class IconDatabase: format_version: int = 1 @@ -124,3 +122,61 @@ class IconDatabase: def to_data(self) -> dict[str, Any]: return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}} + +class SimulationMethod(Enum): + DASSL = "dassl" + +@dataclass +class Simulation: + component: ComponentID + name: str + + start_time: float + duration: float + use_timed_steps: bool + number_of_steps: int + step_size: float + + method: SimulationMethod + + dassl_tolerance: float + + @classmethod + def from_data(cls, data: Mapping[str, Any]) -> Simulation: + return cls( + component=SimulationID(data.get("component"), SimulationID()), + name=data.get("name", ""), + start_time=float(data.get("start_time", "0.0")), + duration=float(data.get("duration", "1.0")), + use_timed_steps=bool(data.get("use_timed_steps", "false")), + number_of_steps=int(data.get("number_of_steps", "500")), + step_size=float(data.get("step_size", "0.001")), + method=SimulationMethod(data.get("method", "dassl")), + dassl_tolerance=float(data.get("dassl_tolerance", "1e-6")), + ) + def to_data(self) -> dict[str, Any]: + return { + "component": str(self.component), + "name": self.name, + "start_time": str(self.start_time), + "duration": str(self.duration), + "use_timed_steps": str(self.use_timed_steps), + "number_of_steps": str(self.number_of_steps), + "step_size": str(self.step_size), + "method": str(self.method), + "dassl_tolerance": str(self.dassl_tolerance), + } + +@dataclass +class SimulationDatabase: + format_versio: int = 1 + simulations: dict[SimulationID, Simulation] = field(default_factory=dict) + + @classmethod + def from_data(cls, data: Mapping[str, Any]) -> SimulationDatabase: + sims = {SimulationID(key): Simulation.from_data(value) for key, value in data.get("simulations", {}).items()} + return cls(format_version=int(data.get("format_version", 1)), simulations=sims) + + def to_data(self) -> dict[str, Any]: + return {"format_version": self.format_version, "simulations": {str(key): sim.to_data() for key, sim in self.simulations.items()}} + \ No newline at end of file diff --git a/src/bedit_gui/ui/forms/main_window.ui b/src/bedit_gui/ui/forms/main_window.ui index 6c1a0fb..533333d 100644 --- a/src/bedit_gui/ui/forms/main_window.ui +++ b/src/bedit_gui/ui/forms/main_window.ui @@ -74,9 +74,20 @@ + + + Simulation + + + + + + + + @@ -153,6 +164,21 @@ + + + toolBar + + + TopToolBarArea + + + false + + + + + + @@ -386,6 +412,54 @@ QAction::MenuRole::NoRole + + + + :/icons/icons/configure.png:/icons/icons/configure.png + + + Simulation Settings + + + QAction::MenuRole::NoRole + + + + + + :/icons/icons/view-form-table.png:/icons/icons/view-form-table.png + + + Edit Parameters + + + QAction::MenuRole::NoRole + + + + + + :/icons/icons/run-build.png:/icons/icons/run-build.png + + + Compile Model + + + QAction::MenuRole::NoRole + + + + + + :/icons/icons/office-chart-line.png:/icons/icons/office-chart-line.png + + + Open Simulation Window + + + QAction::MenuRole::NoRole + + diff --git a/src/bedit_gui/ui/forms/simulation_settings.ui b/src/bedit_gui/ui/forms/simulation_settings.ui new file mode 100644 index 0000000..f460840 --- /dev/null +++ b/src/bedit_gui/ui/forms/simulation_settings.ui @@ -0,0 +1,236 @@ + + + Dialog + + + + 0 + 0 + 562 + 451 + + + + Dialog + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + + + Name: + + + + + + + Component: + + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + Start time: + + + + + + + Simulation length: + + + + + + + s + + + + + + + s + + + + + + + + + Step size + + + + + + + Number of steps + + + + + + + + + Step size: + + + + + + + + + + + + + + s + + + 4 + + + 0.001000000000000 + + + + + + + + + + Number of steps: + + + + + + + + + + Simulation method: + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + 1e-06 + + + + + + + Tolerance: + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +