basic openmodelica model emitting
This commit is contained in:
@@ -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,
|
||||
|
||||
326
BEdit/src/bedit/core/simulation/compiler.py
Normal file
326
BEdit/src/bedit/core/simulation/compiler.py
Normal 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}")
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
22
BEdit/src/bedit/data/syntax/openmodelica.json
Normal file
22
BEdit/src/bedit/data/syntax/openmodelica.json
Normal 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": []
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
108
BEdit/src/bedit/gui/dialogs/parameter_options.py
Normal file
108
BEdit/src/bedit/gui/dialogs/parameter_options.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
252
BEdit/src/bedit/gui/editors/openmodelica.py
Normal file
252
BEdit/src/bedit/gui/editors/openmodelica.py
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
120
BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py
Normal file
120
BEdit/src/bedit/gui/generated/ui_parameter_options_dialog.py
Normal 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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
33
BEdit/src/bedit/gui/simulation_reload.py
Normal file
33
BEdit/src/bedit/gui/simulation_reload.py
Normal 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
|
||||
Reference in New Issue
Block a user