basic openmodelica model emitting

This commit is contained in:
2026-07-20 17:34:18 +02:00
parent 42bf09610b
commit d368b36b8c
26 changed files with 1608 additions and 594 deletions

View File

@@ -54,6 +54,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Components may contain nested graph or text implementations.
- Ports retain stable IDs. Display names, types, orientation, positions, and icon
anchors may change without changing IDs.
- Parameters belong to `Component` rather than a particular implementation kind,
so graph and text components share stable-ID name/type/value records.
- Port orientation is presented as one unified list in the UI, while the model
indexes inputs and outputs separately for connection semantics.
- Port types are registered in `core/port_types.py`. Only compatible types may be
@@ -92,9 +94,23 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Avoid the QSettings group name `general`; Qt treats `General` specially in INI
files. Autosave keys live under `autosave/`.
- User-visible document edits should participate in undo/redo.
- The text-definition editor uses OpenModelica highlighting and completion from
`src/bedit/data/syntax/openmodelica.json`. Keep keywords, types, built-ins, and
named BEdit `$name$` macro completions editable there; arbitrary `$name$`
expressions are highlighted as BEvalues. Highlight colors and bold/italic
styles are persisted under `syntax/<category>/` in application settings.
- Application-wide messages use `core.application_log.get_logger()`. The main
window installs the Qt log-panel handler; core code must only use standard
Python logging and must not import the GUI handler.
- File → Reload Simulation Code (`Ctrl+F5`) reloads modules under
`bedit.core.simulation`, replaces the shared application/controller service,
and preserves the previous instance attributes where possible.
- Simulation compilation lives in `core/simulation/compiler.py`; the simulation
service only owns application state and delegates compilation. Ports with
`multipleConnections` are emitted as Modelica arrays. Their size is inferred
per component instance from graph connections and exposed while compiling as
`$portname_N$`; array connection endpoints receive stable one-based indices in
graph connection order.
## Qt Designer and generated files
@@ -134,6 +150,9 @@ pyside6-uic --from-imports ui/text_definition_editor.ui \
pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
-o src/bedit/gui/generated/ui_simulation_settings_dialog.py
pyside6-uic --from-imports ui/parameter_options_dialog.ui \
-o src/bedit/gui/generated/ui_parameter_options_dialog.py
```
When adding a promoted/custom widget in Designer, its header must use the real

View File

@@ -26,7 +26,11 @@ bedit = "bedit.gui.app:main"
where = ["src"]
[tool.setuptools.package-data]
bedit = ["data/libraries/*.json", "data/libraries/*.beb"]
bedit = [
"data/libraries/*.json",
"data/libraries/*.beb",
"data/syntax/*.json",
]
[tool.ruff]
line-length = 100

View File

@@ -173,9 +173,9 @@ class Parameter:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
if not isinstance(data, dict):
raise ValueError("Each text component parameter must be an object")
raise ValueError("Each component parameter must be an object")
if "id" not in data:
raise ValueError("Each text component parameter must have an ID")
raise ValueError("Each component parameter must have an ID")
return cls(
id=str(data["id"]),
name=str(data.get("name", "")),
@@ -301,6 +301,7 @@ class Component:
rotation: float = 0.0
inputs: list[Port] = field(default_factory=list)
outputs: list[Port] = field(default_factory=list)
parameters: list[Parameter] = field(default_factory=list)
icon: Icon = field(default_factory=Icon)
properties: dict[str, Any] = field(default_factory=dict)
implementation_kind: str = "graph"
@@ -323,6 +324,7 @@ class Component:
"inputs": [port.to_dict() for port in self.inputs],
"outputs": [port.to_dict() for port in self.outputs],
},
"parameters": [parameter.to_dict() for parameter in self.parameters],
"icon": self.icon.to_dict(),
"properties": self.properties,
"library": {"showSubtree": self.show_subtree_in_library},
@@ -338,18 +340,18 @@ class Component:
if kind not in {"graph", "text"}:
raise ValueError(f"Unknown component implementation kind: {kind}")
source: dict[str, Any] = {}
parameters = data.get("parameters", [])
if kind == "text":
raw_source = implementation.get("source", {})
equations = raw_source.get("equations", "")
parameters = raw_source.get("parameters", [])
parameters = data.get("parameters", raw_source.get("parameters", []))
if not isinstance(equations, str):
raise ValueError("Text component equations must be a string")
if not isinstance(parameters, list):
raise ValueError("Text component parameters must be a list")
source = {
"equations": equations,
"parameters": [Parameter.from_dict(item).to_dict() for item in parameters],
}
if not isinstance(parameters, list):
raise ValueError("Component parameters must be a list")
return cls(
id=str(data["id"]),
name=str(data.get("name", "Unnamed")),
@@ -358,6 +360,7 @@ class Component:
rotation=float(data.get("rotation", 0.0)),
inputs=[Port.from_dict(item) for item in interface.get("inputs", [])],
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])],
parameters=[Parameter.from_dict(item) for item in parameters],
icon=Icon.from_dict(data.get("icon")),
properties=dict(data.get("properties", {})),
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
@@ -434,6 +437,14 @@ class GraphDocument:
if component.id in seen:
raise ValueError(f"Duplicate component ID: {component.id}")
seen.add(component.id)
parameter_ids = [parameter.id for parameter in component.parameters]
parameter_names = [parameter.name for parameter in component.parameters]
if len(set(parameter_ids)) != len(parameter_ids):
raise ValueError(f"Component {component.name!r} has duplicate parameter IDs")
if len(set(parameter_names)) != len(parameter_names):
raise ValueError(f"Component {component.name!r} has duplicate parameter names")
if any(not name.strip() for name in parameter_names):
raise ValueError(f"Component {component.name!r} has an unnamed parameter")
if component.implementation_kind == "text" and component.graph.blocks:
raise ValueError(f"Text component {component.name} cannot contain a graph")
self._validate_graph(component)
@@ -612,6 +623,7 @@ def clone_component(source: Component) -> Component:
)
for port in current.outputs
],
parameters=deepcopy(current.parameters),
icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind,

View File

@@ -0,0 +1,326 @@
import re
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
_MODELICA_TYPES = {
"real": "Real",
"integer": "Integer",
"boolean": "Boolean",
"string": "String",
}
_BEVALUE_PATTERN = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)\$")
@dataclass(frozen=True)
class CompilationResult:
"""The intermediate data and generated source produced by compilation."""
graph: dict[str, Any]
objects_by_id: dict[str, Any]
modelica: str
def compile_graph(graph: dict[str, Any]) -> CompilationResult:
"""Clean, index, and emit a serialized component tree."""
cleaned_graph = cleanup_graph(deepcopy(graph))
objects_by_id = build_id_list(cleaned_graph)
return CompilationResult(
graph=cleaned_graph,
objects_by_id=objects_by_id,
modelica=emit_model(cleaned_graph, objects_by_id),
)
def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
"""Index all addressable objects in a component tree by their stable ID."""
id_list: dict[str, Any] = {}
id_kinds: dict[str, str] = {}
def add(item: dict[str, Any], description: str) -> None:
item_id = item.get("id")
if not item_id:
raise ValueError(f"{description} has no ID")
# Port IDs identify a port on a component definition and may therefore
# recur in cloned component instances. Component and junction IDs are
# document objects and must remain globally unique.
if item_id in id_list and not (
description == "port" and id_kinds[item_id] == "port"
):
raise ValueError(f"Duplicate simulation object ID: {item_id}")
id_list[item_id] = item
id_kinds[item_id] = description
def visit(component: dict[str, Any]) -> None:
add(component, "component")
interface = component.get("interface", {})
for port in interface.get("inputs", []):
add(port, "port")
for port in interface.get("outputs", []):
add(port, "port")
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return
nested_graph = implementation.get("graph", {})
for junction in nested_graph.get("junctions", []):
add(junction, "junction")
for block in nested_graph.get("blocks", []):
visit(block)
visit(graph)
return id_list
def emit_model(
graph: dict[str, Any],
id_list: dict[str, Any],
indent: int = 0,
connection_counts: dict[str, int] | None = None,
) -> str:
"""Emit a component and its nested definitions as Modelica source."""
del id_list # Kept in the public API for compiler extensions and inspection.
indentation = "\t" * indent
body_indent = "\t" * (indent + 1)
model_name = identifier(graph["id"])
lines = [f"{indentation}model {model_name}"]
implementation = graph.get("implementation", {})
implementation_kind = implementation.get("kind")
nested_graph = implementation.get("graph", {})
port_counts = connection_counts or _interface_connection_counts(graph)
macros = _port_count_macros(graph, port_counts)
if implementation_kind == "graph":
for block in nested_graph.get("blocks", []):
lines.extend(
emit_model(
block,
{},
indent + 1,
_block_connection_counts(nested_graph, block["id"]),
)
.rstrip()
.splitlines()
)
interface = graph.get("interface", {})
for port in interface.get("inputs", []):
lines.append(_port_declaration(port, "input", indent + 1, macros))
for port in interface.get("outputs", []):
lines.append(_port_declaration(port, "output", indent + 1, macros))
for parameter in graph.get("parameters", []):
parameter_type = modelica_type(parameter.get("type", "real"))
parameter_name = identifier(parameter["name"])
value = expand_bevalues(str(parameter.get("value", "0")), macros)
lines.append(
f"{body_indent}parameter {parameter_type} {parameter_name} = {value};"
)
if implementation_kind == "graph":
for block in nested_graph.get("blocks", []):
block_type = identifier(block["id"])
block_name = identifier(block["name"])
lines.append(f"{body_indent}{block_type} {block_name};")
for junction in nested_graph.get("junctions", []):
junction_type = modelica_type(junction.get("type", "signal"))
lines.append(
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
)
lines.append(f"{indentation}equation")
if implementation_kind == "graph":
blocks = {block["id"]: block for block in nested_graph.get("blocks", [])}
junctions = {
junction["id"]: junction for junction in nested_graph.get("junctions", [])
}
endpoint_indices: dict[tuple[str, str, str], int] = {}
for connection in nested_graph.get("connections", []):
source = _endpoint_expression(
connection["source"], graph, blocks, junctions, endpoint_indices
)
target = _endpoint_expression(
connection["target"], graph, blocks, junctions, endpoint_indices
)
# Connector types may require different equations in future.
lines.append(f"{body_indent}{target} = {source};")
else:
equations = str(implementation.get("source", {}).get("equations", ""))
equations = expand_bevalues(equations, macros)
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in equations.splitlines()
)
lines.append(f"{indentation}end {model_name};")
return "\n".join(lines) + "\n"
def cleanup_graph(graph: dict[str, Any]) -> dict[str, Any]:
"""Remove annotations and UI-only data from a serialized component tree."""
def remove_key_with_lists(data: Any, target_key: str) -> None:
if isinstance(data, dict):
for key in list(data):
if key == target_key:
del data[key]
else:
remove_key_with_lists(data[key], target_key)
elif isinstance(data, list):
for item in data:
remove_key_with_lists(item, target_key)
for key in (
"position",
"rotation",
"iconPosition",
"icon",
"annotations",
"library",
"properties",
):
remove_key_with_lists(graph, key)
return graph
def _port_declaration(
port: dict[str, Any], direction: str, indent: int, macros: dict[str, str]
) -> str:
port_type = modelica_type(port.get("type", "signal"))
indentation = "\t" * indent
port_name = identifier(port["name"])
dimension = f"[${port_name}_N$]" if port.get("multipleConnections", False) else ""
declaration = f"{indentation}{direction} {port_type} {port_name}{dimension};"
return expand_bevalues(declaration, macros)
def _endpoint_expression(
endpoint: dict[str, Any],
owner: dict[str, Any],
blocks: dict[str, dict[str, Any]],
junctions: dict[str, dict[str, Any]],
endpoint_indices: dict[tuple[str, str, str], int],
) -> str:
if "junction" in endpoint:
junction_id = endpoint["junction"]
if junction_id not in junctions:
raise ValueError(f"Connection references unknown junction {junction_id!r}")
return _junction_name(junction_id)
if "interface" in endpoint:
port = _find_port(owner, endpoint["interface"])
expression = identifier(port["name"])
return _index_array_endpoint(
expression, port, ("interface", owner["id"], port["id"]), endpoint_indices
)
block_id = endpoint.get("block")
port_id = endpoint.get("port")
block = blocks.get(block_id)
if block is None:
raise ValueError(f"Connection references unknown block {block_id!r}")
port = _find_port(block, port_id)
expression = f"{identifier(block['name'])}.{identifier(port['name'])}"
return _index_array_endpoint(
expression, port, ("block", block_id, port_id), endpoint_indices
)
def _index_array_endpoint(
expression: str,
port: dict[str, Any],
key: tuple[str, str, str],
endpoint_indices: dict[tuple[str, str, str], int],
) -> str:
if not port.get("multipleConnections", False):
return expression
endpoint_indices[key] = endpoint_indices.get(key, 0) + 1
return f"{expression}[{endpoint_indices[key]}]"
def _block_connection_counts(
graph: dict[str, Any], block_id: str
) -> dict[str, int]:
counts: dict[str, int] = {}
for connection in graph.get("connections", []):
for endpoint in (connection.get("source", {}), connection.get("target", {})):
if endpoint.get("block") == block_id and endpoint.get("port"):
port_id = endpoint["port"]
counts[port_id] = counts.get(port_id, 0) + 1
return counts
def _interface_connection_counts(component: dict[str, Any]) -> dict[str, int]:
implementation = component.get("implementation", {})
if implementation.get("kind") != "graph":
return {}
counts: dict[str, int] = {}
for connection in implementation.get("graph", {}).get("connections", []):
for endpoint in (connection.get("source", {}), connection.get("target", {})):
if endpoint.get("interface"):
port_id = endpoint["interface"]
counts[port_id] = counts.get(port_id, 0) + 1
return counts
def _port_count_macros(
component: dict[str, Any], connection_counts: dict[str, int]
) -> dict[str, str]:
macros: dict[str, str] = {}
interface = component.get("interface", {})
for port in (*interface.get("inputs", []), *interface.get("outputs", [])):
if port.get("multipleConnections", False):
macros[f"{identifier(port['name'])}_N"] = str(
connection_counts.get(port["id"], 0)
)
return macros
def expand_bevalues(text: str, values: dict[str, str]) -> str:
"""Replace BEdit ``$name$`` macros and reject unresolved compiler values."""
def replace(match: re.Match[str]) -> str:
name = match.group(1)
if name not in values:
raise ValueError(f"Unknown BEdit value ${name}$")
return values[name]
return _BEVALUE_PATTERN.sub(replace, text)
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
interface = component.get("interface", {})
ports = [*interface.get("inputs", []), *interface.get("outputs", [])]
for port in ports:
if port.get("id") == port_id:
return port
raise ValueError(
f"Component {component.get('name', component.get('id', '?'))!r} "
f"has no port {port_id!r}"
)
def modelica_type(value: str) -> str:
normalized = str(value).strip().lower()
if normalized in {"signal", "signal array"}:
return "Real"
return _MODELICA_TYPES.get(normalized, identifier(str(value)))
def identifier(value: str) -> str:
"""Return a safe unquoted Modelica identifier."""
normalized = re.sub(r"[^A-Za-z0-9_]", "_", str(value).strip())
if not normalized:
raise ValueError("Modelica names cannot be empty")
if normalized[0].isdigit():
normalized = f"model_{normalized}"
return normalized
def _junction_name(junction_id: str) -> str:
return identifier(f"junction_{junction_id}")

View File

@@ -1,18 +1,26 @@
from copy import deepcopy
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.compiler import compile_graph
log = get_logger(__name__)
class Simulation:
"""Application-owned simulation service and state container."""
"""Application-owned simulation state and compiler facade."""
def __init__(self) -> None:
self.state: dict[str, Any] = {}
self.last_compilation_input: dict[str, Any] | None = None
self.last_compilation_output: str | None = None
self.id_list: dict[str, Any] = {}
def compile(self, graph: dict[str, Any]) -> None:
"""Compile a graph definition.
"""Compile a serialized component tree and retain the result."""
This is intentionally a stub. Keeping a copy of the input makes the
service useful for incremental compiler development and UI inspection.
"""
self.last_compilation_input = deepcopy(graph)
result = compile_graph(graph)
self.last_compilation_input = result.graph
self.id_list = result.objects_by_id
self.last_compilation_output = result.modelica
log.info("Generated Modelica model:\n%s", self.last_compilation_output)

View File

@@ -0,0 +1,22 @@
{
"keywords": [
"algorithm", "and", "annotation", "block", "break", "class", "connect",
"connector", "constant", "constrainedby", "der", "discrete", "each", "else",
"elseif", "elsewhen", "encapsulated", "end", "enumeration", "equation",
"expandable", "extends", "external", "false", "final", "flow", "for",
"function", "if", "import", "impure", "in", "initial", "inner", "input",
"loop", "model", "not", "operator", "or", "outer", "output", "package",
"parameter", "partial", "protected", "public", "pure", "record", "redeclare",
"replaceable", "return", "stream", "then", "true", "type", "when", "while",
"within"
],
"types": ["Boolean", "Integer", "Real", "String"],
"builtins": [
"abs", "acos", "actualStream", "asin", "assert", "atan", "atan2", "cardinality",
"ceil", "change", "cos", "cosh", "delay", "div", "edge", "exp", "floor",
"homotopy", "inStream", "integer", "log", "log10", "max", "min", "mod",
"noEvent", "pre", "reinit", "rem", "sample", "semiLinear", "sign", "sin",
"sinh", "smooth", "sqrt", "sum", "tan", "tanh", "terminal", "terminate"
],
"bevalues": []
}

View File

@@ -252,6 +252,19 @@ class EditComponentPropertiesCommand(QUndoCommand):
self.controller._set_component_properties(self.component_id, self.old)
class EditComponentParametersCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: list, new: list) -> None:
super().__init__("Edit component parameters")
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_parameters(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_parameters(self.component_id, self.old)
class RenameInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
super().__init__("Rename interface port")

View File

@@ -17,6 +17,7 @@ from bedit.gui.controllers.commands import (
EditTextDefinitionCommand,
EditComponentAppearanceCommand,
EditComponentPropertiesCommand,
EditComponentParametersCommand,
MoveComponentCommand,
MoveInterfacePortCommand,
PasteSelectionCommand,
@@ -166,7 +167,7 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, self.document.roots.values(), number),
implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": "", "parameters": []} if kind == "text" else {},
source={"equations": ""} if kind == "text" else {},
)
self.undo_stack.push(AddComponentCommand(self, None, component))
self.activate_component(component.id)
@@ -185,7 +186,7 @@ class DocumentController(QObject):
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": "", "parameters": []} if kind == "text" else {},
source={"equations": ""} if kind == "text" else {},
)
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
return component.id
@@ -628,14 +629,15 @@ class DocumentController(QObject):
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"source": deepcopy(component.source),
"parameters": [parameter.to_dict() for parameter in component.parameters],
}
new = {
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"source": {
"equations": equations,
"parameters": [parameter.to_dict() for parameter in parameters],
},
"parameters": [parameter.to_dict() for parameter in parameters],
}
if old != new:
candidate = deepcopy(self.document)
@@ -643,6 +645,7 @@ class DocumentController(QObject):
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.source = deepcopy(new["source"])
candidate_component.parameters = deepcopy(parameters)
candidate.validate()
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
@@ -810,6 +813,27 @@ class DocumentController(QObject):
if old != new:
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
def edit_component_parameters(
self, component_id: str, parameters: list[Parameter]
) -> None:
component = self.document.find_component(component_id) if self.document else None
if component is None:
return
ids = [parameter.id for parameter in parameters]
names = [parameter.name for parameter in parameters]
if len(set(ids)) != len(ids):
raise ValueError("Parameter IDs must be unique")
if len(set(names)) != len(names):
raise ValueError("Parameter names must be unique")
if any(not name.strip() for name in names):
raise ValueError("Every parameter must have a name")
old = [parameter.to_dict() for parameter in component.parameters]
new = [parameter.to_dict() for parameter in parameters]
if old != new:
self.undo_stack.push(
EditComponentParametersCommand(self, component_id, old, new)
)
def delete_selection(
self,
block_ids: set[str],
@@ -1164,6 +1188,17 @@ class DocumentController(QObject):
component.properties = deepcopy(properties)
self.componentPropertiesChanged.emit(component_id)
def _set_component_parameters(self, component_id: str, values: list[dict]) -> None:
component = self.document.find_component(component_id) if self.document else None
if component is not None:
component.parameters = [Parameter.from_dict(item) for item in values]
self.documentReset.emit()
if (
component_id == self.active_component_id
and component.implementation_kind == "text"
):
self.textDefinitionChanged.emit(component_id)
def _delete_items(
self,
owner_id: str | None,
@@ -1239,6 +1274,9 @@ class DocumentController(QObject):
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
component.source = deepcopy(values["source"])
component.parameters = [
Parameter.from_dict(item) for item in values.get("parameters", [])
]
self.interfaceChanged.emit()
self.documentReset.emit()
self.textDefinitionChanged.emit(component_id)

View File

@@ -0,0 +1,108 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QListWidgetItem, QMessageBox
from bedit.core.model import Component, Parameter
from bedit.gui.generated.ui_parameter_options_dialog import Ui_ParameterOptionsDialog
PARAMETER_ROLE = Qt.ItemDataRole.UserRole
class ParameterOptionsDialog(QDialog):
"""Editor for parameters shared by graph and text components."""
def __init__(self, component: Component, parent=None, *, read_only: bool = False) -> None:
super().__init__(parent)
self.ui = Ui_ParameterOptionsDialog()
self.ui.setupUi(self)
self.setWindowTitle(f"Parameter Options — {component.name}")
self.parameters = deepcopy(component.parameters)
self.read_only = read_only
self._loading = False
self.ui.parameterList.currentRowChanged.connect(self._load_current)
self.ui.addParameterButton.clicked.connect(self.add_parameter)
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
self.ui.nameEdit.textEdited.connect(self._store_current)
self.ui.typeEdit.textEdited.connect(self._store_current)
self.ui.valueEdit.textEdited.connect(self._store_current)
self.ui.parameterSplitter.setSizes([250, 370])
if read_only:
self.ui.addParameterButton.setEnabled(False)
self.ui.removeParameterButton.setEnabled(False)
self.ui.nameEdit.setReadOnly(True)
self.ui.typeEdit.setReadOnly(True)
self.ui.valueEdit.setReadOnly(True)
self._rebuild_list(0 if self.parameters else -1)
def _rebuild_list(self, row: int = -1) -> None:
self.ui.parameterList.clear()
for parameter in self.parameters:
item = QListWidgetItem(f"{parameter.name} [{parameter.type}] = {parameter.value}")
item.setData(PARAMETER_ROLE, parameter.id)
self.ui.parameterList.addItem(item)
self.ui.parameterList.setCurrentRow(min(row, len(self.parameters) - 1))
self._update_enabled()
def _load_current(self, row: int) -> None:
self._loading = True
if 0 <= row < len(self.parameters):
parameter = self.parameters[row]
self.ui.nameEdit.setText(parameter.name)
self.ui.typeEdit.setText(parameter.type)
self.ui.valueEdit.setText(parameter.value)
else:
self.ui.nameEdit.clear()
self.ui.typeEdit.clear()
self.ui.valueEdit.clear()
self._loading = False
self._update_enabled()
def _update_enabled(self) -> None:
enabled = self.ui.parameterList.currentRow() >= 0
self.ui.removeParameterButton.setEnabled(enabled and not self.read_only)
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeEdit.setEnabled(enabled)
self.ui.valueEdit.setEnabled(enabled)
def _store_current(self) -> None:
row = self.ui.parameterList.currentRow()
if self._loading or not (0 <= row < len(self.parameters)):
return
parameter = self.parameters[row]
parameter.name = self.ui.nameEdit.text()
parameter.type = self.ui.typeEdit.text()
parameter.value = self.ui.valueEdit.text()
self.ui.parameterList.item(row).setText(
f"{parameter.name} [{parameter.type}] = {parameter.value}"
)
def add_parameter(self) -> None:
self.parameters.append(
Parameter(
id=f"parameter-{uuid4().hex[:8]}",
name=f"Parameter {len(self.parameters) + 1}",
)
)
self._rebuild_list(len(self.parameters) - 1)
self.ui.nameEdit.selectAll()
self.ui.nameEdit.setFocus()
def remove_parameter(self) -> None:
row = self.ui.parameterList.currentRow()
if row >= 0:
self.parameters.pop(row)
self._rebuild_list(min(row, len(self.parameters) - 1))
def accept(self) -> None:
self._store_current()
if any(not parameter.name.strip() for parameter in self.parameters):
QMessageBox.warning(self, "Invalid parameter", "Every parameter needs a name.")
return
names = [parameter.name for parameter in self.parameters]
if len(set(names)) != len(names):
QMessageBox.warning(self, "Invalid parameter", "Parameter names must be unique.")
return
super().accept()

View File

@@ -1,11 +1,13 @@
from pathlib import Path
from PySide6.QtCore import QSettings, Signal
from PySide6.QtWidgets import QDialog, QFileDialog
from PySide6.QtCore import QSettings, Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QColorDialog, QDialog, QFileDialog, QHeaderView, QTableWidgetItem
from bedit.core.libraries import bundled_library_path, default_library_paths
from bedit.gui.preferences import application_settings
from bedit.gui.generated.ui_settings_dialog import Ui_SettingsDialog
from bedit.gui.editors.openmodelica import HIGHLIGHT_STYLES
class SettingsDialog(QDialog):
@@ -25,6 +27,7 @@ class SettingsDialog(QDialog):
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
self.ui.libraryPathsList.itemSelectionChanged.connect(self._update_remove_button)
self.ui.syntaxStylesTable.cellDoubleClicked.connect(self._choose_syntax_color)
self._load_settings()
def _load_settings(self) -> None:
@@ -43,8 +46,47 @@ class SettingsDialog(QDialog):
self.ui.graphGridSpinBox.setValue(self.graph_grid_size(self.settings))
self.ui.graphSnapSpinBox.setValue(self.graph_snap_size(self.settings))
self.ui.iconGridSpinBox.setValue(self.icon_grid_size(self.settings))
self._load_syntax_styles()
self._update_remove_button()
def _load_syntax_styles(self) -> None:
table = self.ui.syntaxStylesTable
table.setRowCount(len(HIGHLIGHT_STYLES))
table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
for row, (category, defaults) in enumerate(HIGHLIGHT_STYLES.items()):
label, default_color, default_bold, default_italic = defaults
name_item = QTableWidgetItem(label)
name_item.setData(Qt.ItemDataRole.UserRole, category)
name_item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
table.setItem(row, 0, name_item)
color = str(self.settings.value(f"syntax/{category}/color", default_color))
color_item = QTableWidgetItem(color)
color_item.setBackground(QColor(color))
color_item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable)
table.setItem(row, 1, color_item)
for column, key, default in (
(2, "bold", default_bold),
(3, "italic", default_italic),
):
item = QTableWidgetItem()
item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
enabled = self._as_bool(
self.settings.value(f"syntax/{category}/{key}", default)
)
item.setCheckState(
Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked
)
table.setItem(row, column, item)
def _choose_syntax_color(self, row: int, column: int) -> None:
if column != 1:
return
item = self.ui.syntaxStylesTable.item(row, column)
color = QColorDialog.getColor(QColor(item.text()), self, "Highlight colour")
if color.isValid():
item.setText(color.name())
item.setBackground(color)
@staticmethod
def _as_bool(value) -> bool:
if isinstance(value, str):
@@ -134,6 +176,24 @@ class SettingsDialog(QDialog):
self.settings.setValue("grid/graphSize", self.ui.graphGridSpinBox.value())
self.settings.setValue("grid/graphSnapSize", self.ui.graphSnapSpinBox.value())
self.settings.setValue("grid/iconSize", self.ui.iconGridSpinBox.value())
for row in range(self.ui.syntaxStylesTable.rowCount()):
category = self.ui.syntaxStylesTable.item(row, 0).data(
Qt.ItemDataRole.UserRole
)
self.settings.setValue(
f"syntax/{category}/color",
self.ui.syntaxStylesTable.item(row, 1).text(),
)
self.settings.setValue(
f"syntax/{category}/bold",
self.ui.syntaxStylesTable.item(row, 2).checkState()
== Qt.CheckState.Checked,
)
self.settings.setValue(
f"syntax/{category}/italic",
self.ui.syntaxStylesTable.item(row, 3).checkState()
== Qt.CheckState.Checked,
)
self.settings.sync()
self.settingsChanged.emit()
super().accept()

View File

@@ -0,0 +1,252 @@
import json
from importlib.resources import files
from PySide6.QtCore import QRegularExpression, QStringListModel, Qt
from PySide6.QtGui import (
QColor,
QFont,
QKeyEvent,
QSyntaxHighlighter,
QTextCharFormat,
QTextCursor,
)
from PySide6.QtWidgets import QCompleter, QPlainTextEdit
from bedit.gui.preferences import application_settings
HIGHLIGHT_STYLES = {
"keywords": ("Keywords", "#7c3aed", True, False),
"types": ("Types", "#0369a1", True, False),
"builtins": ("Built-ins", "#0f766e", False, False),
"bevalues": ("BEvalues", "#c026d3", True, False),
"inputs": ("Inputs", "#1d4ed8", False, False),
"outputs": ("Outputs", "#be123c", False, False),
"parameters": ("Parameters", "#a16207", False, False),
"numbers": ("Numbers", "#b45309", False, False),
"strings": ("Strings", "#15803d", False, False),
"comments": ("Comments", "#6b7280", False, True),
}
def load_openmodelica_syntax() -> dict[str, list[str]]:
resource = files("bedit").joinpath("data/syntax/openmodelica.json")
with resource.open(encoding="utf-8") as file:
values = json.load(file)
return {
category: [str(word) for word in values.get(category, [])]
for category in ("keywords", "types", "builtins", "bevalues")
}
def _format(color: str, *, bold: bool = False, italic: bool = False) -> QTextCharFormat:
value = QTextCharFormat()
value.setForeground(QColor(color))
value.setFontWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal)
value.setFontItalic(italic)
return value
def _as_bool(value) -> bool:
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
def highlighting_style(category: str) -> tuple[str, bool, bool]:
_label, default_color, default_bold, default_italic = HIGHLIGHT_STYLES[category]
settings = application_settings()
prefix = f"syntax/{category}"
return (
str(settings.value(f"{prefix}/color", default_color)),
_as_bool(settings.value(f"{prefix}/bold", default_bold)),
_as_bool(settings.value(f"{prefix}/italic", default_italic)),
)
class OpenModelicaHighlighter(QSyntaxHighlighter):
"""Syntax highlighter driven by the editable OpenModelica word list."""
def __init__(
self,
document,
syntax: dict[str, list[str]],
symbols: dict[str, list[str]],
) -> None:
super().__init__(document)
self.rules: list[tuple[QRegularExpression, QTextCharFormat]] = []
for category in (
"keywords",
"types",
"builtins",
"inputs",
"outputs",
"parameters",
):
words = syntax.get(category, symbols.get(category, []))
if words:
pattern = r"\b(?:" + "|".join(map(QRegularExpression.escape, words)) + r")\b"
color, bold, italic = highlighting_style(category)
self.rules.append(
(QRegularExpression(pattern), _format(color, bold=bold, italic=italic))
)
bevalue_style = highlighting_style("bevalues")
number_style = highlighting_style("numbers")
string_style = highlighting_style("strings")
self.rules.extend(
[
(
QRegularExpression(r"\$[A-Za-z_][A-Za-z0-9_]*\$"),
_format(
bevalue_style[0],
bold=bevalue_style[1],
italic=bevalue_style[2],
),
),
(
QRegularExpression(r"\b(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\b"),
_format(number_style[0], bold=number_style[1], italic=number_style[2]),
),
(
QRegularExpression(r'"(?:\\.|[^"\\])*"'),
_format(string_style[0], bold=string_style[1], italic=string_style[2]),
),
]
)
comment_style = highlighting_style("comments")
self.comment_format = _format(
comment_style[0], bold=comment_style[1], italic=comment_style[2]
)
self.rules.append((QRegularExpression(r"//.*$"), self.comment_format))
self.comment_start = QRegularExpression(r"/\*")
self.comment_end = QRegularExpression(r"\*/")
def highlightBlock(self, text: str) -> None: # noqa: N802
for expression, text_format in self.rules:
match = expression.globalMatch(text)
while match.hasNext():
result = match.next()
self.setFormat(result.capturedStart(), result.capturedLength(), text_format)
self.setCurrentBlockState(0)
start = (
0
if self.previousBlockState() == 1
else self.comment_start.match(text).capturedStart()
)
while start >= 0:
end_match = self.comment_end.match(text, start + 2)
if end_match.hasMatch():
length = end_match.capturedEnd() - start
else:
self.setCurrentBlockState(1)
length = len(text) - start
self.setFormat(start, length, self.comment_format)
if not end_match.hasMatch():
break
start = self.comment_start.match(text, start + length).capturedStart()
class OpenModelicaEditor(QPlainTextEdit):
"""OpenModelica text editor with syntax highlighting and completion."""
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.setPlaceholderText("Enter OpenModelica equations here…")
self.syntax = load_openmodelica_syntax()
self.symbols = {"inputs": [], "outputs": [], "parameters": []}
self.highlighter = OpenModelicaHighlighter(
self.document(), self.syntax, self.symbols
)
self.completer = QCompleter(self)
self.completer.setWidget(self)
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
self.completer.activated.connect(self._insert_completion)
self._rebuild_completions()
def set_symbols(
self,
inputs: list[str],
outputs: list[str],
parameters: list[str],
) -> None:
self.symbols = {
"inputs": [name for name in inputs if name],
"outputs": [name for name in outputs if name],
"parameters": [name for name in parameters if name],
}
self.reload_highlighting()
def reload_highlighting(self) -> None:
self.highlighter.setDocument(None)
self.highlighter = OpenModelicaHighlighter(
self.document(), self.syntax, self.symbols
)
self._rebuild_completions()
def _rebuild_completions(self) -> None:
words = sorted(
{
*self.syntax["keywords"],
*self.syntax["types"],
*self.syntax["builtins"],
*(f"${name.strip('$')}$" for name in self.syntax["bevalues"]),
*self.symbols["inputs"],
*self.symbols["outputs"],
*self.symbols["parameters"],
},
key=str.casefold,
)
self.completer.setModel(QStringListModel(words, self.completer))
def _completion_prefix(self) -> str:
cursor = self.textCursor()
text = cursor.block().text()[: cursor.positionInBlock()]
index = len(text)
while index > 0 and (text[index - 1].isalnum() or text[index - 1] in "_$"):
index -= 1
return text[index:]
def _insert_completion(self, completion: str) -> None:
prefix = self._completion_prefix()
cursor = self.textCursor()
cursor.movePosition(
QTextCursor.MoveOperation.Left,
QTextCursor.MoveMode.KeepAnchor,
len(prefix),
)
cursor.insertText(completion)
self.setTextCursor(cursor)
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
popup = self.completer.popup()
if popup.isVisible() and event.key() in {
Qt.Key.Key_Enter,
Qt.Key.Key_Return,
Qt.Key.Key_Escape,
Qt.Key.Key_Tab,
Qt.Key.Key_Backtab,
}:
event.ignore()
return
explicit = (
event.modifiers() == Qt.KeyboardModifier.ControlModifier
and event.key() == Qt.Key.Key_Space
)
if not explicit:
super().keyPressEvent(event)
prefix = self._completion_prefix()
if not explicit and (len(prefix) < 2 or event.text() == ""):
popup.hide()
return
self.completer.setCompletionPrefix(prefix)
if self.completer.completionCount() == 0:
popup.hide()
return
rectangle = self.cursorRect()
rectangle.setWidth(
popup.sizeHintForColumn(0) + popup.verticalScrollBar().sizeHint().width()
)
self.completer.complete(rectangle)

View File

@@ -34,8 +34,8 @@ class TextDefinitionEditor(QWidget):
self.ui.columnSplitter.setSizes([560, 340])
self.ui.definitionSplitter.setSizes([300, 300])
self.ui.equationsEdit.textChanged.connect(self._mark_modified)
self.ui.portsTable.cellChanged.connect(self._mark_modified)
self.ui.parametersTable.cellChanged.connect(self._mark_modified)
self.ui.portsTable.cellChanged.connect(self._symbols_modified)
self.ui.parametersTable.cellChanged.connect(self._symbols_modified)
self.ui.addPortButton.clicked.connect(self.add_port)
self.ui.removePortButton.clicked.connect(self.remove_port)
self.ui.addParameterButton.clicked.connect(self.add_parameter)
@@ -102,6 +102,11 @@ class TextDefinitionEditor(QWidget):
self.ui.parametersTable.setRowCount(0)
for parameter in parameters:
self._append_parameter(parameter)
self.ui.equationsEdit.set_symbols(
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in parameters],
)
self._loading = False
self.set_modified(False)
self._update_buttons()
@@ -116,12 +121,25 @@ class TextDefinitionEditor(QWidget):
self.set_modified(True)
self.definitionEdited.emit()
def _symbols_modified(self, *_args) -> None:
if not self._loading:
self._refresh_editor_symbols()
self._mark_modified()
def _refresh_editor_symbols(self) -> None:
inputs, outputs = self.ports
self.ui.equationsEdit.set_symbols(
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in self.parameters],
)
def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
combo = QComboBox(self)
for label, value in values:
combo.addItem(label, value)
combo.setCurrentIndex(max(0, combo.findData(current)))
combo.currentIndexChanged.connect(self._mark_modified)
combo.currentIndexChanged.connect(self._symbols_modified)
return combo
def _append_port(self, port: Port, orientation: str) -> None:
@@ -141,7 +159,7 @@ class TextDefinitionEditor(QWidget):
)
multiple = QCheckBox("Any", self)
multiple.setChecked(port.allows_multiple_connections)
multiple.toggled.connect(self._mark_modified)
multiple.toggled.connect(self._symbols_modified)
table.setCellWidget(row, 3, multiple)
def _append_parameter(self, parameter: Parameter) -> None:
@@ -165,13 +183,13 @@ class TextDefinitionEditor(QWidget):
self._append_port(port, "input")
self._loading = False
self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1)
self._mark_modified()
self._symbols_modified()
def remove_port(self) -> None:
row = self.ui.portsTable.currentRow()
if row >= 0:
self.ui.portsTable.removeRow(row)
self._mark_modified()
self._symbols_modified()
self._update_buttons()
def add_parameter(self) -> None:
@@ -183,13 +201,13 @@ class TextDefinitionEditor(QWidget):
self._append_parameter(parameter)
self._loading = False
self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1)
self._mark_modified()
self._symbols_modified()
def remove_parameter(self) -> None:
row = self.ui.parametersTable.currentRow()
if row >= 0:
self.ui.parametersTable.removeRow(row)
self._mark_modified()
self._symbols_modified()
self._update_buttons()
def _update_buttons(self) -> None:

View File

@@ -73,6 +73,8 @@ class Ui_MainWindow(object):
self.actionOpen.setIcon(icon7)
self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionReloadSimulation = QAction(MainWindow)
self.actionReloadSimulation.setObjectName(u"actionReloadSimulation")
self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave")
icon8 = QIcon()
@@ -397,6 +399,7 @@ class Ui_MainWindow(object):
self.menuFile.addAction(self.actionNew)
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionReloadLibraries)
self.menuFile.addAction(self.actionReloadSimulation)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
@@ -450,6 +453,9 @@ class Ui_MainWindow(object):
#if QT_CONFIG(statustip)
self.actionCompile.setStatusTip(QCoreApplication.translate("MainWindow", u"Compile the active graph for simulation", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionCompile.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut)
self.actionNew.setText(QCoreApplication.translate("MainWindow", u"&New", None))
#if QT_CONFIG(statustip)
self.actionNew.setStatusTip(QCoreApplication.translate("MainWindow", u"Create a new document", None))
@@ -488,7 +494,14 @@ class Ui_MainWindow(object):
self.actionReloadLibraries.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload configured library files from disk", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"Shift+F5", None))
#endif // QT_CONFIG(shortcut)
self.actionReloadSimulation.setText(QCoreApplication.translate("MainWindow", u"Reload &Simulation Code", None))
#if QT_CONFIG(statustip)
self.actionReloadSimulation.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload the simulation package while preserving runtime state", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionReloadSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+F5", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
#if QT_CONFIG(statustip)

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'parameter_options_dialog.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QFormLayout, QHBoxLayout, QLabel, QLineEdit,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
QSplitter, QVBoxLayout, QWidget)
class Ui_ParameterOptionsDialog(object):
def setupUi(self, ParameterOptionsDialog):
if not ParameterOptionsDialog.objectName():
ParameterOptionsDialog.setObjectName(u"ParameterOptionsDialog")
ParameterOptionsDialog.resize(620, 380)
self.dialogLayout = QVBoxLayout(ParameterOptionsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.parameterSplitter = QSplitter(ParameterOptionsDialog)
self.parameterSplitter.setObjectName(u"parameterSplitter")
self.parameterSplitter.setOrientation(Qt.Orientation.Horizontal)
self.parameterListPanel = QWidget(self.parameterSplitter)
self.parameterListPanel.setObjectName(u"parameterListPanel")
self.parameterListLayout = QVBoxLayout(self.parameterListPanel)
self.parameterListLayout.setObjectName(u"parameterListLayout")
self.parameterListLayout.setContentsMargins(0, 0, 0, 0)
self.parameterList = QListWidget(self.parameterListPanel)
self.parameterList.setObjectName(u"parameterList")
self.parameterListLayout.addWidget(self.parameterList)
self.parameterButtonsLayout = QHBoxLayout()
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
self.addParameterButton = QPushButton(self.parameterListPanel)
self.addParameterButton.setObjectName(u"addParameterButton")
self.parameterButtonsLayout.addWidget(self.addParameterButton)
self.removeParameterButton = QPushButton(self.parameterListPanel)
self.removeParameterButton.setObjectName(u"removeParameterButton")
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
self.parameterListLayout.addLayout(self.parameterButtonsLayout)
self.parameterSplitter.addWidget(self.parameterListPanel)
self.parameterDetailsPanel = QWidget(self.parameterSplitter)
self.parameterDetailsPanel.setObjectName(u"parameterDetailsPanel")
self.parameterDetailsForm = QFormLayout(self.parameterDetailsPanel)
self.parameterDetailsForm.setObjectName(u"parameterDetailsForm")
self.parameterDetailsForm.setContentsMargins(0, 0, 0, 0)
self.nameLabel = QLabel(self.parameterDetailsPanel)
self.nameLabel.setObjectName(u"nameLabel")
self.parameterDetailsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
self.nameEdit = QLineEdit(self.parameterDetailsPanel)
self.nameEdit.setObjectName(u"nameEdit")
self.parameterDetailsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
self.typeLabel = QLabel(self.parameterDetailsPanel)
self.typeLabel.setObjectName(u"typeLabel")
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
self.typeEdit = QLineEdit(self.parameterDetailsPanel)
self.typeEdit.setObjectName(u"typeEdit")
self.parameterDetailsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.typeEdit)
self.valueLabel = QLabel(self.parameterDetailsPanel)
self.valueLabel.setObjectName(u"valueLabel")
self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.valueLabel)
self.valueEdit = QLineEdit(self.parameterDetailsPanel)
self.valueEdit.setObjectName(u"valueEdit")
self.parameterDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.valueEdit)
self.parameterSplitter.addWidget(self.parameterDetailsPanel)
self.dialogLayout.addWidget(self.parameterSplitter)
self.buttonBox = QDialogButtonBox(ParameterOptionsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(ParameterOptionsDialog)
self.buttonBox.accepted.connect(ParameterOptionsDialog.accept)
self.buttonBox.rejected.connect(ParameterOptionsDialog.reject)
QMetaObject.connectSlotsByName(ParameterOptionsDialog)
# setupUi
def retranslateUi(self, ParameterOptionsDialog):
ParameterOptionsDialog.setWindowTitle(QCoreApplication.translate("ParameterOptionsDialog", u"Parameter Options", None))
self.addParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Add Parameter", None))
self.removeParameterButton.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Remove Parameter", None))
self.nameLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Name:", None))
self.typeLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Type:", None))
self.valueLabel.setText(QCoreApplication.translate("ParameterOptionsDialog", u"Value:", None))
# retranslateUi

View File

@@ -15,17 +15,18 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QFormLayout, QGroupBox, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
QSpacerItem, QSpinBox, QTabWidget, QVBoxLayout,
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QListWidget, QListWidgetItem,
QPushButton, QSizePolicy, QSpacerItem, QSpinBox,
QTabWidget, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
class Ui_SettingsDialog(object):
def setupUi(self, SettingsDialog):
if not SettingsDialog.objectName():
SettingsDialog.setObjectName(u"SettingsDialog")
SettingsDialog.resize(480, 420)
SettingsDialog.resize(480, 520)
SettingsDialog.setModal(True)
self.dialogLayout = QVBoxLayout(SettingsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
@@ -103,6 +104,34 @@ class Ui_SettingsDialog(object):
self.generalLayout.addItem(self.generalSpacer)
self.settingsTabs.addTab(self.generalTab, "")
self.syntaxTab = QWidget()
self.syntaxTab.setObjectName(u"syntaxTab")
self.syntaxTabLayout = QVBoxLayout(self.syntaxTab)
self.syntaxTabLayout.setObjectName(u"syntaxTabLayout")
self.syntaxHintLabel = QLabel(self.syntaxTab)
self.syntaxHintLabel.setObjectName(u"syntaxHintLabel")
self.syntaxHintLabel.setWordWrap(True)
self.syntaxTabLayout.addWidget(self.syntaxHintLabel)
self.syntaxStylesTable = QTableWidget(self.syntaxTab)
if (self.syntaxStylesTable.columnCount() < 4):
self.syntaxStylesTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.syntaxStylesTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
self.syntaxStylesTable.setObjectName(u"syntaxStylesTable")
self.syntaxStylesTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.syntaxStylesTable.setColumnCount(4)
self.syntaxTabLayout.addWidget(self.syntaxStylesTable)
self.settingsTabs.addTab(self.syntaxTab, "")
self.librariesTab = QWidget()
self.librariesTab.setObjectName(u"librariesTab")
self.librariesTabLayout = QVBoxLayout(self.librariesTab)
@@ -176,6 +205,16 @@ class Ui_SettingsDialog(object):
self.iconGridLabel.setText(QCoreApplication.translate("SettingsDialog", u"Icon grid size:", None))
self.iconGridSpinBox.setSuffix(QCoreApplication.translate("SettingsDialog", u" units", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.generalTab), QCoreApplication.translate("SettingsDialog", u"General", None))
self.syntaxHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Double-click a colour cell to choose a colour. Word lists remain editable in data/syntax/openmodelica.json.", None))
___qtablewidgetitem = self.syntaxStylesTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("SettingsDialog", u"Expression type", None))
___qtablewidgetitem1 = self.syntaxStylesTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("SettingsDialog", u"Colour", None))
___qtablewidgetitem2 = self.syntaxStylesTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("SettingsDialog", u"Bold", None))
___qtablewidgetitem3 = self.syntaxStylesTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("SettingsDialog", u"Italic", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.syntaxTab), QCoreApplication.translate("SettingsDialog", u"Text highlighting", None))
self.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON or BEdit Binary files from these files or folders at startup:", None))
self.addLibraryFileButton.setText(QCoreApplication.translate("SettingsDialog", u"Add File\u2026", None))
self.addLibraryFolderButton.setText(QCoreApplication.translate("SettingsDialog", u"Add Folder\u2026", None))

View File

@@ -16,9 +16,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
QHeaderView, QPlainTextEdit, QPushButton, QSizePolicy,
QSpacerItem, QSplitter, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget)
QHeaderView, QPushButton, QSizePolicy, QSpacerItem,
QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
from bedit.gui.editors.openmodelica import OpenModelicaEditor
class Ui_TextDefinitionEditor(object):
def setupUi(self, TextDefinitionEditor):
@@ -36,9 +38,8 @@ class Ui_TextDefinitionEditor(object):
self.equationsGroup.setObjectName(u"equationsGroup")
self.equationsLayout = QVBoxLayout(self.equationsGroup)
self.equationsLayout.setObjectName(u"equationsLayout")
self.equationsEdit = QPlainTextEdit(self.equationsGroup)
self.equationsEdit = OpenModelicaEditor(self.equationsGroup)
self.equationsEdit.setObjectName(u"equationsEdit")
self.equationsEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.equationsLayout.addWidget(self.equationsEdit)
@@ -141,7 +142,6 @@ class Ui_TextDefinitionEditor(object):
def retranslateUi(self, TextDefinitionEditor):
self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
self.equationsEdit.setPlaceholderText(QCoreApplication.translate("TextDefinitionEditor", u"Enter equations here\u2026", None))
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))

View File

@@ -252,6 +252,7 @@ class ComponentGraphicsItem(QGraphicsObject):
menu = QMenu()
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(event.screenPos())
if selected is options_action:
scene = self.scene()
@@ -261,6 +262,10 @@ class ComponentGraphicsItem(QGraphicsObject):
scene = self.scene()
if isinstance(scene, GraphScene):
scene.componentPortOptionsRequested.emit(self.component_id)
elif selected is parameters_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.componentParameterOptionsRequested.emit(self.component_id)
event.accept()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
@@ -832,6 +837,7 @@ class LineAnnotationGraphicsItem(QGraphicsPathItem):
class GraphScene(QGraphicsScene):
componentOptionsRequested = Signal(str)
componentPortOptionsRequested = Signal(str)
componentParameterOptionsRequested = Signal(str)
portOptionsRequested = Signal(str, str)
connectionOptionsRequested = Signal(str)
@@ -1694,6 +1700,7 @@ class GraphWorkspaceView(QGraphicsView):
toolModeShortcutRequested = Signal(str)
componentOptionsRequested = Signal(str)
componentPortOptionsRequested = Signal(str)
componentParameterOptionsRequested = Signal(str)
portOptionsRequested = Signal(str, str)
connectionOptionsRequested = Signal(str)
selectionAvailabilityChanged = Signal(bool)
@@ -1779,6 +1786,9 @@ class GraphWorkspaceView(QGraphicsView):
scene = GraphScene(controller, self)
scene.componentOptionsRequested.connect(self.componentOptionsRequested)
scene.componentPortOptionsRequested.connect(self.componentPortOptionsRequested)
scene.componentParameterOptionsRequested.connect(
self.componentParameterOptionsRequested
)
scene.portOptionsRequested.connect(self.portOptionsRequested)
scene.connectionOptionsRequested.connect(self.connectionOptionsRequested)
scene.selectionChanged.connect(

View File

@@ -12,7 +12,7 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component, Parameter
from bedit.core.model import Component
from bedit.core.application_log import get_logger
from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation
@@ -29,8 +29,10 @@ from bedit.gui.models.library_tree import (
)
from bedit.gui.dialogs.settings import SettingsDialog
from bedit.gui.dialogs.port_options import PortOptionsDialog
from bedit.gui.dialogs.parameter_options import ParameterOptionsDialog
from bedit.gui.dialogs.simulation_settings import SimulationSettingsDialog
from bedit.gui.preferences import application_settings
from bedit.gui.simulation_reload import reload_simulation
from bedit.gui.generated.ui_main_window import Ui_MainWindow
from bedit.gui.application_log import ApplicationLogHandler
@@ -99,6 +101,9 @@ class MainWindow(QMainWindow):
self.ui.graphView.set_model(self.document_controller)
self.ui.graphView.componentOptionsRequested.connect(self.show_component_options)
self.ui.graphView.componentPortOptionsRequested.connect(self.show_component_port_options)
self.ui.graphView.componentParameterOptionsRequested.connect(
self.show_component_parameter_options
)
self.ui.graphView.portOptionsRequested.connect(self.show_port_options)
self.ui.graphView.connectionOptionsRequested.connect(self.show_connection_options)
self.ui.graphView.selectionAvailabilityChanged.connect(
@@ -132,6 +137,7 @@ class MainWindow(QMainWindow):
self.ui.actionNew.triggered.connect(self.new_document)
self.ui.actionOpen.triggered.connect(self.open_document)
self.ui.actionReloadLibraries.triggered.connect(self.reload_libraries)
self.ui.actionReloadSimulation.triggered.connect(self.reload_simulation_code)
self.ui.actionSave.triggered.connect(self.save_document)
self.ui.actionSaveAs.triggered.connect(self.save_document_as)
self.ui.actionClose.triggered.connect(self.close_document)
@@ -191,6 +197,18 @@ class MainWindow(QMainWindow):
"\n".join(self.libraries.load_warnings),
)
@Slot()
def reload_simulation_code(self) -> None:
try:
replacement = reload_simulation(self.simulation)
except Exception as error:
self.log.exception("Could not reload simulation code")
QMessageBox.critical(self, "Could not reload simulation code", str(error))
return
self.simulation = replacement
self.document_controller.simulation = replacement
self.log.info("Reloaded simulation code")
@Slot()
def show_simulation_settings(self) -> None:
component = self.document_controller.active_component
@@ -314,10 +332,7 @@ class MainWindow(QMainWindow):
component.source.get("equations", ""),
component.inputs,
component.outputs,
[
Parameter.from_dict(parameter)
for parameter in component.source.get("parameters", [])
],
component.parameters,
)
def _resolve_source_edits(self) -> bool:
@@ -475,6 +490,7 @@ class MainWindow(QMainWindow):
if scene is not None:
scene.update()
self.ui.graphView.viewport().update()
self.ui.textDefinitionEditor.ui.equationsEdit.reload_highlighting()
def _document_opened_changed(self, opened: bool) -> None:
for action in (self.ui.actionClose, self.ui.actionSave, self.ui.actionSaveAs):
@@ -509,6 +525,7 @@ class MainWindow(QMainWindow):
menu.addSeparator()
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if graph_action is not None and selected is graph_action:
@@ -519,6 +536,8 @@ class MainWindow(QMainWindow):
self.show_component_options(component_id)
elif selected is ports_action:
self.show_component_port_options(component_id)
elif selected is parameters_action:
self.show_component_parameter_options(component_id)
elif selected is delete_action:
answer = QMessageBox.question(
self,
@@ -549,7 +568,9 @@ class MainWindow(QMainWindow):
return
menu = QMenu(self)
ports_action = menu.addAction("Port Options…")
if menu.exec(tree.viewport().mapToGlobal(position)) is ports_action:
parameters_action = menu.addAction("Parameter Options…")
selected = menu.exec(tree.viewport().mapToGlobal(position))
if selected is ports_action:
dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
@@ -572,6 +593,32 @@ class MainWindow(QMainWindow):
self.log.error("Could not change library ports: %s", error)
QMessageBox.warning(self, "Cannot change library ports", str(error))
self.library_tree_model.rebuild()
elif selected is parameters_action:
dialog = ParameterOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_parameters = deepcopy(component.parameters)
component.parameters = dialog.parameters
library = next(
(
library
for library in self.libraries.libraries
if any(item is component for item in library.document.all_components())
),
None,
)
try:
if library is not None:
library.document.validate()
DocumentSerializer.save(
library.document, Path(library.source_path)
)
except (OSError, ValueError) as error:
component.parameters = old_parameters
self.log.error("Could not change library parameters: %s", error)
QMessageBox.warning(
self, "Cannot change library parameters", str(error)
)
self.library_tree_model.rebuild()
@Slot(str)
def show_component_port_options(self, component_id: str) -> None:
@@ -590,6 +637,23 @@ class MainWindow(QMainWindow):
self.log.error("Could not change component ports: %s", error)
QMessageBox.warning(self, "Cannot change ports", str(error))
@Slot(str)
def show_component_parameter_options(self, component_id: str) -> None:
document = self.document_controller.document
component = document.find_component(component_id) if document else None
if component is None:
return
dialog = ParameterOptionsDialog(component, self)
if dialog.exec() != dialog.DialogCode.Accepted:
return
try:
self.document_controller.edit_component_parameters(
component_id, dialog.parameters
)
except ValueError as error:
self.log.error("Could not change component parameters: %s", error)
QMessageBox.warning(self, "Cannot change parameters", str(error))
@Slot(str)
def show_component_options(self, component_id: str) -> None:
document = self.document_controller.document

View File

@@ -0,0 +1,33 @@
from copy import deepcopy
import importlib
import pkgutil
from types import ModuleType
SIMULATION_PACKAGE = "bedit.core.simulation"
def _saved_instance_state(instance) -> dict:
state = {}
for name, value in vars(instance).items():
try:
state[name] = deepcopy(value)
except Exception:
state[name] = value
return state
def reload_simulation(current):
"""Reload the simulation package and return a fresh state-preserving instance."""
saved_state = _saved_instance_state(current)
importlib.invalidate_caches()
package = importlib.import_module(SIMULATION_PACKAGE)
discovered: list[ModuleType] = []
for module_info in pkgutil.walk_packages(package.__path__, f"{SIMULATION_PACKAGE}."):
discovered.append(importlib.import_module(module_info.name))
for module in sorted(discovered, key=lambda item: item.__name__.count("."), reverse=True):
importlib.reload(module)
package = importlib.reload(package)
replacement = package.Simulation()
vars(replacement).update(saved_state)
return replacement

View File

@@ -365,81 +365,112 @@
</item>
<item>
<widget class="QSplitter" name="workspaceVerticalSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QStackedWidget" name="workspaceStack">
<property name="currentIndex">
<number>0</number>
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QWidget" name="graphPage">
<layout class="QVBoxLayout" name="graphPageLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="GraphWorkspaceView" name="graphView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="textPage">
<layout class="QVBoxLayout" name="textPageLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="TextDefinitionEditor" name="textDefinitionEditor" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="emptyPage">
<property name="styleSheet">
<string notr="true">background-color: #9a9a9a;</string>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QStackedWidget" name="workspaceStack">
<property name="currentIndex">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="emptyPageLayout">
<item>
<widget class="QLabel" name="emptyWorkspaceLabel">
<property name="styleSheet">
<string notr="true">background: transparent; color: #202020;</string>
</property>
<property name="text">
<string>No document open</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="graphPage">
<layout class="QVBoxLayout" name="graphPageLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="GraphWorkspaceView" name="graphView"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="textPage">
<layout class="QVBoxLayout" name="textPageLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="TextDefinitionEditor" name="textDefinitionEditor" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="emptyPage">
<property name="styleSheet">
<string notr="true">background-color: #9a9a9a;</string>
</property>
<layout class="QVBoxLayout" name="emptyPageLayout">
<item>
<widget class="QLabel" name="emptyWorkspaceLabel">
<property name="styleSheet">
<string notr="true">background: transparent; color: #202020;</string>
</property>
<property name="text">
<string>No document open</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QTabWidget" name="outputTabs">
<property name="minimumSize"><size><width>0</width><height>100</height></size></property>
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<widget class="QWidget" name="logTab">
<attribute name="title"><string>Log</string></attribute>
<attribute name="title">
<string>Log</string>
</attribute>
<layout class="QVBoxLayout" name="logTabLayout">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<item><widget class="QPlainTextEdit" name="logOutput"><property name="readOnly"><bool>true</bool></property><property name="maximumBlockCount"><number>5000</number></property><property name="placeholderText"><string>Application messages appear here.</string></property></widget></item>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPlainTextEdit" name="logOutput">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="maximumBlockCount">
<number>5000</number>
</property>
<property name="placeholderText">
<string>Application messages appear here.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
@@ -467,6 +498,7 @@
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionReloadLibraries"/>
<addaction name="actionReloadSimulation"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
@@ -626,6 +658,9 @@
<property name="statusTip">
<string>Compile the active graph for simulation</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
</action>
<action name="actionNew">
<property name="icon">
@@ -716,7 +751,18 @@
<string>Reload configured library files from disk</string>
</property>
<property name="shortcut">
<string>F5</string>
<string>Shift+F5</string>
</property>
</action>
<action name="actionReloadSimulation">
<property name="text">
<string>Reload &amp;Simulation Code</string>
</property>
<property name="statusTip">
<string>Reload the simulation package while preserving runtime state</string>
</property>
<property name="shortcut">
<string>Ctrl+F5</string>
</property>
</action>
<action name="actionSave">

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ParameterOptionsDialog</class>
<widget class="QDialog" name="ParameterOptionsDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>620</width><height>380</height></rect></property>
<property name="windowTitle"><string>Parameter Options</string></property>
<layout class="QVBoxLayout" name="dialogLayout">
<item><widget class="QSplitter" name="parameterSplitter"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><widget class="QWidget" name="parameterListPanel"><layout class="QVBoxLayout" name="parameterListLayout"><item><widget class="QListWidget" name="parameterList"/></item><item><layout class="QHBoxLayout" name="parameterButtonsLayout"><item><widget class="QPushButton" name="addParameterButton"><property name="text"><string>Add Parameter</string></property></widget></item><item><widget class="QPushButton" name="removeParameterButton"><property name="text"><string>Remove Parameter</string></property></widget></item></layout></item></layout></widget><widget class="QWidget" name="parameterDetailsPanel"><layout class="QFormLayout" name="parameterDetailsForm"><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"><widget class="QLineEdit" name="typeEdit"/></item><item row="2" column="0"><widget class="QLabel" name="valueLabel"><property name="text"><string>Value:</string></property></widget></item><item row="2" column="1"><widget class="QLineEdit" name="valueEdit"/></item></layout></widget></widget></item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
</layout>
</widget>
<resources/>
<connections><connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>ParameterOptionsDialog</receiver><slot>accept()</slot><hints/></connection><connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ParameterOptionsDialog</receiver><slot>reject()</slot><hints/></connection></connections>
</ui>

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>480</width>
<height>420</height>
<height>520</height>
</rect>
</property>
<property name="windowTitle">
@@ -86,6 +86,13 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="syntaxTab">
<attribute name="title"><string>Text highlighting</string></attribute>
<layout class="QVBoxLayout" name="syntaxTabLayout">
<item><widget class="QLabel" name="syntaxHintLabel"><property name="text"><string>Double-click a colour cell to choose a colour. Word lists remain editable in data/syntax/openmodelica.json.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
<item><widget class="QTableWidget" name="syntaxStylesTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="columnCount"><number>4</number></property><column><property name="text"><string>Expression type</string></property></column><column><property name="text"><string>Colour</string></property></column><column><property name="text"><string>Bold</string></property></column><column><property name="text"><string>Italic</string></property></column></widget></item>
</layout>
</widget>
<widget class="QWidget" name="librariesTab">
<attribute name="title">
<string>Libraries</string>

View File

@@ -15,7 +15,7 @@
<widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="QPlainTextEdit" name="equationsEdit"><property name="lineWrapMode"><enum>QPlainTextEdit::LineWrapMode::NoWrap</enum></property><property name="placeholderText"><string>Enter equations here…</string></property></widget></item>
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
</layout>
</widget>
<widget class="QSplitter" name="definitionSplitter">
@@ -40,6 +40,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>OpenModelicaEditor</class>
<extends>QPlainTextEdit</extends>
<header>bedit.gui.editors.openmodelica</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -6,51 +6,18 @@
},
"roots": [
{
"id": "57fe8127-ea9d-4a8b-8d09-da430ffbbd92",
"name": "PID",
"id": "6060fdd6-e816-4da9-8bb6-fde1a66d3e92",
"name": "Test",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-1342716d",
"name": "in",
"position": {
"x": -384.0,
"y": -112.0
},
"properties": {
"iconPosition": {
"x": 0.0,
"y": 0.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-c75d0f16",
"name": "out",
"position": {
"x": 320.0,
"y": -112.0
},
"properties": {
"iconPosition": {
"x": 0.0,
"y": 0.0
}
},
"type": "signal",
"multipleConnections": false
}
]
"inputs": [],
"outputs": []
},
"parameters": [],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
@@ -89,17 +56,124 @@
"showName": false
},
"library": {
"showSubtree": false
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [
{
"id": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"name": "g_D",
"id": "2e6a192c-5aeb-45d6-80ee-d64a72392b76",
"name": "Constant0",
"position": {
"x": -128.0,
"x": -160.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": [
{
"id": "port-ddc97277",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"parameters": [
{
"id": "parameter-022177fc",
"name": "v",
"type": "real",
"value": "1"
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"fill": "none",
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"type": "line",
"width": 0.0,
"x": 40.0,
"y": 40.0
},
{
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 88.0
},
{
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.0,
"stroke": "#ffaa00",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 64.0
}
]
},
"properties": {
"showName": true
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "y = v;"
}
}
},
{
"id": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0",
"name": "gain0",
"position": {
"x": -32.0,
"y": -160.0
},
"rotation": 0.0,
@@ -107,7 +181,7 @@
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"name": "u",
"position": {
"x": 0.0,
"y": 0.0
@@ -125,7 +199,7 @@
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
@@ -141,6 +215,14 @@
}
]
},
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "2.5"
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
@@ -188,49 +270,24 @@
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
"equations": "y = k*u;"
}
}
},
{
"id": "478e7ff8-baa1-467f-8707-e375b99395f6",
"name": "differentiate0",
"id": "0b29c706-eb1b-4839-8ad2-bbed64efc01e",
"name": "const_and_time",
"position": {
"x": 0.0,
"y": -160.0
"x": -160.0,
"y": -32.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-1cbabc8f",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"inputs": [],
"outputs": [
{
"id": "port-4eeea4e7",
"name": "out",
"id": "port-ddc97277",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
@@ -246,6 +303,14 @@
}
]
},
"parameters": [
{
"id": "parameter-022177fc",
"name": "v",
"type": "real",
"value": "4.8"
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
@@ -269,123 +334,37 @@
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 18.0,
"fill": "none",
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "d/dt",
"type": "text",
"width": 48.0,
"type": "line",
"width": 0.0,
"x": 40.0,
"y": 40.0
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "initial out = initial;\nout = der(in);",
"parameters": [
{
"id": "parameter-331e38be",
"name": "initial",
"type": "real",
"value": "0"
}
]
}
}
},
{
"id": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"name": "g_P",
"position": {
"x": -64.0,
"y": -288.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 24.0,
"height": 48.0,
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "K",
"type": "text",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 40.0
"y": 88.0
},
{
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.0,
"stroke": "#ffaa00",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 64.0
}
]
},
@@ -398,23 +377,15 @@
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
"equations": "y = v+time;"
}
}
},
{
"id": "1995074c-4580-4ca3-8691-6923aedc5833",
"name": "g_I",
"id": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf",
"name": "gain1",
"position": {
"x": -128.0,
"x": -32.0,
"y": -32.0
},
"rotation": 0.0,
@@ -422,7 +393,7 @@
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"name": "u",
"position": {
"x": 0.0,
"y": 0.0
@@ -440,7 +411,7 @@
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
@@ -456,6 +427,14 @@
}
]
},
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "-5"
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
@@ -503,31 +482,23 @@
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
"equations": "y = k*u;"
}
}
},
{
"id": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"id": "59bb51be-8185-4b0b-a894-e38db61aff18",
"name": "add0",
"position": {
"x": 128.0,
"y": -160.0
"x": 96.0,
"y": -96.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-c995845c",
"name": "in",
"name": "u",
"position": {
"x": 0.0,
"y": 0.0
@@ -545,7 +516,7 @@
"outputs": [
{
"id": "port-a2f0dea6",
"name": "out",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
@@ -561,6 +532,7 @@
}
]
},
"parameters": [],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
@@ -607,251 +579,20 @@
"implementation": {
"kind": "text",
"source": {
"equations": "out = sum(in[i] for i in 1:in.N);",
"parameters": []
}
}
},
{
"id": "48af4110-839a-45f7-a105-7e7dbc51676b",
"name": "integrate0",
"position": {
"x": 0.0,
"y": -32.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-12903207",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-b3370b1a",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 18.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "dt",
"type": "text",
"width": 28.0,
"x": 64.0,
"y": 40.0
},
{
"color": "#00007f",
"fill": "none",
"fontSize": 32.0,
"height": 54.0,
"lineStyle": "none",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "\u222b",
"type": "text",
"width": 28.0,
"x": 40.0,
"y": 32.0
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "initial out = initial;\nder(out) = in;",
"parameters": [
{
"id": "parameter-6bdc1c76",
"name": "initial",
"type": "real",
"value": "0"
}
]
"equations": "y = sum(u[i] for i in 1:$u_N$ );"
}
}
}
],
"connections": [
{
"id": "f89a8edb-b662-4a55-aa00-6d681b0bf6ae",
"id": "df23ff13-2a30-4c85-be66-56f66114d58e",
"source": {
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"port": "port-fe9e6486"
"block": "2e6a192c-5aeb-45d6-80ee-d64a72392b76",
"port": "port-ddc97277"
},
"target": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": [
{
"x": 192.0,
"y": -224.0
}
]
}
},
{
"id": "c1a2f8e2-b98f-45ee-9ef6-fad8cd42b71b",
"source": {
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
"port": "port-4eeea4e7"
},
"target": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "e889f7bd-c684-4e59-8e9d-1693bd2ba87f",
"source": {
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"port": "port-fe9e6486"
},
"target": {
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
"port": "port-1cbabc8f"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "1cebb01c-7da4-48c7-b7ea-5585f64cd524",
"source": {
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
"port": "port-fe9e6486"
},
"target": {
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
"port": "port-12903207"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "6d16540e-d902-4ebf-a6a1-dcdda0cc8458",
"source": {
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
"port": "port-b3370b1a"
},
"target": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": [
{
"x": 192.0,
"y": 32.0
}
]
}
},
{
"id": "206f1501-4509-4ba7-a28e-156e35de51ea",
"source": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-a2f0dea6"
},
"target": {
"interface": "port-c75d0f16"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "5d234bec-8754-46e8-b9e8-1c802bb9f9ba",
"source": {
"interface": "port-1342716d"
},
"target": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "33ba6f27-6f3a-4290-baac-b3cddcaa6b50",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"target": {
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"block": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0",
"port": "port-df34ce84"
},
"name": "",
@@ -860,58 +601,54 @@
}
},
{
"id": "ee4a3409-fc46-485c-b475-b456dddb93b5",
"id": "00f7d291-641b-4200-afa3-f1ec941faed7",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
"block": "0b29c706-eb1b-4839-8ad2-bbed64efc01e",
"port": "port-ddc97277"
},
"target": {
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"block": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf",
"port": "port-df34ce84"
},
"name": "",
"properties": {
"waypoints": [
{
"x": -192.0,
"y": -224.0
}
]
"waypoints": []
}
},
{
"id": "8d1a5f3c-b706-4671-873a-2121f55f748b",
"id": "7a776aab-deb5-4e50-838e-b6b4635f349c",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
"block": "f4a78255-8b73-4bb3-ae8e-24b566c72bc0",
"port": "port-fe9e6486"
},
"target": {
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
"port": "port-df34ce84"
"block": "59bb51be-8185-4b0b-a894-e38db61aff18",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": [
{
"x": -192.0,
"y": 32.0
}
]
"waypoints": []
}
},
{
"id": "5b3d71c3-282a-4c2d-85f8-b92bba5bf2df",
"source": {
"block": "43f739a0-e50f-44ae-bd95-0e0c1a10d1cf",
"port": "port-fe9e6486"
},
"target": {
"block": "59bb51be-8185-4b0b-a894-e38db61aff18",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": []
}
}
],
"annotations": [],
"junctions": [
{
"id": "7a552e5e-b15d-46ca-a891-37e5129f88d1",
"position": {
"x": -192.0,
"y": -96.0
},
"type": "signal"
}
],
"simulation": {
"enabled": true
}
"junctions": [],
"simulation": {}
}
}
}