diff --git a/docs/gui_doc.md b/docs/gui_doc.md
index 20d3ddc..00dd110 100644
--- a/docs/gui_doc.md
+++ b/docs/gui_doc.md
@@ -237,17 +237,17 @@ Sometimes, the value or visibility of one parameter depends on the value of anot
Example (based on pid):
``` python
-def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool:
+def is_parameter_active(self, param_name: str, instance_params: Dict[str, Any]) -> bool:
if param_name == "Kp":
- return instance_values["controller"] in ["P", "PI", "PD", "PID"]
+ return instance_params["controller"] in ["P", "PI", "PD", "PID"]
elif param_name == "Ki":
- return instance_values["controller"] in ["I", "PI", "PID"]
+ return instance_params["controller"] in ["I", "PI", "PID"]
elif param_name == "Kd":
- return instance_values["controller"] in ["PD", "PID"]
- return super().is_parameter_active(param_name, instance_values)
+ return instance_params["controller"] in ["PD", "PID"]
+ return super().is_parameter_active(param_name, instance_params)
```
Explanation:
-- `instance_values` contains the current values of all parameters for this block instance.
+- `instance_params` contains the current values of all parameters for this block instance.
- The `controller` parameter determines which other parameters are active:
- if the controller type includes `P`, then `Kp` is active.
- if it includes `I`, then `Ki` is active.
diff --git a/docs/uml/pySimBlocks-GUI.uxf b/docs/uml/pySimBlocks-GUI.uxf
index 134e6d1..f4806da 100644
--- a/docs/uml/pySimBlocks-GUI.uxf
+++ b/docs/uml/pySimBlocks-GUI.uxf
@@ -168,7 +168,7 @@ description: str
doc_path: Path | None
--
get_param(param_name: str) -> ParameterMeta | None
-is_parameter_active(param_name: str, instance_values: Dict[str, Any]) -> bool
+is_parameter_active(param_name: str, instance_params: Dict[str, Any]) -> bool
resolve_port_group(PortMeta, direction: str, BlockInstance) -> list[PortInstance]
build_ports(BlockInstance) -> list[PortInstance)
diff --git a/pySimBlocks/gui/addons/sofa/sofa_service.py b/pySimBlocks/gui/addons/sofa/sofa_service.py
index 1488276..5e3c087 100644
--- a/pySimBlocks/gui/addons/sofa/sofa_service.py
+++ b/pySimBlocks/gui/addons/sofa/sofa_service.py
@@ -24,7 +24,7 @@
from PySide6.QtCore import QProcess, QProcessEnvironment
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
from pySimBlocks.gui.services.yaml_tools import save_yaml
from pySimBlocks.project.generate_sofa_controller import generate_sofa_controller
diff --git a/pySimBlocks/gui/blocks/block_dialog_session.py b/pySimBlocks/gui/blocks/block_dialog_session.py
new file mode 100644
index 0000000..f321cc9
--- /dev/null
+++ b/pySimBlocks/gui/blocks/block_dialog_session.py
@@ -0,0 +1,47 @@
+# ******************************************************************************
+# pySimBlocks
+# Copyright (c) 2026 Université de Lille & INRIA
+# ******************************************************************************
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see .
+# ******************************************************************************
+# Authors: see Authors.txt
+# ******************************************************************************
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from PySide6.QtWidgets import QLineEdit
+
+from pySimBlocks.gui.models.block_instance import BlockInstance
+
+if TYPE_CHECKING:
+ from pySimBlocks.gui.blocks.block_meta import BlockMeta
+
+
+class BlockDialogSession:
+ def __init__(
+ self,
+ meta: "BlockMeta",
+ instance: BlockInstance,
+ project_dir: Path | None = None,
+ ):
+ self.meta = meta
+ self.instance = instance
+ self.project_dir = project_dir
+
+ # --- STATE UI (par dialog) ---
+ self.local_params = dict(instance.parameters)
+ self.param_widgets = {}
+ self.param_labels = {}
+ self.name_edit: QLineEdit | None = None
diff --git a/pySimBlocks/gui/blocks/block_meta.py b/pySimBlocks/gui/blocks/block_meta.py
index f8c6973..38b48bb 100644
--- a/pySimBlocks/gui/blocks/block_meta.py
+++ b/pySimBlocks/gui/blocks/block_meta.py
@@ -18,13 +18,32 @@
# Authors: see Authors.txt
# ******************************************************************************
+import ast
+import os
from abc import ABC
from pathlib import Path
-from typing import Any, Dict, List, Literal
+from typing import Any, Dict, Literal, Sequence
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
+ QComboBox,
+ QFileDialog,
+ QFormLayout,
+ QFrame,
+ QHBoxLayout,
+ QLabel,
+ QLineEdit,
+ QPushButton,
+ QSizePolicy,
+ QTextBrowser,
+ QVBoxLayout,
+ QWidget,
+)
+
+from pySimBlocks.gui.blocks.block_dialog_session import BlockDialogSession
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class BlockMeta(ABC):
@@ -80,25 +99,42 @@ def __init__(self):
description: str
# ----------- Optional declarations -----------
-
doc_path: Path | None = None
- parameters: List[ParameterMeta] = []
- inputs: List[PortMeta] = []
- outputs: List[PortMeta] = []
+ parameters: Sequence[ParameterMeta] = ()
+ inputs: Sequence[PortMeta] = ()
+ outputs: Sequence[PortMeta] = ()
- def get_param(self, param_name: str) -> ParameterMeta | None:
- for param in self.parameters:
- if param.name == param_name:
- return param
- return None
+ # --------------------------------------------------------------------------
+ # Dialog session management
+ # --------------------------------------------------------------------------
+ def create_dialog_session(
+ self,
+ instance: BlockInstance,
+ project_dir: Path | None = None,
+ ) -> BlockDialogSession:
+ return BlockDialogSession(self, instance, project_dir)
- def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool:
+ # --------------------------------------------------------------------------
+ # Parameter resolution
+ # --------------------------------------------------------------------------
+ def is_parameter_active(self,
+ param_name: str,
+ instance_params: Dict[str, Any]) -> bool:
"""
Default: all parameters are always active.
Children override if needed.
"""
return True
-
+
+ # ------------------------------------------------------------
+ def gather_params(self, session: BlockDialogSession) -> dict[str, Any]:
+ # Keep full local state, including inactive params, so values are cached
+ # across visibility toggles and dialog reopen.
+ return session.local_params.copy()
+
+ # --------------------------------------------------------------------------
+ # Port resolution
+ # --------------------------------------------------------------------------
def resolve_port_group(self,
port_meta: PortMeta,
direction: Literal['input', 'output'],
@@ -110,6 +146,7 @@ def resolve_port_group(self,
"""
return [PortInstance(port_meta.name, port_meta.display_as, direction, instance)]
+ # ------------------------------------------------------------
def build_ports(self, instance: "BlockInstance") -> list["PortInstance"]:
ports = []
@@ -120,3 +157,248 @@ def build_ports(self, instance: "BlockInstance") -> list["PortInstance"]:
ports.extend(self.resolve_port_group(pmeta, "output", instance))
return ports
+
+
+ # --------------------------------------------------------------------------
+ # QT dialog display
+ # --------------------------------------------------------------------------
+ def build_description(self, form: QFormLayout):
+ """ Default description display. Children can override if needed. """
+ title = QLabel(f"{self.name}")
+ title.setAlignment(Qt.AlignmentFlag.AlignLeft)
+ form.addRow(title)
+
+ frame = QFrame()
+ frame.setFrameShape(QFrame.StyledPanel)
+ frame.setFrameShadow(QFrame.Raised)
+ frame.setLineWidth(1)
+
+ frame_layout = QVBoxLayout(frame)
+ frame_layout.setContentsMargins(8, 6, 8, 6)
+
+ desc = QTextBrowser()
+ desc.setMarkdown(self.description)
+ desc.setReadOnly(True)
+ desc.setFrameShape(QFrame.NoFrame)
+ desc.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
+ desc.document().setTextWidth(400)
+ desc.setFixedHeight(int(desc.document().size().height()) + 6)
+
+ frame_layout.addWidget(desc)
+ form.addRow(frame)
+
+
+ # ------------------------------------------------------
+ def build_pre_param(self,
+ session: BlockDialogSession,
+ form: QFormLayout,
+ readonly: bool = False):
+ """ Default: no pre-parameter widgets. Children override if needed. """
+ pass
+
+ # ------------------------------------------------------------
+ def build_param(self,
+ session: BlockDialogSession,
+ form: QFormLayout,
+ readonly: bool = False):
+ """ Default: no parameter widgets. Children override if needed. """
+
+
+ # --- Block name ---
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ # --- Parameters ---
+ for param_meta in self.parameters:
+ param_name = param_meta.name
+
+ label, widget = self._create_param_row(session, param_meta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+
+ session.param_widgets[param_name] = widget
+ session.param_labels[param_name] = label
+
+
+ # ------------------------------------------------------------
+ def build_post_param(self,
+ session: BlockDialogSession,
+ form: QFormLayout,
+ readonly: bool = False):
+ """ Default: no post-parameter widgets. Children override if needed. """
+ pass
+
+ # ------------------------------------------------------------
+ def build_file_param_row(
+ self,
+ session: BlockDialogSession,
+ form: QFormLayout,
+ pmeta: ParameterMeta,
+ readonly: bool = False,
+ file_filter: str = "Python files (*.py);;All files (*)",
+ ) -> None:
+ edit = self._create_edit_widget(session, pmeta, readonly)
+ if readonly:
+ self._set_readonly_style(edit)
+
+ browse_btn = QPushButton("...")
+ browse_btn.setToolTip("Select file from disk")
+ browse_btn.setEnabled(not readonly)
+ browse_btn.clicked.connect(
+ lambda: self._browse_and_set_relative_file(edit, session.project_dir, file_filter)
+ )
+
+ row_widget = QWidget()
+ row_layout = QHBoxLayout(row_widget)
+ row_layout.setContentsMargins(0, 0, 0, 0)
+ row_layout.addWidget(edit)
+ row_layout.addWidget(browse_btn)
+
+ label = QLabel(f"{pmeta.name}:")
+ if pmeta.description:
+ label.setToolTip(pmeta.description)
+
+ form.addRow(label, row_widget)
+ session.param_widgets[pmeta.name] = row_widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------------
+ def refresh_form(self, session: BlockDialogSession):
+ """
+ Refresh the parameter widgets visibility based on
+ BlockMeta.is_parameter_active and current local_params.
+ """
+
+ for param_name, widget in session.param_widgets.items():
+ label = session.param_labels[param_name]
+
+ active = self.is_parameter_active(param_name, session.local_params)
+
+ widget.setVisible(active)
+ label.setVisible(active)
+
+
+ # --------------------------------------------------------------------------
+ # Private methods
+ # --------------------------------------------------------------------------
+ def _create_param_row(self,
+ session: BlockDialogSession,
+ pmeta: ParameterMeta,
+ readonly: bool = False
+ ) -> tuple[QLabel, QWidget]:
+
+ # ENUM
+ if pmeta.type == "enum":
+ widget = self._create_enum_widget(session, pmeta, readonly)
+
+ # Default: text edit
+ widget = self._create_edit_widget(session, pmeta, readonly)
+
+ label = QLabel(f"{pmeta.name}:")
+ if pmeta.description:
+ label.setToolTip(pmeta.description)
+
+ return label, widget
+
+
+ # ------------------------------------------------------------
+ def _create_edit_widget(self,
+ session: BlockDialogSession,
+ pmeta: ParameterMeta,
+ readonly: bool = False) -> QLineEdit:
+ edit = QLineEdit()
+ value = session.local_params.get(pmeta.name)
+ if value is not None:
+ edit.setText(str(value))
+ elif pmeta.default is not None:
+ edit.setText(str(pmeta.default))
+ edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, pmeta.name, session, readonly)
+ )
+ return edit
+
+ # ------------------------------------------------------------
+ def _create_enum_widget(self,
+ session: BlockDialogSession,
+ pmeta: ParameterMeta,
+ readonly: bool = False) -> QComboBox:
+ combo = QComboBox()
+ for v in pmeta.enum:
+ combo.addItem(str(v), userData=v)
+ value = session.local_params.get(pmeta.name)
+ if value is not None:
+ combo.setCurrentText(str(value))
+ combo.currentTextChanged.connect(
+ lambda val: self._on_param_changed(val, pmeta.name, session, readonly)
+ )
+ return combo
+
+ # ------------------------------------------------------------
+ def _browse_and_set_relative_file(
+ self,
+ edit: QLineEdit,
+ project_dir: Path | None,
+ file_filter: str,
+ ) -> None:
+ if project_dir is None:
+ return
+
+ base_dir = project_dir.expanduser()
+ start_dir = base_dir if base_dir.is_dir() else Path.cwd()
+
+ selected_file, _ = QFileDialog.getOpenFileName(
+ edit,
+ "Select file",
+ str(start_dir),
+ file_filter,
+ )
+ if not selected_file:
+ return
+
+ selected_path = Path(selected_file).resolve()
+ base_resolved = base_dir.resolve()
+ try:
+ relative_path = selected_path.relative_to(base_resolved)
+ except ValueError:
+ relative_path = Path(os.path.relpath(str(selected_path), str(base_resolved)))
+
+ edit.setText(str(relative_path))
+
+ # ------------------------------------------------------------
+ def _on_param_changed( self, val: str, name: str, session: BlockDialogSession, readonly: bool,):
+ if readonly:
+ return
+
+ if name == "name":
+ session.instance.name = val
+ else:
+ text = str(val).strip()
+ try:
+ session.local_params[name] = ast.literal_eval(text)
+ except (ValueError, SyntaxError):
+ session.local_params[name] = text
+ self.refresh_form(session)
+
+ # ------------------------------------------------------------
+ def _set_readonly_style(self, widget: QWidget):
+ if isinstance(widget, QLineEdit):
+ widget.setReadOnly(True)
+ widget.setStyleSheet("""
+ QLineEdit {
+ background-color: #2b2b2b;
+ color: #888888;
+ border: 1px solid #444444;
+ }
+ """)
+ elif isinstance(widget, QComboBox):
+ widget.setEnabled(False)
diff --git a/pySimBlocks/gui/blocks/controllers/pid.py b/pySimBlocks/gui/blocks/controllers/pid.py
index 017626f..30750f2 100644
--- a/pySimBlocks/gui/blocks/controllers/pid.py
+++ b/pySimBlocks/gui/blocks/controllers/pid.py
@@ -104,13 +104,13 @@ def __init__(self):
)
]
- def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool:
+ def is_parameter_active(self, param_name: str, instance_params: Dict[str, Any]) -> bool:
if param_name == "Kp":
- return instance_values["controller"] in ["P", "PI", "PD", "PID"]
+ return instance_params["controller"] in ["P", "PI", "PD", "PID"]
elif param_name == "Ki":
- return instance_values["controller"] in ["I", "PI", "PID"]
+ return instance_params["controller"] in ["I", "PI", "PID"]
elif param_name == "Kd":
- return instance_values["controller"] in ["PD", "PID"]
+ return instance_params["controller"] in ["PD", "PID"]
- return super().is_parameter_active(param_name, instance_values)
+ return super().is_parameter_active(param_name, instance_params)
diff --git a/pySimBlocks/gui/blocks/operators/algebraic_function.py b/pySimBlocks/gui/blocks/operators/algebraic_function.py
index 69742a1..62faea8 100644
--- a/pySimBlocks/gui/blocks/operators/algebraic_function.py
+++ b/pySimBlocks/gui/blocks/operators/algebraic_function.py
@@ -18,10 +18,17 @@
# Authors: see Authors.txt
# ******************************************************************************
+import os
+import subprocess
+import sys
+from pathlib import Path
from typing import Literal
+
+from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta, ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class AlgebraicFunctionMeta(BlockMeta):
@@ -88,6 +95,10 @@ def __init__(self):
)
]
+
+ # --------------------------------------------------------------------------
+ # Port resolution
+ # --------------------------------------------------------------------------
def resolve_port_group(
self,
port_meta: PortMeta,
@@ -127,3 +138,95 @@ def resolve_port_group(
return ports
return super().resolve_port_group(port_meta, direction, instance)
+
+ # --------------------------------------------------------------------------
+ # Dialog methods
+ # --------------------------------------------------------------------------
+ def build_param(
+ self,
+ session,
+ form: QFormLayout,
+ readonly: bool = False,
+ ):
+ # --- Block name ---
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ # --- Parameters ---
+ for pmeta in self.parameters:
+ if pmeta.name == "file_path":
+ self.build_file_param_row(
+ session,
+ form,
+ pmeta,
+ readonly=readonly,
+ file_filter="Python files (*.py);;All files (*)",
+ )
+ continue
+
+ label, widget = self._create_param_row(session, pmeta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+ session.param_widgets[pmeta.name] = widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
+ open_btn = QPushButton("Open file")
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
+ form.addRow(QLabel(""), open_btn)
+ session.open_file_btn = open_btn
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def refresh_form(self, session):
+ super().refresh_form(session)
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def _resolve_file_path(self, session) -> Path | None:
+ raw = session.local_params.get("file_path")
+ if not raw:
+ return None
+
+ path = Path(str(raw)).expanduser()
+ if not path.is_absolute() and session.project_dir is not None:
+ path = (session.project_dir / path).resolve()
+ return path
+
+ # ------------------------------------------------------
+ def _refresh_open_button_state(self, session) -> None:
+ btn = getattr(session, "open_file_btn", None)
+ if btn is None:
+ return
+
+ target = self._resolve_file_path(session)
+ exists = target is not None and target.is_file()
+ btn.setEnabled(exists)
+ if exists:
+ btn.setToolTip(str(target))
+ else:
+ btn.setToolTip("Set a valid existing file_path to open the file.")
+
+ # ------------------------------------------------------
+ def _open_file_from_session(self, session) -> None:
+ target = self._resolve_file_path(session)
+ if target is None or not target.is_file():
+ return
+
+ if sys.platform.startswith("darwin"):
+ subprocess.Popen(["open", str(target)])
+ elif os.name == "nt":
+ os.startfile(str(target))
+ else:
+ subprocess.Popen(["xdg-open", str(target)])
diff --git a/pySimBlocks/gui/blocks/operators/demux.py b/pySimBlocks/gui/blocks/operators/demux.py
index 42c652b..9690f67 100644
--- a/pySimBlocks/gui/blocks/operators/demux.py
+++ b/pySimBlocks/gui/blocks/operators/demux.py
@@ -23,7 +23,7 @@
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class DemuxMeta(BlockMeta):
diff --git a/pySimBlocks/gui/blocks/operators/mux.py b/pySimBlocks/gui/blocks/operators/mux.py
index 948583a..8dc7dc9 100644
--- a/pySimBlocks/gui/blocks/operators/mux.py
+++ b/pySimBlocks/gui/blocks/operators/mux.py
@@ -22,7 +22,7 @@
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class MuxMeta(BlockMeta):
diff --git a/pySimBlocks/gui/blocks/operators/product.py b/pySimBlocks/gui/blocks/operators/product.py
index 2326feb..90bff20 100644
--- a/pySimBlocks/gui/blocks/operators/product.py
+++ b/pySimBlocks/gui/blocks/operators/product.py
@@ -23,7 +23,7 @@
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class ProductMeta(BlockMeta):
diff --git a/pySimBlocks/gui/blocks/operators/sum.py b/pySimBlocks/gui/blocks/operators/sum.py
index bec848b..3095c85 100644
--- a/pySimBlocks/gui/blocks/operators/sum.py
+++ b/pySimBlocks/gui/blocks/operators/sum.py
@@ -22,7 +22,7 @@
from typing import Literal
from pySimBlocks.gui.blocks.block_meta import BlockMeta, ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class SumMeta(BlockMeta):
diff --git a/pySimBlocks/gui/blocks/sources/file_source.py b/pySimBlocks/gui/blocks/sources/file_source.py
index 35ca298..5e3a5e8 100644
--- a/pySimBlocks/gui/blocks/sources/file_source.py
+++ b/pySimBlocks/gui/blocks/sources/file_source.py
@@ -18,8 +18,14 @@
# Authors: see Authors.txt
# ******************************************************************************
+import os
+import subprocess
+import sys
+from pathlib import Path
from typing import Any, Dict
+from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
@@ -84,8 +90,11 @@ def __init__(self):
)
]
- def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool:
- file_path = str(instance_values.get("file_path", "") or "")
+ # ------------------------------------------------------
+ def is_parameter_active(self,
+ param_name: str,
+ instance_params: Dict[str, Any]) -> bool:
+ file_path = str(instance_params.get("file_path", "") or "")
ext = file_path.rsplit(".", 1)[-1].lower() if "." in file_path else ""
if param_name == "key":
@@ -94,4 +103,94 @@ def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any])
if param_name == "use_time":
return ext in {"npz", "csv"}
- return super().is_parameter_active(param_name, instance_values)
+ return super().is_parameter_active(param_name, instance_params)
+
+ # --------------------------------------------------------------------------
+ # Dialog Methods
+ # --------------------------------------------------------------------------
+ def build_param(
+ self,
+ session,
+ form: QFormLayout,
+ readonly: bool = False,
+ ):
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ for pmeta in self.parameters:
+ if pmeta.name == "file_path":
+ self.build_file_param_row(
+ session,
+ form,
+ pmeta,
+ readonly=readonly,
+ file_filter="Data files (*.npz *.npy *.csv);;All files (*)",
+ )
+ continue
+
+ label, widget = self._create_param_row(session, pmeta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+ session.param_widgets[pmeta.name] = widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
+ open_btn = QPushButton("Open file")
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
+ form.addRow(QLabel(""), open_btn)
+ session.open_file_btn = open_btn
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def refresh_form(self, session):
+ super().refresh_form(session)
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def _resolve_file_path(self, session) -> Path | None:
+ raw = session.local_params.get("file_path")
+ if not raw:
+ return None
+
+ path = Path(str(raw)).expanduser()
+ if not path.is_absolute() and session.project_dir is not None:
+ path = (session.project_dir / path).resolve()
+ return path
+
+ # ------------------------------------------------------
+ def _refresh_open_button_state(self, session) -> None:
+ btn = getattr(session, "open_file_btn", None)
+ if btn is None:
+ return
+
+ target = self._resolve_file_path(session)
+ exists = target is not None and target.is_file()
+ btn.setEnabled(exists)
+ if exists:
+ btn.setToolTip(str(target))
+ else:
+ btn.setToolTip("Set a valid existing file_path to open the file.")
+
+ # ------------------------------------------------------
+ def _open_file_from_session(self, session) -> None:
+ target = self._resolve_file_path(session)
+ if target is None or not target.is_file():
+ return
+
+ if sys.platform.startswith("darwin"):
+ subprocess.Popen(["open", str(target)])
+ elif os.name == "nt":
+ os.startfile(str(target))
+ else:
+ subprocess.Popen(["xdg-open", str(target)])
diff --git a/pySimBlocks/gui/blocks/systems/non_linear_state_space.py b/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
index 3735cb8..316fb91 100644
--- a/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
+++ b/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
@@ -18,10 +18,17 @@
# Authors: see Authors.txt
# ******************************************************************************
+import os
+import subprocess
+import sys
+from pathlib import Path
from typing import Literal
+
+from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta, ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class NonLinearStateSpaceMeta(BlockMeta):
@@ -100,6 +107,9 @@ def __init__(self):
)
]
+ # --------------------------------------------------------------------------
+ # Port resolution
+ # --------------------------------------------------------------------------
def resolve_port_group(
self,
port_meta: PortMeta,
@@ -131,3 +141,93 @@ def resolve_port_group(
) for key in output_keys
]
return super().resolve_port_group(port_meta, direction, instance)
+
+ # --------------------------------------------------------------------------
+ # Dialog methods
+ # --------------------------------------------------------------------------
+ def build_param(
+ self,
+ session,
+ form: QFormLayout,
+ readonly: bool = False,
+ ):
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ for pmeta in self.parameters:
+ if pmeta.name == "file_path":
+ self.build_file_param_row(
+ session,
+ form,
+ pmeta,
+ readonly=readonly,
+ file_filter="Python files (*.py);;All files (*)",
+ )
+ continue
+
+ label, widget = self._create_param_row(session, pmeta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+ session.param_widgets[pmeta.name] = widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
+ open_btn = QPushButton("Open file")
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
+ form.addRow(QLabel(""), open_btn)
+ session.open_file_btn = open_btn
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def refresh_form(self, session):
+ super().refresh_form(session)
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def _resolve_file_path(self, session) -> Path | None:
+ raw = session.local_params.get("file_path")
+ if not raw:
+ return None
+
+ path = Path(str(raw)).expanduser()
+ if not path.is_absolute() and session.project_dir is not None:
+ path = (session.project_dir / path).resolve()
+ return path
+
+ # ------------------------------------------------------
+ def _refresh_open_button_state(self, session) -> None:
+ btn = getattr(session, "open_file_btn", None)
+ if btn is None:
+ return
+
+ target = self._resolve_file_path(session)
+ exists = target is not None and target.is_file()
+ btn.setEnabled(exists)
+ if exists:
+ btn.setToolTip(str(target))
+ else:
+ btn.setToolTip("Set a valid existing file_path to open the file.")
+
+ # ------------------------------------------------------
+ def _open_file_from_session(self, session) -> None:
+ target = self._resolve_file_path(session)
+ if target is None or not target.is_file():
+ return
+
+ if sys.platform.startswith("darwin"):
+ subprocess.Popen(["open", str(target)])
+ elif os.name == "nt":
+ os.startfile(str(target))
+ else:
+ subprocess.Popen(["xdg-open", str(target)])
diff --git a/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py b/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
index abbc83b..7d78730 100644
--- a/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
+++ b/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
@@ -18,11 +18,18 @@
# Authors: see Authors.txt
# ******************************************************************************
+import os
+import subprocess
+import sys
+from pathlib import Path
from typing import Literal
+
+from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class SofaExchangeIOMeta(BlockMeta):
@@ -83,6 +90,9 @@ def __init__(self):
)
]
+ # --------------------------------------------------------------------------
+ # Port Resolution
+ # --------------------------------------------------------------------------
def resolve_port_group(
self,
port_meta: PortMeta,
@@ -119,3 +129,93 @@ def resolve_port_group(
]
return super().resolve_port_group(port_meta, direction, instance)
+
+ # --------------------------------------------------------------------------
+ # Dialog Methods
+ # --------------------------------------------------------------------------
+ def build_param(
+ self,
+ session,
+ form: QFormLayout,
+ readonly: bool = False,
+ ):
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ for pmeta in self.parameters:
+ if pmeta.name == "scene_file":
+ self.build_file_param_row(
+ session,
+ form,
+ pmeta,
+ readonly=readonly,
+ file_filter="SOFA scene files (*.py);;All files (*)",
+ )
+ continue
+
+ label, widget = self._create_param_row(session, pmeta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+ session.param_widgets[pmeta.name] = widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
+ open_btn = QPushButton("Open file")
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
+ form.addRow(QLabel(""), open_btn)
+ session.open_file_btn = open_btn
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def refresh_form(self, session):
+ super().refresh_form(session)
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def _resolve_file_path(self, session) -> Path | None:
+ raw = session.local_params.get("scene_file")
+ if not raw:
+ return None
+
+ path = Path(str(raw)).expanduser()
+ if not path.is_absolute() and session.project_dir is not None:
+ path = (session.project_dir / path).resolve()
+ return path
+
+ # ------------------------------------------------------
+ def _refresh_open_button_state(self, session) -> None:
+ btn = getattr(session, "open_file_btn", None)
+ if btn is None:
+ return
+
+ target = self._resolve_file_path(session)
+ exists = target is not None and target.is_file()
+ btn.setEnabled(exists)
+ if exists:
+ btn.setToolTip(str(target))
+ else:
+ btn.setToolTip("Set a valid existing scene_file to open the file.")
+
+ # ------------------------------------------------------
+ def _open_file_from_session(self, session) -> None:
+ target = self._resolve_file_path(session)
+ if target is None or not target.is_file():
+ return
+
+ if sys.platform.startswith("darwin"):
+ subprocess.Popen(["open", str(target)])
+ elif os.name == "nt":
+ os.startfile(str(target))
+ else:
+ subprocess.Popen(["xdg-open", str(target)])
diff --git a/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py b/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
index e205cc8..ede6b49 100644
--- a/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
+++ b/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
@@ -18,11 +18,18 @@
# Authors: see Authors.txt
# ******************************************************************************
+import os
+import subprocess
+import sys
+from pathlib import Path
from typing import Literal
+
+from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
from pySimBlocks.gui.blocks.port_meta import PortMeta
-from pySimBlocks.gui.model import BlockInstance, PortInstance
+from pySimBlocks.gui.models import BlockInstance, PortInstance
class SofaPlantMeta(BlockMeta):
@@ -82,6 +89,9 @@ def __init__(self):
)
]
+ # --------------------------------------------------------------------------
+ # Port Resolution
+ # --------------------------------------------------------------------------
def resolve_port_group(
self,
port_meta: PortMeta,
@@ -118,3 +128,93 @@ def resolve_port_group(
]
return super().resolve_port_group(port_meta, direction, instance)
+
+ # --------------------------------------------------------------------------
+ # Dialog Methods
+ # --------------------------------------------------------------------------
+ def build_param(
+ self,
+ session,
+ form: QFormLayout,
+ readonly: bool = False,
+ ):
+ name_edit = QLineEdit(session.instance.name)
+ name_edit.textChanged.connect(
+ lambda val: self._on_param_changed(val, "name", session, readonly)
+ )
+ form.addRow(QLabel("Block name:"), name_edit)
+ if readonly:
+ name_edit.setReadOnly(True)
+ session.name_edit = name_edit
+
+ for pmeta in self.parameters:
+ if pmeta.name == "scene_file":
+ self.build_file_param_row(
+ session,
+ form,
+ pmeta,
+ readonly=readonly,
+ file_filter="SOFA scene files (*.py);;All files (*)",
+ )
+ continue
+
+ label, widget = self._create_param_row(session, pmeta, readonly)
+ if widget is None:
+ continue
+ if readonly:
+ self._set_readonly_style(widget)
+
+ form.addRow(label, widget)
+ session.param_widgets[pmeta.name] = widget
+ session.param_labels[pmeta.name] = label
+
+ # ------------------------------------------------------
+ def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
+ open_btn = QPushButton("Open file")
+ open_btn.clicked.connect(lambda: self._open_file_from_session(session))
+ form.addRow(QLabel(""), open_btn)
+ session.open_file_btn = open_btn
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def refresh_form(self, session):
+ super().refresh_form(session)
+ self._refresh_open_button_state(session)
+
+ # ------------------------------------------------------
+ def _resolve_file_path(self, session) -> Path | None:
+ raw = session.local_params.get("scene_file")
+ if not raw:
+ return None
+
+ path = Path(str(raw)).expanduser()
+ if not path.is_absolute() and session.project_dir is not None:
+ path = (session.project_dir / path).resolve()
+ return path
+
+ # ------------------------------------------------------
+ def _refresh_open_button_state(self, session) -> None:
+ btn = getattr(session, "open_file_btn", None)
+ if btn is None:
+ return
+
+ target = self._resolve_file_path(session)
+ exists = target is not None and target.is_file()
+ btn.setEnabled(exists)
+ if exists:
+ btn.setToolTip(str(target))
+ else:
+ btn.setToolTip("Set a valid existing scene_file to open the file.")
+
+ # ------------------------------------------------------
+ def _open_file_from_session(self, session) -> None:
+ target = self._resolve_file_path(session)
+ if target is None or not target.is_file():
+ return
+
+ if sys.platform.startswith("darwin"):
+ subprocess.Popen(["open", str(target)])
+ elif os.name == "nt":
+ os.startfile(str(target))
+ else:
+ subprocess.Popen(["xdg-open", str(target)])
diff --git a/pySimBlocks/gui/dialogs/block_dialog.py b/pySimBlocks/gui/dialogs/block_dialog.py
index 2f526c0..5c7f46f 100644
--- a/pySimBlocks/gui/dialogs/block_dialog.py
+++ b/pySimBlocks/gui/dialogs/block_dialog.py
@@ -18,31 +18,21 @@
# Authors: see Authors.txt
# ******************************************************************************
-import ast
+from typing import TYPE_CHECKING
-from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
- QComboBox,
QDialog,
QFormLayout,
- QFrame,
QHBoxLayout,
- QLabel,
- QLineEdit,
QMessageBox,
QPushButton,
- QSizePolicy,
- QTextBrowser,
QVBoxLayout,
)
-from pySimBlocks.gui.blocks.block_meta import ParameterMeta
from pySimBlocks.gui.dialogs.help_dialog import HelpDialog
-from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pySimBlocks.gui.graphics.block_item import BlockItem
- from PySide6.QtWidgets import QWidget
class BlockDialog(QDialog):
@@ -52,7 +42,8 @@ def __init__(self,
):
super().__init__()
self.block = block
- self.local_params: dict[str, Any] = dict(block.instance.parameters)
+ self.meta = block.instance.meta
+ self.instance = block.instance
self.readonly = readonly
if self.readonly:
@@ -61,13 +52,30 @@ def __init__(self,
self.setWindowTitle(f"Edit [{self.block.instance.name}] Parameters")
self.setMinimumWidth(300)
- self.param_widgets: dict[str, QWidget] = {}
- self.param_labels: dict[str, QLabel] = {}
-
main_layout = QVBoxLayout(self)
- self.build_parameters_form(main_layout)
+ project_dir = None
+ if hasattr(self.block, "view") and self.block.view is not None:
+ controller = getattr(self.block.view, "project_controller", None)
+ if controller is not None and controller.project_state is not None:
+ project_dir = controller.project_state.directory_path
+
+ self.session = self.meta.create_dialog_session(self.instance, project_dir)
+ self.build_meta_layout(main_layout)
+ self.build_buttons_layout(main_layout)
+
+ # --------------------------------------------------------------------------
+ # Dialog Layout Methods
+ # --------------------------------------------------------------------------
+ def build_meta_layout(self, layout: QVBoxLayout):
+ form = QFormLayout()
+ self.meta.build_description(form)
+ self.meta.build_pre_param(self.session, form, self.readonly)
+ self.meta.build_param(self.session, form, self.readonly)
+ self.meta.build_post_param(self.session, form, self.readonly)
+ layout.addLayout(form)
+ self.meta.refresh_form(self.session)
- # --- Buttons row ---
+ def build_buttons_layout(self, layout: QVBoxLayout):
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
@@ -85,129 +93,29 @@ def __init__(self,
apply_btn.clicked.connect(self.apply)
buttons_layout.addWidget(apply_btn)
- main_layout.addLayout(buttons_layout)
-
- # ------------------------------------------------------------
- # Form
- def description_part(self, form):
- title = QLabel(f"{self.block.instance.meta.name}")
- title.setAlignment(Qt.AlignmentFlag.AlignLeft)
- form.addRow(title)
-
- frame = QFrame()
- frame.setFrameShape(QFrame.StyledPanel)
- frame.setFrameShadow(QFrame.Raised)
- frame.setLineWidth(1)
-
- frame_layout = QVBoxLayout(frame)
- frame_layout.setContentsMargins(8, 6, 8, 6)
-
- desc = QTextBrowser()
- desc.setMarkdown(self.block.instance.meta.description)
- desc.setReadOnly(True)
- desc.setFrameShape(QFrame.NoFrame)
- desc.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
- desc.document().setTextWidth(400)
- desc.setFixedHeight(int(desc.document().size().height()) + 6)
-
- frame_layout.addWidget(desc)
- form.addRow(frame)
-
- def build_parameters_form(self, layout):
- meta = self.block.instance.meta
- inst_params = self.block.instance.parameters
-
- form = QFormLayout()
- self.description_part(form)
-
- # --- Block name ---
- self.name_edit = QLineEdit(self.block.instance.name)
- form.addRow(QLabel("Block name:"), self.name_edit)
if self.readonly:
- self.name_edit.setReadOnly(True)
-
- for param_meta in meta.parameters:
- param_name = param_meta.name
-
- widget = self._create_param_widget(param_meta, inst_params)
- if widget is None:
- continue
- if self.readonly:
- if isinstance(widget, QLineEdit):
- widget.setReadOnly(True)
- widget.setStyleSheet("""
- QLineEdit {
- background-color: #2b2b2b;
- color: #888888;
- border: 1px solid #444444;
- }
- """)
- elif isinstance(widget, QComboBox):
- widget.setEnabled(False)
-
- label = QLabel(f"{param_name}:")
- if param_meta.description:
- label.setToolTip(param_meta.description)
- form.addRow(label, widget)
-
- self.param_widgets[param_name] = widget
- self.param_labels[param_name] = label
-
- layout.addLayout(form)
- self.refresh_form()
-
- def refresh_form(self):
- """
- Refresh the parameter widgets visibility based on
- BlockMeta.is_parameter_active and current local_params.
- """
- meta = self.block.instance.meta
+ ok_btn.setEnabled(False)
+ apply_btn.setEnabled(False)
- for param_name, widget in self.param_widgets.items():
- label = self.param_labels[param_name]
+ layout.addLayout(buttons_layout)
- active = meta.is_parameter_active(param_name, self.local_params)
- widget.setVisible(active)
- label.setVisible(active)
-
- # ------------------------------------------------------------
- # Buttons
- # ------------------------------------------------------------
+ # --------------------------------------------------------------------------
+ # Button Methods
+ # --------------------------------------------------------------------------
def apply(self):
if self.readonly:
return
- def get_param_value(widget: 'QWidget'):
- if isinstance(widget, QComboBox):
- return widget.currentText()
-
- if isinstance(widget, QLineEdit):
- text = widget.text().strip()
- if not text:
- return None
- try:
- return ast.literal_eval(text)
- except Exception:
- return text
-
- return None
-
- params: dict[str, Any] = {
- "name": self.name_edit.text(),
- **{
- pname: get_param_value(widget)
- for pname, widget in self.param_widgets.items()
- }
- }
-
+ params = self.meta.gather_params(self.session)
self.block.view.update_block_param_event(self.block.instance, params)
+ # ------------------------------------------------------------
def ok(self):
self.apply()
self.accept()
-
+ # ------------------------------------------------------------
def open_help(self):
help_path = self.block.instance.meta.doc_path
@@ -215,45 +123,3 @@ def open_help(self):
HelpDialog(help_path, self).exec()
else:
QMessageBox.information(self, "Help", "No documentation available.")
-
-
- # ------------------------------------------------------------
- # internal methods
- def _create_param_widget(self,
- meta: ParameterMeta,
- inst_params: dict[str, Any]
- ):
- param_name = meta.name
- param_type = meta.type
- value = inst_params.get(param_name)
-
- # ENUM
- if param_type == "enum":
- combo = QComboBox()
- for v in meta.enum:
- combo.addItem(str(v))
- if value is not None:
- combo.setCurrentText(str(value))
- combo.currentTextChanged.connect(
- lambda val, name=param_name: self._on_param_changed(name, val)
- )
- return combo
-
- # SCALAR / FLOAT / INT / VECTOR / MATRIX
- edit = QLineEdit()
- if value is not None:
- edit.setText(str(value))
- elif meta.default:
- edit.setText(str(meta.default))
- edit.textChanged.connect(
- lambda val, name=param_name: self._on_param_changed(name, val)
- )
-
- return edit
-
-
- def _on_param_changed(self, name, value):
- if self.readonly:
- return
- self.local_params[name] = value
- self.refresh_form()
diff --git a/pySimBlocks/gui/dialogs/display_yaml_dialog.py b/pySimBlocks/gui/dialogs/display_yaml_dialog.py
index c671aff..fa5d0e0 100644
--- a/pySimBlocks/gui/dialogs/display_yaml_dialog.py
+++ b/pySimBlocks/gui/dialogs/display_yaml_dialog.py
@@ -30,7 +30,7 @@
)
from PySide6.QtGui import QFont
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.services.yaml_tools import dump_parameter_yaml, dump_model_yaml, dump_layout_yaml
from pySimBlocks.gui.widgets.diagram_view import DiagramView
diff --git a/pySimBlocks/gui/dialogs/plot_dialog.py b/pySimBlocks/gui/dialogs/plot_dialog.py
index 0806a65..8c48900 100644
--- a/pySimBlocks/gui/dialogs/plot_dialog.py
+++ b/pySimBlocks/gui/dialogs/plot_dialog.py
@@ -31,7 +31,7 @@
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.core.config import PlotConfig
from pySimBlocks.project.plot_from_config import plot_from_config
diff --git a/pySimBlocks/gui/dialogs/settings/plots.py b/pySimBlocks/gui/dialogs/settings/plots.py
index bea481a..0687746 100644
--- a/pySimBlocks/gui/dialogs/settings/plots.py
+++ b/pySimBlocks/gui/dialogs/settings/plots.py
@@ -24,7 +24,7 @@
)
from PySide6.QtCore import Qt
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
diff --git a/pySimBlocks/gui/dialogs/settings/project.py b/pySimBlocks/gui/dialogs/settings/project.py
index bd2335e..5b01010 100644
--- a/pySimBlocks/gui/dialogs/settings/project.py
+++ b/pySimBlocks/gui/dialogs/settings/project.py
@@ -24,7 +24,7 @@
QWidget, QFormLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QFileDialog, QHBoxLayout
)
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
from pySimBlocks.gui.services.project_loader import ProjectLoaderYaml
diff --git a/pySimBlocks/gui/dialogs/settings/simulation.py b/pySimBlocks/gui/dialogs/settings/simulation.py
index 11cf7e5..1567db2 100644
--- a/pySimBlocks/gui/dialogs/settings/simulation.py
+++ b/pySimBlocks/gui/dialogs/settings/simulation.py
@@ -24,7 +24,7 @@
)
from PySide6.QtCore import Qt
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
diff --git a/pySimBlocks/gui/dialogs/settings_dialog.py b/pySimBlocks/gui/dialogs/settings_dialog.py
index 3efa3d8..d25f0de 100644
--- a/pySimBlocks/gui/dialogs/settings_dialog.py
+++ b/pySimBlocks/gui/dialogs/settings_dialog.py
@@ -25,7 +25,7 @@
from pySimBlocks.gui.dialogs.settings.project import ProjectSettingsWidget
from pySimBlocks.gui.dialogs.settings.simulation import SimulationSettingsWidget
from pySimBlocks.gui.dialogs.settings.plots import PlotSettingsWidget
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
diff --git a/pySimBlocks/gui/graphics/block_item.py b/pySimBlocks/gui/graphics/block_item.py
index 4f358c0..2b80f11 100644
--- a/pySimBlocks/gui/graphics/block_item.py
+++ b/pySimBlocks/gui/graphics/block_item.py
@@ -28,7 +28,7 @@
from pySimBlocks.gui.graphics.port_item import PortItem
if TYPE_CHECKING:
- from pySimBlocks.gui.model.block_instance import BlockInstance
+ from pySimBlocks.gui.models.block_instance import BlockInstance
from pySimBlocks.gui.widgets.diagram_view import DiagramView
diff --git a/pySimBlocks/gui/graphics/connection_item.py b/pySimBlocks/gui/graphics/connection_item.py
index 573514d..4ba9221 100644
--- a/pySimBlocks/gui/graphics/connection_item.py
+++ b/pySimBlocks/gui/graphics/connection_item.py
@@ -23,7 +23,7 @@
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem
from pySimBlocks.gui.graphics.port_item import PortItem
-from pySimBlocks.gui.model.connection_instance import ConnectionInstance
+from pySimBlocks.gui.models.connection_instance import ConnectionInstance
class OrthogonalRoute:
diff --git a/pySimBlocks/gui/graphics/port_item.py b/pySimBlocks/gui/graphics/port_item.py
index 9941401..e400070 100644
--- a/pySimBlocks/gui/graphics/port_item.py
+++ b/pySimBlocks/gui/graphics/port_item.py
@@ -26,7 +26,7 @@
if TYPE_CHECKING:
from pySimBlocks.gui.graphics.block_item import BlockItem
- from pySimBlocks.gui.model.port_instance import PortInstance
+ from pySimBlocks.gui.models.port_instance import PortInstance
class PortItem(QGraphicsItem):
diff --git a/pySimBlocks/gui/main_window.py b/pySimBlocks/gui/main_window.py
index e59a5ec..8ef9041 100644
--- a/pySimBlocks/gui/main_window.py
+++ b/pySimBlocks/gui/main_window.py
@@ -29,7 +29,7 @@
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.dialogs.unsaved_dialog import UnsavedChangesDialog
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
from pySimBlocks.gui.services.project_loader import ProjectLoaderYaml
from pySimBlocks.gui.services.project_saver import ProjectSaverYaml
diff --git a/pySimBlocks/gui/model/__init__.py b/pySimBlocks/gui/models/__init__.py
similarity index 82%
rename from pySimBlocks/gui/model/__init__.py
rename to pySimBlocks/gui/models/__init__.py
index 5fa8750..f89cebb 100644
--- a/pySimBlocks/gui/model/__init__.py
+++ b/pySimBlocks/gui/models/__init__.py
@@ -18,10 +18,10 @@
# Authors: see Authors.txt
# ******************************************************************************
-from pySimBlocks.gui.model.block_instance import BlockInstance
-from pySimBlocks.gui.model.connection_instance import ConnectionInstance
-from pySimBlocks.gui.model.port_instance import PortInstance
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.block_instance import BlockInstance
+from pySimBlocks.gui.models.connection_instance import ConnectionInstance
+from pySimBlocks.gui.models.port_instance import PortInstance
+from pySimBlocks.gui.models.project_state import ProjectState
__all__ = [
"BlockInstance",
diff --git a/pySimBlocks/gui/model/block_instance.py b/pySimBlocks/gui/models/block_instance.py
similarity index 98%
rename from pySimBlocks/gui/model/block_instance.py
rename to pySimBlocks/gui/models/block_instance.py
index 6514063..95ed7d4 100644
--- a/pySimBlocks/gui/model/block_instance.py
+++ b/pySimBlocks/gui/models/block_instance.py
@@ -21,7 +21,7 @@
import uuid
from typing import TYPE_CHECKING, Any, Dict, List
-from pySimBlocks.gui.model.port_instance import PortInstance
+from pySimBlocks.gui.models.port_instance import PortInstance
try: # Python 3.11+
from typing import Self
diff --git a/pySimBlocks/gui/model/connection_instance.py b/pySimBlocks/gui/models/connection_instance.py
similarity index 100%
rename from pySimBlocks/gui/model/connection_instance.py
rename to pySimBlocks/gui/models/connection_instance.py
diff --git a/pySimBlocks/gui/model/port_instance.py b/pySimBlocks/gui/models/port_instance.py
similarity index 96%
rename from pySimBlocks/gui/model/port_instance.py
rename to pySimBlocks/gui/models/port_instance.py
index ce09d11..b818e80 100644
--- a/pySimBlocks/gui/model/port_instance.py
+++ b/pySimBlocks/gui/models/port_instance.py
@@ -23,7 +23,7 @@
from pySimBlocks.gui.blocks.port_meta import PortMeta
if TYPE_CHECKING:
- from pySimBlocks.gui.model.connection_instance import ConnectionInstance
+ from pySimBlocks.gui.models.connection_instance import ConnectionInstance
from pySimBlocks.gui.project_controller import BlockInstance
class PortInstance:
diff --git a/pySimBlocks/gui/model/project_simulation_params.py b/pySimBlocks/gui/models/project_simulation_params.py
similarity index 100%
rename from pySimBlocks/gui/model/project_simulation_params.py
rename to pySimBlocks/gui/models/project_simulation_params.py
diff --git a/pySimBlocks/gui/model/project_state.py b/pySimBlocks/gui/models/project_state.py
similarity index 94%
rename from pySimBlocks/gui/model/project_state.py
rename to pySimBlocks/gui/models/project_state.py
index 00277de..949e924 100644
--- a/pySimBlocks/gui/model/project_state.py
+++ b/pySimBlocks/gui/models/project_state.py
@@ -20,9 +20,9 @@
from pathlib import Path
-from pySimBlocks.gui.model.block_instance import BlockInstance, PortInstance
-from pySimBlocks.gui.model.connection_instance import ConnectionInstance
-from pySimBlocks.gui.model.project_simulation_params import ProjectSimulationParams
+from pySimBlocks.gui.models.block_instance import BlockInstance, PortInstance
+from pySimBlocks.gui.models.connection_instance import ConnectionInstance
+from pySimBlocks.gui.models.project_simulation_params import ProjectSimulationParams
class ProjectState:
def __init__(self, directory_path: Path):
diff --git a/pySimBlocks/gui/project_controller.py b/pySimBlocks/gui/project_controller.py
index 60455a3..1e13ebb 100644
--- a/pySimBlocks/gui/project_controller.py
+++ b/pySimBlocks/gui/project_controller.py
@@ -25,7 +25,7 @@
from PySide6.QtCore import QObject, Signal, QPointF
-from pySimBlocks.gui.model import (
+from pySimBlocks.gui.models import (
BlockInstance,
ConnectionInstance,
PortInstance,
diff --git a/pySimBlocks/gui/services/project_loader.py b/pySimBlocks/gui/services/project_loader.py
index 3fbf260..c19e876 100644
--- a/pySimBlocks/gui/services/project_loader.py
+++ b/pySimBlocks/gui/services/project_loader.py
@@ -71,9 +71,12 @@ def _load_blocks(self,
# ---- parameters ----
raw_params = params_blocks.get(name, {})
- for pname, pvalue in raw_params.items():
- if pname in block.parameters:
- block.parameters[pname] = pvalue
+ for pmeta in block.meta.parameters:
+ pname = pmeta.name
+ if pname in raw_params:
+ block.parameters[pname] = raw_params[pname]
+ elif pmeta.autofill and pmeta.default is not None:
+ block.parameters[pname] = pmeta.default
block.resolve_ports()
controller.view.refresh_block_port(block)
diff --git a/pySimBlocks/gui/services/project_saver.py b/pySimBlocks/gui/services/project_saver.py
index 003aefd..e2ec36f 100644
--- a/pySimBlocks/gui/services/project_saver.py
+++ b/pySimBlocks/gui/services/project_saver.py
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
-from pySimBlocks.gui.model import ProjectState
+from pySimBlocks.gui.models import ProjectState
from pySimBlocks.gui.graphics import BlockItem
from pySimBlocks.gui.services.yaml_tools import save_yaml
from pySimBlocks.project.generate_run_script import generate_python_content
diff --git a/pySimBlocks/gui/services/simulation_runner.py b/pySimBlocks/gui/services/simulation_runner.py
index 9bf209f..bae73b6 100644
--- a/pySimBlocks/gui/services/simulation_runner.py
+++ b/pySimBlocks/gui/services/simulation_runner.py
@@ -1,7 +1,7 @@
import os
import shutil
import sys
-from pySimBlocks.gui.model import ProjectState
+from pySimBlocks.gui.models import ProjectState
from pySimBlocks.gui.services.yaml_tools import save_yaml
from pySimBlocks.project.generate_run_script import generate_python_content
diff --git a/pySimBlocks/gui/services/yaml_tools.py b/pySimBlocks/gui/services/yaml_tools.py
index 7a30722..047510a 100644
--- a/pySimBlocks/gui/services/yaml_tools.py
+++ b/pySimBlocks/gui/services/yaml_tools.py
@@ -23,7 +23,7 @@
import yaml
from pySimBlocks.gui.graphics.block_item import BlockItem
-from pySimBlocks.gui.model.project_state import ProjectState
+from pySimBlocks.gui.models.project_state import ProjectState
def load_yaml_file(path: str) -> dict:
diff --git a/pySimBlocks/gui/widgets/block_list.py b/pySimBlocks/gui/widgets/block_list.py
index d8949d1..b8940e5 100644
--- a/pySimBlocks/gui/widgets/block_list.py
+++ b/pySimBlocks/gui/widgets/block_list.py
@@ -25,7 +25,7 @@
from pySimBlocks.gui.blocks.block_meta import BlockMeta
from pySimBlocks.gui.dialogs.block_dialog import BlockDialog
-from pySimBlocks.gui.model.block_instance import BlockInstance
+from pySimBlocks.gui.models.block_instance import BlockInstance
class _PreviewBlock:
def __init__(self, instance):
diff --git a/pySimBlocks/gui/widgets/diagram_view.py b/pySimBlocks/gui/widgets/diagram_view.py
index 1a41038..a4f98bc 100644
--- a/pySimBlocks/gui/widgets/diagram_view.py
+++ b/pySimBlocks/gui/widgets/diagram_view.py
@@ -28,8 +28,8 @@
from pySimBlocks.gui.graphics.connection_item import ConnectionItem, OrthogonalRoute
from pySimBlocks.gui.graphics.port_item import PortItem
from pySimBlocks.gui.graphics.theme import make_theme
-from pySimBlocks.gui.model.block_instance import BlockInstance
-from pySimBlocks.gui.model.connection_instance import ConnectionInstance
+from pySimBlocks.gui.models.block_instance import BlockInstance
+from pySimBlocks.gui.models.connection_instance import ConnectionInstance
if TYPE_CHECKING:
from pySimBlocks.gui.project_controller import ProjectController
diff --git a/test/gui/test_block_meta_param_cache.py b/test/gui/test_block_meta_param_cache.py
new file mode 100644
index 0000000..62df61e
--- /dev/null
+++ b/test/gui/test_block_meta_param_cache.py
@@ -0,0 +1,16 @@
+from pySimBlocks.gui.blocks.block_dialog_session import BlockDialogSession
+from pySimBlocks.gui.blocks.controllers.pid import PIDMeta
+from pySimBlocks.gui.models.block_instance import BlockInstance
+
+
+def test_gather_params_keeps_inactive_values_for_cache():
+ meta = PIDMeta()
+ instance = BlockInstance(meta)
+ session = BlockDialogSession(meta, instance)
+
+ session.local_params["controller"] = "PD" # Ki inactive for PD
+ session.local_params["Ki"] = 10
+
+ gathered = meta.gather_params(session)
+
+ assert gathered["Ki"] == 10