Compare commits

...

19 Commits

Author SHA1 Message Date
48ac9e2f59 proper modelica text model editing 2026-07-21 17:16:06 +02:00
2f6487e510 Added direct graph navigation without activating Matplotlib toolbar modes:
Mouse wheel zooms in and out around the cursor.
Left-click dragging pans the graph.
Matplotlib toolbar modes still work and take precedence when activated.
The existing Home button can reset the view.
2026-07-21 15:29:58 +02:00
eb8f77ce64 better signal graphing 2026-07-21 15:24:24 +02:00
35d933ccbb Added signal graphing 2026-07-21 15:14:12 +02:00
edd7bb98f2 Saving sim results 2026-07-21 14:40:03 +02:00
ddc004dee5 sim window save/load 2026-07-21 14:01:55 +02:00
cdfc891980 Start of a sim window 2026-07-21 13:54:57 +02:00
6fb2478589 basic runner setup changed 2026-07-21 13:21:52 +02:00
dc770a0886 new runner layout, still todo 2026-07-20 22:03:43 +02:00
38e3d7aae9 Added run button and parameter dialog 2026-07-20 18:46:59 +02:00
4cfe7c3a0e Fixed om emission 2026-07-20 18:17:45 +02:00
d368b36b8c basic openmodelica model emitting 2026-07-20 17:34:31 +02:00
42bf09610b Added binary file format 2026-07-20 15:39:34 +02:00
9ed8e371ef added stub simulation entry and logs 2026-07-20 15:26:18 +02:00
495edd42f4 Added array ports and junctions 2026-07-20 15:00:06 +02:00
a067c85994 new text mode editor 2026-07-20 14:24:47 +02:00
bf2512995f labels 2026-07-20 14:02:22 +02:00
8f87a3b477 new connection editing 2026-07-20 13:50:33 +02:00
6a3e9f01d0 Fixed dragging corners 2026-07-20 13:24:37 +02:00
76 changed files with 10402 additions and 1009 deletions

View File

@@ -6,7 +6,6 @@
"type": "shell",
"command": "pyside6-designer",
"args": [
"${workspaceFolder}/ui/*.ui"
],
"options": {
"cwd": "${workspaceFolder}",
@@ -116,6 +115,14 @@
"options": {"cwd": "${workspaceFolder}"},
"problemMatcher": []
},
{
"label": "Qt: Compile Simulation Window UI to Python",
"type": "shell",
"command": "pyside6-uic",
"args": ["--from-imports", "${workspaceFolder}/ui/simulation_window.ui", "-o", "${workspaceFolder}/src/bedit/gui/generated/ui_simulation_window.py"],
"options": {"cwd": "${workspaceFolder}"},
"problemMatcher": []
},
{
"label": "Qt: Build Designer Files",
"dependsOrder": "sequence",
@@ -126,7 +133,8 @@
"Qt: Compile Component Options UI to Python",
"Qt: Compile Port Options UI to Python",
"Qt: Compile Shape Options UI to Python",
"Qt: Compile Icon Editor UI to Python"
"Qt: Compile Icon Editor UI to Python",
"Qt: Compile Simulation Window UI to Python"
],
"problemMatcher": [],
"group": {

View File

@@ -24,7 +24,8 @@ src/bedit/
│ ├── model.py # Document, component, graph, port, icon data
│ ├── port_types.py # Port type definitions and compatibility
│ ├── serializer.py # JSON persistence
── libraries.py # Library file discovery and parsing
── libraries.py # Library file discovery and parsing
│ └── simulation/ # Qt-free composition and OpenModelica interface code
└── gui/ # All Qt-dependent code
├── app.py # QApplication startup and palette
├── main_window.py # Top-level UI orchestration
@@ -53,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
@@ -60,15 +63,21 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Port removal or reorientation must be rejected when it would invalidate an
existing connection.
- Connections reference port IDs, never port names.
- Graph interaction has separate Pointer and Connect modes. Connections store a
`properties.routing` value (`direct`, `angled`, or `spline`); angled routes
store absolute scene points in `properties.waypoints`.
- Connection junctions are explicit typed graph objects. Splitting a connection
creates one incoming and one outgoing segment; the junction can source further
branches without overlapping full connection paths.
- Graph interaction has separate Pointer and Connect modes. Port hints are only
visible in Connect mode. Connecting two blocks opens the compatible port-pair
chooser; explicit port clicks determine its default selection. Connections
use one freely angled polyline format with absolute `properties.waypoints`.
Semantic ports choose compatibility, while each rendered endpoint is the
intersection of the owning hitbox and its center-to-adjacent-route-point ray.
- Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters.
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
They use integer layers below or above graph layer 0. Annotation lines reuse
connection routing and absolute `properties.waypoints` semantics. Graph
connection polyline and absolute `properties.waypoints` semantics. Graph
annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so
their style fields and rendering must remain aligned.
- Icon editing uses a fixed 128×128 coordinate space.
@@ -85,6 +94,80 @@ 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.
- Component clipboard data is shared by the graph, document tree, and library
tree. Pasting into the document node creates roots; pasting into a graph node
creates children at an origin-normalized position. Always clone pasted trees
with fresh IDs, preserve connections between jointly copied graph blocks, and
assign unique sibling names.
- 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.
- Text component sources may contain private Modelica declarations in
`source.declarations`. The text-definition editor exposes declarations above
`source.initialEquations` and `source.equations`, with the same highlighting
and completion in all three fields. The composer emits each in its matching
Modelica section.
- 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.
- Modelica composition lives in `core/simulation/composer.py`; the simulation
service only owns application state and delegates composition. 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.
- Simulation → Export Model composes through `Simulation.compose_source()` without
building the model, then writes the generated source as a `.mo` file.
- OpenModelica integration belongs in `core/simulation/openmodelica.py`. Its
persistent worker and OMC session start lazily on the first queued request.
Never perform OMPython work directly on the Qt GUI thread. Result and error
callbacks run on background threads and must use a Qt signal before touching UI.
One lazy temporary working directory is shared by all requests in the session.
Explicit application shutdown closes OMC and removes that directory plus the
current session's OMPython log and port files; `__del__` is only a fallback.
- Simulation runs start an ephemeral localhost TCP listener before launching the
generated model through OMC's `system()` function. OpenModelica's newline-delimited
`xmltcp` status and message records are parsed in the core and forwarded through
callbacks; the simulation service retains the latest progress for polling.
- The application owns one reusable `SimulationWindow`. Starting a run clears its
progress, log, and future result views. Extend graph presentation through its
Designer-owned `resultsLayout` and the
`clear_result_views()`/`load_result_views()` hooks.
- Simulation-window geometry, dock/toolbar state, and central-results visibility
persist under `simulationWindow/` through `application_settings()`.
- Standalone simulation results are modeled in `core/simulation/results.py`.
Its versioned schema retains model status, messages, metadata, and plottable
traces so the simulation window can open results without an active document.
Human-readable `.json` uses JSON, while the default `.ber` format uses the same
compressed MessagePack approach as `.beb` documents.
- After a successful OpenModelica run, `<model>_res.csv` is parsed on the worker
before temporary-directory cleanup. `SimulationResults.data` stores every CSV
column as a numeric array, including `time`, for later plotting and persistence.
- The simulation window's dockable Signals tree derives hierarchy from dot-separated
result-column names and bracketed array indices (`a[1]` becomes `a → 1`). Leaf
items retain the exact full column name in `UserRole`; plotting code should
consume `SimulationWindow.selected_signal_names()`.
- Simulation graph tabs persist as `SimulationResults.graphs`; every graph has a
stable ID, editable title, and its own `traces` list. Runtime graph widgets belong
in `GraphWorkspacePage.plot_layout`, not in the serialized core model. The
Signals tree checkboxes edit the active graph's traces, and each page embeds a
Matplotlib QtAgg canvas. Each graph persists its own `x_axis` signal (default
`time`), selectable from the Signals tree context menu.
- Embedded Matplotlib canvases provide cursor-centered wheel zoom and direct
left-button drag panning without activating navigation-toolbar modes. Their
custom navigation toolbar restores an explicit data-derived home view.
- Rerunning the same composed model retains graph tabs, ordering, titles, X axes,
and surviving trace settings while replacing numeric data. Missing signals are
pruned from traces/X-axis selection and newly returned columns appear in the tree.
- The optional OpenModelica executable is persisted as
`simulation/openModelicaPath`. The GUI passes it into `Simulation`; core must
not read `QSettings`. An empty path uses OMPython/PATH discovery, while an
explicit `.../bin/omc` path is converted to the OpenModelica home directory.
## Qt Designer and generated files
@@ -118,6 +201,21 @@ pyside6-uic --from-imports ui/shape_options_dialog.ui \
pyside6-uic --from-imports ui/icon_editor_dialog.ui \
-o src/bedit/gui/generated/ui_icon_editor_dialog.py
pyside6-uic --from-imports ui/text_definition_editor.ui \
-o src/bedit/gui/generated/ui_text_definition_editor.py
pyside6-uic --from-imports ui/simulation_settings_dialog.ui \
-o src/bedit/gui/generated/ui_simulation_settings_dialog.py
pyside6-uic --from-imports ui/simulation_window.ui \
-o src/bedit/gui/generated/ui_simulation_window.py
pyside6-uic --from-imports ui/graph_parameters_dialog.ui \
-o src/bedit/gui/generated/ui_graph_parameters_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
@@ -146,6 +244,9 @@ runtime. A tiny generic prompt with one field and OK/Cancel may remain code-only
## Document and library files
- Documents use the `bedit-document` JSON format.
- Documents can be stored as human-readable `.bedit.json`/`.json` through
`JsonDocumentSerializer`, or as compressed MessagePack `.beb` through
`BebDocumentSerializer`. UI document I/O dispatches via `DocumentSerializer`.
- `test.bedit.json` is a useful manually created example during development.
- Library documents use the same recursive document model.
- Library parsing belongs in `core/libraries.py`; Qt change notifications belong

View File

@@ -110,16 +110,19 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
- Components, interface terminals, and connections are selectable. Use a rubber
band or Ctrl-click for multiple selection, Delete to remove items, and the
standard Cut/Copy/Paste shortcuts to duplicate selected component groups.
- Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then
click an output and input. Angled connections accept intermediate corner
clicks, use right-angle segments, and snap to the graph snapping grid.
Right-click cancels an unfinished connection.
- Switch the graph header to **Connect** and click two blocks or their temporary
port hints. A chooser lists every compatible output/input pairing in both
directions and preselects the pairing implied by the clicks. Empty-canvas
clicks add freely angled, grid-snapped vertices; right-click cancels.
- Select a routed connection to reveal its draggable nodes. Right-click a line
to add a node, or right-click a node to delete it.
to add a node, or right-click a node to delete it. Visible wire endpoints are
projected onto each icon's hitbox where the ray from its center toward the
adjacent route point crosses the boundary. Hidden semantic port positions do
not constrain the wire.
- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines
share the Direct, Angled, and Spline routing controls. Annotation context menus
provide the same shape styling as the icon editor and can move annotations
through integer layers below or above graph layer 0.
use the same freely angled polyline and draggable-node behavior as connections.
Annotation context menus provide the same shape styling as the icon editor and
can move annotations through integer layers below or above graph layer 0.
- Double-click a graph component or select it and use **Down** to open its owned
subgraph; use **Up** to return.
- Graph components show **Pointer**, **Input**, and **Output** tools. Select an
@@ -129,7 +132,8 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
icon shape, icon text, fill color, and border color. The same dialog can hide
that component's contained subtree from the Libraries tree.
- The icon editor uses Pointer and click-drag drawing tools. Its toolbar and
mouse wheel provide zoom in, zoom out, and fit-to-canvas controls.
mouse wheel provide zoom in, zoom out, and fit-to-canvas controls. Selected
rectangles expose draggable corner and edge resize handles in both editors.
- Double-click a text component to edit its input list, output list, and
`implementation.source` JSON.
- Right-click any graph component under Current Document to add nested graph or
@@ -142,7 +146,7 @@ BEdit document used as a copy source. The built-in example defines A, B, and C.
- File → Close Document removes the active document and returns to an empty
workspace. An open graph uses a light gray grid whose visible and snapping
spacing are configured separately.
- Edit → Settings → Libraries accepts document files or folders of JSON files.
- Edit → Settings → Libraries accepts JSON or `.beb` document files and folders.
Every component owns its ports, declarative icon, properties, and child graph:

View File

@@ -9,7 +9,10 @@ description = "A starter Qt desktop application"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"matplotlib>=3.8,<4",
"msgpack>=1.0,<2",
"PySide6>=6.7,<7",
"OMPython>4.0",
]
[project.optional-dependencies]
@@ -25,7 +28,11 @@ bedit = "bedit.gui.app:main"
where = ["src"]
[tool.setuptools.package-data]
bedit = ["data/libraries/*.json"]
bedit = [
"data/libraries/*.json",
"data/libraries/*.beb",
"data/syntax/*.json",
]
[tool.ruff]
line-length = 100

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

View File

@@ -1,5 +1,16 @@
<RCC>
<qresource prefix="icons">
<file>icons/list-remove.png</file>
<file>icons/list-add.png</file>
<file>icons/view-form-table.png</file>
<file>icons/office-chart-line.png</file>
<file>icons/run-build.png</file>
<file>icons/preferences-system.png</file>
<file>icons/draw-triangle.png</file>
<file>icons/draw-ellipse.png</file>
<file>icons/draw-circle.png</file>
<file>icons/draw-path.png</file>
<file>icons/network-connect.png</file>
<file>icons/arrow-down.png</file>
<file>icons/configure.png</file>
<file>icons/arrow-up.png</file>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
import logging
LOGGER_NAME = "bedit"
def get_logger(name: str | None = None) -> logging.Logger:
"""Return the shared application logger, optionally scoped to a module."""
return logging.getLogger(LOGGER_NAME if name is None else f"{LOGGER_NAME}.{name}")

View File

@@ -1,8 +1,8 @@
import json
from dataclasses import dataclass
from pathlib import Path
from bedit.core.model import GraphDocument
from bedit.core.serializer import DocumentSerializer
@dataclass(frozen=True)
@@ -13,7 +13,7 @@ class LibraryDocument:
def bundled_library_path() -> Path:
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json"
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.beb"
def default_library_paths() -> list[str]:
@@ -21,12 +21,12 @@ def default_library_paths() -> list[str]:
def load_library_file(path: Path) -> LibraryDocument:
with path.open(encoding="utf-8") as file:
data = json.load(file)
document = GraphDocument.from_dict(data)
document = DocumentSerializer.load(path)
name = str(document.metadata.get("name") or path.stem)
return LibraryDocument(name, document, str(path))
def library_candidates(path: Path) -> list[Path]:
return sorted(path.glob("*.json")) if path.is_dir() else [path]
if not path.is_dir():
return [path]
return sorted((*path.glob("*.json"), *path.glob("*.beb")))

View File

@@ -16,6 +16,7 @@ class Port:
y: float = 0.0
properties: dict[str, Any] = field(default_factory=dict)
type: str = "signal"
allows_multiple_connections: bool = False
def to_dict(self) -> dict[str, Any]:
return {
@@ -24,6 +25,7 @@ class Port:
"position": {"x": self.x, "y": self.y},
"properties": self.properties,
"type": self.type,
"multipleConnections": self.allows_multiple_connections,
}
@classmethod
@@ -36,6 +38,7 @@ class Port:
y=float(position.get("y", 0.0)),
properties=dict(data.get("properties", {})),
type=str(data.get("type", "signal")),
allows_multiple_connections=bool(data.get("multipleConnections", False)),
)
@@ -109,10 +112,13 @@ class Endpoint:
block: str | None = None
port: str | None = None
interface: str | None = None
junction: str | None = None
def to_dict(self) -> dict[str, str]:
if self.interface is not None:
return {"interface": self.interface}
if self.junction is not None:
return {"junction": self.junction}
if self.block is None or self.port is None:
raise ValueError("A block endpoint requires both block and port")
return {"block": self.block, "port": self.port}
@@ -121,6 +127,8 @@ class Endpoint:
def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
if "interface" in data:
return cls(interface=str(data["interface"]))
if "junction" in data:
return cls(junction=str(data["junction"]))
return cls(block=str(data["block"]), port=str(data["port"]))
@@ -152,6 +160,55 @@ class Connection:
)
@dataclass
class Parameter:
id: str
name: str
type: str = "real"
value: str = "0"
def to_dict(self) -> dict[str, str]:
return {"id": self.id, "name": self.name, "type": self.type, "value": self.value}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
if not isinstance(data, dict):
raise ValueError("Each component parameter must be an object")
if "id" not in data:
raise ValueError("Each component parameter must have an ID")
return cls(
id=str(data["id"]),
name=str(data.get("name", "")),
type=str(data.get("type", "real")),
value=str(data.get("value", "0")),
)
@dataclass
class Junction:
id: str
x: float
y: float
type: str = "signal"
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"position": {"x": self.x, "y": self.y},
"type": self.type,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Junction":
position = data.get("position", {})
return cls(
id=str(data["id"]),
x=float(position.get("x", 0)),
y=float(position.get("y", 0)),
type=str(data.get("type", "signal")),
)
@dataclass
class Annotation:
id: str
@@ -197,12 +254,16 @@ class Graph:
blocks: dict[str, Component] = field(default_factory=dict)
connections: dict[str, Connection] = field(default_factory=dict)
annotations: dict[str, Annotation] = field(default_factory=dict)
junctions: dict[str, Junction] = field(default_factory=dict)
simulation_settings: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"blocks": [block.to_dict() for block in self.blocks.values()],
"connections": [connection.to_dict() for connection in self.connections.values()],
"annotations": [item.to_dict() for item in self.annotations.values()],
"junctions": [junction.to_dict() for junction in self.junctions.values()],
"simulation": deepcopy(self.simulation_settings),
}
@classmethod
@@ -211,18 +272,23 @@ class Graph:
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
junctions = [Junction.from_dict(item) for item in data.get("junctions", [])]
if len({block.id for block in blocks}) != len(blocks):
raise ValueError("A graph contains duplicate component IDs")
if len({connection.id for connection in connections}) != len(connections):
raise ValueError("A graph contains duplicate connection IDs")
if len({item.id for item in annotations}) != len(annotations):
raise ValueError("A graph contains duplicate annotation IDs")
if len({item.id for item in junctions}) != len(junctions):
raise ValueError("A graph contains duplicate junction IDs")
if any(item.kind not in {"box", "line", "text"} for item in annotations):
raise ValueError("A graph contains an unknown annotation kind")
return cls(
blocks={block.id: block for block in blocks},
connections={connection.id: connection for connection in connections},
annotations={item.id: item for item in annotations},
junctions={junction.id: junction for junction in junctions},
simulation_settings=deepcopy(data.get("simulation", {})),
)
@@ -235,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"
@@ -257,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},
@@ -271,6 +339,27 @@ class Component:
kind = str(implementation.get("kind", "graph"))
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", "")
declarations = raw_source.get("declarations", "")
initial_equations = raw_source.get("initialEquations", "")
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(declarations, str):
raise ValueError("Text component declarations must be a string")
if not isinstance(initial_equations, str):
raise ValueError("Text component initial equations must be a string")
source = {
"equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
}
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")),
@@ -279,12 +368,13 @@ 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)),
implementation_kind=kind,
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
source=dict(implementation.get("source", {})) if kind == "text" else {},
source=source,
)
@@ -295,7 +385,7 @@ class GraphDocument:
@classmethod
def empty(cls) -> "GraphDocument":
return cls(metadata={"name": "Untitled"})
return cls(metadata={"name": "Current Document"})
def to_dict(self) -> dict[str, Any]:
return {
@@ -348,24 +438,50 @@ class GraphDocument:
def validate(self) -> None:
seen: set[str] = set()
root_names = [component.name for component in self.roots.values()]
if len(set(root_names)) != len(root_names):
raise ValueError("Root component names must be unique")
for component in self.all_components():
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)
@staticmethod
def _validate_graph(owner: Component) -> None:
child_names = [component.name for component in owner.graph.blocks.values()]
if len(set(child_names)) != len(child_names):
raise ValueError(
f"Component names inside {owner.name!r} must be unique"
)
input_ids = {port.id for port in owner.inputs}
output_ids = {port.id for port in owner.outputs}
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
for port in (*owner.inputs, *owner.outputs):
PortTypeRegistry.get(port.type)
for junction in owner.graph.junctions.values():
PortTypeRegistry.get(junction.type)
endpoint_counts: dict[tuple[str, str, str], int] = {}
for connection in owner.graph.connections.values():
if connection.source.interface is not None:
if connection.source.junction is not None:
junction = owner.graph.junctions.get(connection.source.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown source junction"
)
source_port = Port(junction.id, "Junction", type=junction.type)
elif connection.source.interface is not None:
if connection.source.interface not in input_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
@@ -374,7 +490,19 @@ class GraphDocument:
if source is None or connection.source.port not in {p.id for p in source.outputs}:
raise ValueError(f"Connection {connection.id} uses an unknown block output")
source_port = next(p for p in source.outputs if p.id == connection.source.port)
if connection.target.interface is not None:
if connection.target.junction is not None:
junction = owner.graph.junctions.get(connection.target.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown target junction"
)
target_port = Port(
junction.id,
"Junction",
type=junction.type,
allows_multiple_connections=False,
)
elif connection.target.interface is not None:
if connection.target.interface not in output_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
target_port = next(p for p in owner.outputs if p.id == connection.target.interface)
@@ -385,6 +513,40 @@ class GraphDocument:
target_port = next(p for p in target.inputs if p.id == connection.target.port)
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
raise ValueError(f"Connection {connection.id} joins incompatible port types")
source_key = (
"source-junction"
if connection.source.junction is not None
else "source-interface"
if connection.source.interface is not None
else "source-block",
connection.source.block or "",
connection.source.junction
or connection.source.interface
or connection.source.port
or "",
)
target_key = (
"target-junction"
if connection.target.junction is not None
else "target-interface"
if connection.target.interface is not None
else "target-block",
connection.target.block or "",
connection.target.junction
or connection.target.interface
or connection.target.port
or "",
)
endpoint_counts[source_key] = endpoint_counts.get(source_key, 0) + 1
endpoint_counts[target_key] = endpoint_counts.get(target_key, 0) + 1
if (
endpoint_counts[target_key] > 1
and not target_port.allows_multiple_connections
):
raise ValueError(
f"Input {target_port.name!r} has multiple incoming connections "
"but does not allow them"
)
def clone_component(source: Component) -> Component:
@@ -393,10 +555,15 @@ def clone_component(source: Component) -> Component:
def clone(current: Component) -> Component:
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
child_ids = {old.id: new.id for old, new in child_pairs}
junction_ids = {
junction.id: str(uuid4()) for junction in current.graph.junctions.values()
}
def remap(endpoint: Endpoint) -> Endpoint:
if endpoint.interface is not None:
return endpoint
if endpoint.junction is not None:
return Endpoint(junction=junction_ids[endpoint.junction])
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
graph = Graph(
@@ -427,6 +594,13 @@ def clone_component(source: Component) -> Component:
for annotation in current.graph.annotations.values()
for new_id in [str(uuid4())]
},
junctions={
junction_ids[junction.id]: Junction(
junction_ids[junction.id], junction.x, junction.y, junction.type
)
for junction in current.graph.junctions.values()
},
simulation_settings=deepcopy(current.graph.simulation_settings),
)
return Component(
id=str(uuid4()),
@@ -434,13 +608,30 @@ def clone_component(source: Component) -> Component:
x=current.x,
y=current.y,
inputs=[
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.inputs
],
outputs=[
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
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

@@ -1,6 +1,9 @@
import json
import zlib
from pathlib import Path
import msgpack
from bedit.core.model import GraphDocument
@@ -20,3 +23,54 @@ class JsonDocumentSerializer:
json.dump(document.to_dict(), file, indent=2)
file.write("\n")
temporary_path.replace(path)
class BebDocumentSerializer:
"""Compressed MessagePack serializer for the binary .beb format."""
MAGIC = b"BEB\x00"
VERSION = 1
@classmethod
def load(cls, path: Path) -> GraphDocument:
payload = path.read_bytes()
header = cls.MAGIC + bytes([cls.VERSION])
if not payload.startswith(header):
raise ValueError("This is not a supported BEdit binary document")
try:
data = msgpack.unpackb(zlib.decompress(payload[len(header) :]), raw=False)
except (ValueError, zlib.error, msgpack.exceptions.MsgpackException) as error:
raise ValueError("The BEdit binary document is damaged") from error
if not isinstance(data, dict):
raise ValueError("The BEdit binary document has an invalid root value")
return GraphDocument.from_dict(data)
@classmethod
def save(cls, document: GraphDocument, path: Path) -> None:
packed = msgpack.packb(document.to_dict(), use_bin_type=True)
payload = cls.MAGIC + bytes([cls.VERSION]) + zlib.compress(packed, level=9)
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_bytes(payload)
temporary_path.replace(path)
class DocumentSerializer:
"""Select the document serializer from its file extension."""
@staticmethod
def load(path: Path) -> GraphDocument:
serializer = (
BebDocumentSerializer
if path.suffix.lower() == ".beb"
else JsonDocumentSerializer
)
return serializer.load(path)
@staticmethod
def save(document: GraphDocument, path: Path) -> None:
serializer = (
BebDocumentSerializer
if path.suffix.lower() == ".beb"
else JsonDocumentSerializer
)
serializer.save(document, path)

View File

@@ -0,0 +1,23 @@
from bedit.core.simulation.service import Simulation
from bedit.core.simulation.openmodelica import OpenModelicaInterface
from bedit.core.simulation.results import (
BerSimulationResultsSerializer,
JsonSimulationResultsSerializer,
SimulationExecutionResult,
SimulationGraph,
SimulationResults,
SimulationResultsSerializer,
SimulationTrace,
)
__all__ = [
"OpenModelicaInterface",
"Simulation",
"SimulationResults",
"SimulationExecutionResult",
"SimulationGraph",
"SimulationResultsSerializer",
"SimulationTrace",
"BerSimulationResultsSerializer",
"JsonSimulationResultsSerializer",
]

View File

@@ -0,0 +1,355 @@
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 CompositionResult:
"""The intermediate data and generated source produced by composition."""
graph: dict[str, Any]
objects_by_id: dict[str, Any]
modelica: str
model_name: str
def compose_graph(graph: dict[str, Any]) -> CompositionResult:
"""Clean, index, and compose a serialized component tree."""
cleaned_graph = cleanup_graph(deepcopy(graph))
objects_by_id = build_id_list(cleaned_graph)
return CompositionResult(
graph=cleaned_graph,
objects_by_id=objects_by_id,
modelica=emit_model(cleaned_graph, objects_by_id),
model_name=model_name_for(cleaned_graph),
)
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 composer extensions and inspection.
indentation = "\t" * indent
body_indent = "\t" * (indent + 1)
model_name = model_name_for(graph)
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 == "text":
declarations = str(implementation.get("source", {}).get("declarations", ""))
declarations = expand_bevalues(declarations, macros)
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in declarations.splitlines()
)
if implementation_kind == "graph":
for block in nested_graph.get("blocks", []):
block_type = model_name_for(block)
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'])};"
)
if implementation_kind == "graph":
lines.append(f"{indentation}equation")
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:
initial_equations = str(
implementation.get("source", {}).get("initialEquations", "")
)
initial_equations = expand_bevalues(initial_equations, macros)
equations = str(implementation.get("source", {}).get("equations", ""))
equations = expand_bevalues(equations, macros)
if initial_equations.strip():
lines.append(f"{indentation}initial equation")
lines.extend(
f"{body_indent}{line}" if line.strip() else ""
for line in initial_equations.splitlines()
)
lines.append(f"{indentation}equation")
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 composer 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 model_name_for(component: dict[str, Any]) -> str:
"""Return the generated Modelica type name for a component."""
return identifier(f"m_{component['name']}")
def _junction_name(junction_id: str) -> str:
return identifier(f"junction_{junction_id}")

View File

@@ -0,0 +1,427 @@
import json
import os
import shlex
import shutil
import socket
import tempfile
import time
import xml.etree.ElementTree as ET
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from queue import Queue
from threading import Event, Lock, Thread, current_thread
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.results import (
SimulationExecutionResult,
load_openmodelica_csv,
)
log = get_logger(__name__)
ResultCallback = Callable[[Any], None]
ErrorCallback = Callable[[Exception], None]
@dataclass(frozen=True)
class _Request:
description: str
operation: Callable[[Any, Path], Any]
callback: ResultCallback | None
error_callback: ErrorCallback | None
@dataclass(frozen=True)
class SimulationProgress:
phase: str
current_step_size: float
time: float
progress: int
@dataclass(frozen=True)
class SimulationMessage:
stream: str
type: str
text: str
class OpenModelicaInterface:
"""Asynchronous, persistent interface to one OpenModelica session.
The worker and OMC session are created lazily for the first request. Callbacks
execute on background threads and must not manipulate Qt widgets directly.
"""
def __init__(self, executable_path: str = "") -> None:
self._executable_path = executable_path
self._lifecycle_lock = Lock()
self._queue: Queue[_Request | None] | None = None
self._worker: Thread | None = None
self._temp_dir: Path | None = None
@property
def executable_path(self) -> str:
return self._executable_path
def configure(self, executable_path: str) -> None:
"""Use a new executable path for subsequent requests."""
if executable_path == self._executable_path:
return
self.shutdown(wait=True)
self._executable_path = executable_path
def get_version(
self,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Request the OpenModelica version without blocking the caller."""
self.send_expression("getVersion()", callback, error_callback)
def build_model(
self,
model: str,
model_name: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Load and build one composed model as an ordered worker operation."""
def operation(omc, _temp_dir: Path):
loaded = omc.sendExpression(f"loadString({json.dumps(model)})")
if loaded is not True:
raise RuntimeError("OpenModelica could not load the composed model")
return omc.sendExpression(f"buildModel({model_name})")
self._submit("build model", operation, callback, error_callback)
def run_model(
self,
executable: str,
arguments: list[str],
progress_callback: Callable[[SimulationProgress], None] | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Run a built model and consume its XML/TCP status stream."""
def operation(omc, temp_dir: Path):
return _run_model_with_tcp(
omc,
executable,
arguments,
temp_dir,
progress_callback,
message_callback,
)
self._submit("run simulation", operation, callback, error_callback)
def send_expression(
self,
expression: str,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
*,
parsed: bool = True,
) -> None:
"""Queue an OMC expression for ordered execution on the worker thread."""
def operation(omc, _temp_dir: Path):
return omc.sendExpression(expression, parsed=parsed)
self._submit(expression, operation, callback, error_callback)
def _submit(
self,
description: str,
operation: Callable[[Any, Path], Any],
callback: ResultCallback | None,
error_callback: ErrorCallback | None,
) -> None:
request = _Request(description, operation, callback, error_callback)
self._ensure_worker().put(request)
def shutdown(self, *, wait: bool = True) -> None:
"""Ask the worker to close its OMC session after queued requests."""
with self._lifecycle_lock:
worker = self._worker
queue = self._queue
self._worker = None
self._queue = None
if queue is not None:
queue.put(None)
if wait and worker is not None and worker is not current_thread():
worker.join()
if worker is None:
self._cleanup_temp_dir()
def __del__(self) -> None:
"""Best-effort fallback; normal application shutdown is explicit."""
try:
self.shutdown(wait=False)
except Exception:
pass
def _ensure_worker(self) -> Queue[_Request | None]:
with self._lifecycle_lock:
if self._worker is not None and self._worker.is_alive():
return self._queue
temp_dir = self._ensure_temp_dir_locked()
queue: Queue[_Request | None] = Queue()
worker = Thread(
target=self._worker_main,
args=(queue, self._executable_path, temp_dir),
name="bedit-openmodelica",
daemon=True,
)
self._queue = queue
self._worker = worker
worker.start()
return queue
def _worker_main(
self,
queue: Queue[_Request | None],
executable_path: str,
temp_dir: Path,
) -> None:
omc = None
try:
while (request := queue.get()) is not None:
try:
if omc is None:
omc = _create_session(executable_path)
changed_directory = omc.sendExpression(
f"cd({json.dumps(str(temp_dir))})"
)
if not changed_directory:
raise RuntimeError(
f"OpenModelica could not use {str(temp_dir)!r}"
)
result = request.operation(omc, temp_dir)
except Exception as error:
if request.error_callback is None:
log.exception(
"OpenModelica request failed: %s", request.description
)
else:
_deliver_callback(request.error_callback, error)
else:
if request.callback is not None:
_deliver_callback(request.callback, result)
finally:
transport_files = _ompython_transport_files(omc)
if omc is not None:
try:
omc.sendExpression("quit()")
except Exception:
log.debug("Could not close OpenModelica session", exc_info=True)
for path in transport_files:
try:
path.unlink(missing_ok=True)
except OSError:
log.debug("Could not remove OMPython file %s", path, exc_info=True)
self._cleanup_temp_dir(temp_dir)
def _ensure_temp_dir_locked(self) -> Path:
if self._temp_dir is None:
self._temp_dir = Path(tempfile.mkdtemp(prefix="bedit-openmodelica-"))
return self._temp_dir
def _cleanup_temp_dir(self, expected: Path | None = None) -> None:
with self._lifecycle_lock:
if expected is not None and self._temp_dir != expected:
temp_dir = expected
else:
temp_dir = self._temp_dir
self._temp_dir = None
if temp_dir is not None:
shutil.rmtree(temp_dir, ignore_errors=True)
def _deliver_callback(callback: Callable[[Any], None], value: Any) -> None:
try:
callback(value)
except Exception:
log.exception("OpenModelica callback failed")
def _create_session(executable_path: str):
from OMPython import OMCSessionZMQ
return OMCSessionZMQ(omhome=_openmodelica_home(executable_path))
def _run_model_with_tcp(
omc,
executable: str,
arguments: list[str],
temp_dir: Path,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> SimulationExecutionResult:
executable_path = Path(executable)
executable_command = executable
if not executable_path.is_absolute() and executable_path.parent == Path("."):
executable_command = f"./{executable}"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 0))
server.listen(1)
server.settimeout(0.25)
port = server.getsockname()[1]
command = shlex.join([
executable_command,
*arguments,
f"-port={port}",
"-logFormat=xmltcp",
])
output_path = temp_dir / "simulation-output.txt"
command_finished = Event()
reader_finished = Event()
reader_errors: list[Exception] = []
def read_progress() -> None:
try:
connection = _accept_simulation_connection(server, command_finished)
with connection, connection.makefile(
"r", encoding="utf-8"
) as stream:
for line in stream:
_handle_simulation_xml(
line, progress_callback, message_callback
)
except Exception as error:
reader_errors.append(error)
finally:
reader_finished.set()
reader = Thread(
target=read_progress,
name="bedit-simulation-progress",
daemon=True,
)
reader.start()
log.info("Starting simulation through OpenModelica: %s", command)
try:
return_code = omc.sendExpression(
f"system({json.dumps(command)}, {json.dumps(str(output_path))})"
)
finally:
command_finished.set()
if not reader_finished.wait(20.0):
raise TimeoutError("Simulation progress connection did not close")
if reader_errors:
raise reader_errors[0]
if return_code != 0:
output = output_path.read_text(errors="replace") if output_path.exists() else ""
detail = f": {output.strip()}" if output.strip() else ""
raise RuntimeError(
f"Simulation process exited with status {return_code}{detail}"
)
result_path = temp_dir / f"{Path(executable).stem}_res.csv"
if not result_path.is_file():
raise RuntimeError(
f"OpenModelica did not create the expected result file {result_path.name!r}"
)
data = load_openmodelica_csv(result_path)
log.info(
"Loaded %d result columns from %s", len(data), result_path.name
)
return SimulationExecutionResult(
return_code=int(return_code),
result_file=str(result_path),
data=data,
)
def _accept_simulation_connection(
server: socket.socket, command_finished: Event
) -> socket.socket:
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
try:
connection, _address = server.accept()
return connection
except socket.timeout:
if command_finished.is_set():
raise RuntimeError(
"Simulation command finished before opening its progress connection"
)
raise TimeoutError("Simulation did not connect to the progress server")
def _handle_simulation_xml(
line: str,
progress_callback: Callable[[SimulationProgress], None] | None,
message_callback: Callable[[SimulationMessage], None] | None,
) -> None:
text = line.strip()
if not text:
return
try:
element = ET.fromstring(text)
except ET.ParseError:
log.warning("Invalid simulation status XML: %s", text)
return
if element.tag == "status" and progress_callback is not None:
_deliver_callback(
progress_callback,
SimulationProgress(
phase=element.get("phase", ""),
current_step_size=float(element.get("currentStepSize", 0)),
time=float(element.get("time", 0)),
progress=int(float(element.get("progress", 0))),
),
)
elif element.tag == "message":
message = SimulationMessage(
stream=element.get("stream", ""),
type=element.get("type", ""),
text=element.get("text", ""),
)
log.info("OpenModelica %s: %s", message.stream, message.text)
if message_callback is not None:
_deliver_callback(message_callback, message)
def _ompython_transport_files(omc) -> set[Path]:
"""Return only the log and port files owned by this OMPython session."""
if omc is None:
return set()
process = getattr(omc, "omc_process", None)
if process is None:
return set()
files: set[Path] = set()
temp_dir = getattr(process, "_temp_dir", None)
file_base = getattr(process, "_omc_filebase", None)
if temp_dir is not None and file_base:
files.add(Path(temp_dir) / f"{file_base}.log")
try:
port_file = process._get_portfile_path()
except Exception:
port_file = None
if port_file is not None:
files.add(Path(port_file))
return files
def _openmodelica_home(executable_path: str) -> str | None:
"""Convert an optional omc executable path to the home expected by OMPython."""
if not executable_path.strip():
return None
path = Path(os.path.expandvars(executable_path)).expanduser()
if path.name.lower() in {"omc", "omc.exe"}:
return str(path.parent.parent)
return str(path)

View File

@@ -0,0 +1,222 @@
import csv
import json
import zlib
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from uuid import uuid4
import msgpack
RESULTS_FORMAT = "bedit-simulation-results"
RESULTS_VERSION = 1
@dataclass
class SimulationTrace:
"""One plottable series; samples can be filled by a future result importer."""
name: str
x_values: list[float] = field(default_factory=list)
y_values: list[float] = field(default_factory=list)
x_label: str = "time"
y_label: str = ""
unit: str = ""
properties: dict[str, Any] = field(default_factory=dict)
@dataclass
class SimulationGraph:
"""One graph workspace tab and its configured traces."""
id: str = field(default_factory=lambda: str(uuid4()))
title: str = "Graph 1"
x_axis: str = "time"
traces: list[SimulationTrace] = field(default_factory=list)
@dataclass
class SimulationResults:
"""Serializable state displayed by the standalone simulation window."""
model_name: str = ""
status: dict[str, Any] = field(default_factory=dict)
messages: list[dict[str, str]] = field(default_factory=list)
data: dict[str, list[float]] = field(default_factory=dict)
graphs: list[SimulationGraph] = field(
default_factory=lambda: [SimulationGraph()]
)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"format": RESULTS_FORMAT,
"version": RESULTS_VERSION,
"modelName": self.model_name,
"status": dict(self.status),
"messages": [dict(message) for message in self.messages],
"data": {name: list(values) for name, values in self.data.items()},
"graphs": [asdict(graph) for graph in self.graphs],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "SimulationResults":
if data.get("format") != RESULTS_FORMAT:
raise ValueError("Not a BEdit simulation-results file")
if data.get("version") != RESULTS_VERSION:
raise ValueError(f"Unsupported simulation-results version: {data.get('version')!r}")
try:
graphs = [
SimulationGraph(
id=str(graph["id"]),
title=str(graph["title"]),
x_axis=str(graph.get("x_axis", "time")),
traces=[
SimulationTrace(**trace) for trace in graph.get("traces", [])
],
)
for graph in data.get("graphs", [])
]
return cls(
model_name=str(data.get("modelName", "")),
status=dict(data.get("status", {})),
messages=[dict(message) for message in data.get("messages", [])],
data={
str(name): [float(value) for value in values]
for name, values in dict(data.get("data", {})).items()
},
graphs=graphs,
metadata=dict(data.get("metadata", {})),
)
except (KeyError, TypeError, ValueError) as error:
raise ValueError("Malformed simulation-results data") from error
@dataclass(frozen=True)
class SimulationExecutionResult:
"""Completed process information delivered before its temp files disappear."""
return_code: int
result_file: str
data: dict[str, list[float]]
def load_openmodelica_csv(path: str | Path) -> dict[str, list[float]]:
"""Read an OpenModelica CSV result as one numeric array per column."""
source = Path(path)
try:
with source.open(newline="", encoding="utf-8") as file:
reader = csv.reader(file)
headers = next(reader)
if not headers or any(not header for header in headers):
raise ValueError("The result CSV has an invalid header")
if len(set(headers)) != len(headers):
raise ValueError("The result CSV contains duplicate column names")
columns = {header: [] for header in headers}
for row_number, row in enumerate(reader, start=2):
if len(row) != len(headers):
raise ValueError(
f"Result CSV row {row_number} has {len(row)} values; "
f"expected {len(headers)}"
)
for header, value in zip(headers, row, strict=True):
columns[header].append(float(value))
except OSError as error:
raise ValueError(f"Could not read OpenModelica results: {error}") from error
except StopIteration as error:
raise ValueError("The OpenModelica result CSV is empty") from error
except ValueError as error:
if str(error).startswith(("The result CSV", "Result CSV")):
raise
raise ValueError(f"The OpenModelica result CSV is not numeric: {error}") from error
return columns
class JsonSimulationResultsSerializer:
@staticmethod
def load(path: Path) -> SimulationResults:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"Could not read simulation results: {error}") from error
if not isinstance(data, dict):
raise ValueError("Simulation-results root must be an object")
return SimulationResults.from_dict(data)
@staticmethod
def save(results: SimulationResults, path: Path) -> None:
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_text(
json.dumps(results.to_dict(), indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
temporary_path.replace(path)
class BerSimulationResultsSerializer:
"""Compressed MessagePack serializer for binary simulation results."""
MAGIC = b"BER\x00"
VERSION = 1
@classmethod
def load(cls, path: Path) -> SimulationResults:
try:
payload = path.read_bytes()
except OSError as error:
raise ValueError(f"Could not read simulation results: {error}") from error
header = cls.MAGIC + bytes([cls.VERSION])
if not payload.startswith(header):
raise ValueError("This is not a supported BEdit binary results file")
try:
data = msgpack.unpackb(
zlib.decompress(payload[len(header) :]), raw=False
)
except (ValueError, zlib.error, msgpack.exceptions.MsgpackException) as error:
raise ValueError("The BEdit binary results file is damaged") from error
if not isinstance(data, dict):
raise ValueError("The BEdit binary results file has an invalid root value")
return SimulationResults.from_dict(data)
@classmethod
def save(cls, results: SimulationResults, path: Path) -> None:
packed = msgpack.packb(results.to_dict(), use_bin_type=True)
payload = cls.MAGIC + bytes([cls.VERSION]) + zlib.compress(packed, level=9)
temporary_path = path.with_suffix(path.suffix + ".tmp")
temporary_path.write_bytes(payload)
temporary_path.replace(path)
class SimulationResultsSerializer:
"""Select JSON or compressed MessagePack based on the file extension."""
@staticmethod
def load(path: str | Path) -> SimulationResults:
target = Path(path)
serializer = (
BerSimulationResultsSerializer
if target.suffix.lower() == ".ber"
else JsonSimulationResultsSerializer
)
return serializer.load(target)
@staticmethod
def save(results: SimulationResults, path: str | Path) -> None:
target = Path(path)
serializer = (
BerSimulationResultsSerializer
if target.suffix.lower() == ".ber"
else JsonSimulationResultsSerializer
)
serializer.save(results, target)
def save_simulation_results(path: str | Path, results: SimulationResults) -> None:
SimulationResultsSerializer.save(results, path)
def load_simulation_results(path: str | Path) -> SimulationResults:
return SimulationResultsSerializer.load(path)

View File

@@ -0,0 +1,156 @@
from collections.abc import Callable
from typing import Any
from bedit.core.application_log import get_logger
from bedit.core.simulation.composer import compose_graph
from bedit.core.simulation.openmodelica import (
ErrorCallback,
OpenModelicaInterface,
ResultCallback,
SimulationMessage,
SimulationProgress,
)
log = get_logger(__name__)
class Simulation:
"""Application-owned composition state and OpenModelica interface."""
def __init__(self, *, openmodelica_path: str = "") -> None:
self.state: dict[str, Any] = {}
self.last_composition_input: dict[str, Any] | None = None
self.last_composition_output: str | None = None
self.id_list: dict[str, Any] = {}
self.model_name: str | None = None
self._openmodelica_path = openmodelica_path
self.openmodelica = OpenModelicaInterface(openmodelica_path)
self.model_path: str | None = None
self.simulation_progress: SimulationProgress | None = None
@property
def openmodelica_path(self) -> str:
return self._openmodelica_path
@openmodelica_path.setter
def openmodelica_path(self, value: str) -> None:
self._openmodelica_path = value
self.openmodelica.configure(value)
def compose(
self,
graph: dict[str, Any],
callback: Callable[[str], None] | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Compose and retain the active graph's Modelica representation."""
self.model_path = None
self.compose_source(graph)
def _model_compiled(result):
log.info("Compiling OK: %s", result)
try:
self.model_path = str(result[0])
except (IndexError, TypeError) as error:
failure = RuntimeError(
f"OpenModelica returned an invalid build result: {result!r}"
)
failure.__cause__ = error
if error_callback is not None:
error_callback(failure)
else:
log.error("%s", failure)
return
if callback is not None:
callback(self.model_path)
self.openmodelica.build_model(
self.last_composition_output,
self.model_name,
_model_compiled,
error_callback,
)
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
"""Compose Modelica source without asking OpenModelica to build it."""
result = compose_graph(graph)
self.last_composition_input = result.graph
self.id_list = result.objects_by_id
self.last_composition_output = result.modelica
self.model_name = result.model_name
# log.info("Composed OpenModelica model:\n%s", result.modelica)
return result.model_name, result.modelica
def run_simulation(
self,
graph: dict[str, Any],
progress_callback: Callable[[SimulationProgress], None] | None = None,
message_callback: Callable[[SimulationMessage], None] | None = None,
callback: ResultCallback | None = None,
error_callback: ErrorCallback | None = None,
) -> None:
"""Compose, build, and asynchronously run the current graph."""
self.simulation_progress = None
def report_progress(progress: SimulationProgress) -> None:
self.simulation_progress = progress
if progress_callback is not None:
progress_callback(progress)
def run_model(model_path: str) -> None:
try:
arguments = self.build_simulation_arguments()
except Exception as error:
if error_callback is not None:
error_callback(error)
else:
log.exception("Could not prepare simulation arguments")
return
self.openmodelica.run_model(
model_path,
arguments,
report_progress,
message_callback,
callback,
error_callback,
)
self.compose(graph, run_model, error_callback)
def get_progress(self) -> SimulationProgress | None:
"""Return the most recently received simulation status."""
return self.simulation_progress
def shutdown(self, *, wait: bool = True) -> None:
"""Close OpenModelica and clean its generated working directory."""
self.openmodelica.shutdown(wait=wait)
self.model_path = None
def build_simulation_arguments(self) -> list[str]:
opts = self.last_composition_input["implementation"]["graph"].get(
"simulation", {}
)
start_time = float(opts.get("startTime", 0.0))
stop_time = float(opts.get("stopTime", 1.0))
interval_mode = opts.get("intervalMode", "numberOfIntervals")
interval_time = float(opts.get("intervalTime", 0.002))
if interval_mode == "numberOfIntervals":
intervals = int(opts.get("numberOfIntervals", 500))
if intervals <= 0:
raise ValueError("Number of simulation intervals must be positive")
interval_time = (stop_time - start_time) / intervals
if interval_time <= 0:
raise ValueError("Simulation interval must be positive")
arguments = [
"-outputFormat=csv",
f"-startTime={start_time}",
f"-stopTime={stop_time}",
f"-stepSize={interval_time}",
]
log.info("Running model with: %s", arguments)
return arguments

Binary file not shown.

View File

@@ -1,212 +0,0 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Example"
},
"roots": [
{
"id": "bf714517-e7b2-4f4b-8b53-55642e3beb28",
"name": "A",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-5c5d4695",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-de109124",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "A",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": [],
"parameters": {}
}
}
},
{
"id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f",
"name": "B",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-4f732b3e",
"name": "Port 1",
"position": {
"x": -176.0,
"y": -144.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 48.0
}
},
"type": "signal"
},
{
"id": "port-ef7c4218",
"name": "Port 2",
"position": {
"x": -176.0,
"y": -16.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 80.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-b68679b2",
"name": "Port 3",
"position": {
"x": 128.0,
"y": -80.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "B",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
]
}

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

@@ -0,0 +1,26 @@
import logging
from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QPlainTextEdit
class _LogEmitter(QObject):
lineReady = Signal(str)
class ApplicationLogHandler(logging.Handler):
"""Thread-safe bridge from Python logging into a Qt text view."""
def __init__(self, output: QPlainTextEdit) -> None:
super().__init__()
self.emitter = _LogEmitter(output)
self.emitter.lineReady.connect(output.appendPlainText)
self.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(message)s", "%H:%M:%S")
)
def emit(self, record: logging.LogRecord) -> None:
try:
self.emitter.lineReady.emit(self.format(record))
except Exception:
self.handleError(record)

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand
from bedit.core.model import Annotation, Component, Connection, Port
from bedit.core.model import Annotation, Component, Connection, Junction, Port
class AddComponentCommand(QUndoCommand):
@@ -76,6 +76,32 @@ class AddConnectionCommand(QUndoCommand):
self.controller._remove_connection(self.owner_id, self.connection.id)
class SplitConnectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
original: Connection,
junction: Junction,
first: Connection,
second: Connection,
) -> None:
super().__init__("Add connection junction")
self.controller, self.owner_id = controller, owner_id
self.original, self.junction = original, junction
self.first, self.second = first, second
def redo(self) -> None:
self.controller._split_connection(
self.owner_id, self.original.id, self.junction, self.first, self.second
)
def undo(self) -> None:
self.controller._restore_split_connection(
self.owner_id, self.original, self.junction.id, self.first.id, self.second.id
)
class AddAnnotationCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
super().__init__(f"Draw {annotation.kind}")
@@ -111,6 +137,32 @@ class EditGraphItemCommand(QUndoCommand):
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
class EditSimulationSettingsCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, old: dict, new: dict) -> None:
super().__init__("Edit simulation settings")
self.controller, self.owner_id = controller, owner_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_simulation_settings(self.owner_id, self.new)
def undo(self) -> None:
self.controller._set_simulation_settings(self.owner_id, self.old)
class EditGraphParametersCommand(QUndoCommand):
def __init__(self, controller, old: dict, new: dict) -> None:
super().__init__("Edit graph parameters")
self.controller = controller
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_graph_parameters(self.new)
def undo(self) -> None:
self.controller._set_graph_parameters(self.old)
class DeleteAnnotationsCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
super().__init__("Delete annotations")
@@ -200,6 +252,32 @@ class EditComponentAppearanceCommand(QUndoCommand):
self.controller._set_component_appearance(self.component_id, self.old)
class EditComponentPropertiesCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict, text: str) -> None:
super().__init__(text)
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_properties(self.component_id, self.new)
def undo(self) -> None:
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")
@@ -267,7 +345,7 @@ class PasteSelectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
owner_id: str | None,
blocks: dict[str, Component],
connections: dict[str, Connection],
) -> None:
@@ -309,3 +387,14 @@ class EditTextDefinitionCommand(QUndoCommand):
def undo(self) -> None:
self.controller._set_text_definition(self.component_id, self.old)
def id(self) -> int:
return 1001
def mergeWith(self, other: QUndoCommand) -> bool: # noqa: N802
if not isinstance(other, EditTextDefinitionCommand):
return False
if other.component_id != self.component_id:
return False
self.new = other.new
return True

View File

@@ -13,8 +13,12 @@ from bedit.gui.controllers.commands import (
DeleteSelectionCommand,
DeleteAnnotationsCommand,
EditGraphItemCommand,
EditGraphParametersCommand,
EditSimulationSettingsCommand,
EditTextDefinitionCommand,
EditComponentAppearanceCommand,
EditComponentPropertiesCommand,
EditComponentParametersCommand,
MoveComponentCommand,
MoveInterfacePortCommand,
PasteSelectionCommand,
@@ -22,6 +26,7 @@ from bedit.gui.controllers.commands import (
RenameInterfacePortCommand,
ReplaceSourceCommand,
RotateComponentsCommand,
SplitConnectionCommand,
)
from bedit.core.model import (
Annotation,
@@ -30,11 +35,14 @@ from bedit.core.model import (
Endpoint,
GraphDocument,
Icon,
Junction,
Parameter,
Port,
clone_component,
)
from bedit.core.simulation import Simulation
from bedit.core.port_types import PortTypeRegistry
from bedit.core.serializer import JsonDocumentSerializer
from bedit.core.serializer import DocumentSerializer
class DocumentController(QObject):
@@ -45,6 +53,8 @@ class DocumentController(QObject):
componentRemoved = Signal(str)
componentMoved = Signal(str, QPointF)
componentRotated = Signal(str, float)
componentPropertiesChanged = Signal(str)
textDefinitionChanged = Signal(str)
connectionAdded = Signal(str)
connectionRemoved = Signal(str)
graphItemChanged = Signal(str, str)
@@ -54,8 +64,9 @@ class DocumentController(QObject):
filePathChanged = Signal(object)
modifiedChanged = Signal(bool)
def __init__(self, parent=None) -> None:
def __init__(self, parent=None, *, simulation: Simulation | None = None) -> None:
super().__init__(parent)
self.simulation = simulation or Simulation()
self.document: GraphDocument | None = None
self.active_component_id: str | None = None
self.file_path: Path | None = None
@@ -99,7 +110,7 @@ class DocumentController(QObject):
self.filePathChanged.emit(None)
def load(self, path: Path) -> None:
self.document = JsonDocumentSerializer.load(path)
self.document = DocumentSerializer.load(path)
self.active_component_id = next(iter(self.document.roots), None)
self.file_path = path
self.undo_stack.clear()
@@ -115,7 +126,7 @@ class DocumentController(QObject):
target = path or self.file_path
if target is None:
raise ValueError("No file path has been selected")
JsonDocumentSerializer.save(self.document, target)
DocumentSerializer.save(self.document, target)
self.file_path = target
self.undo_stack.setClean()
self.filePathChanged.emit(target)
@@ -151,12 +162,19 @@ class DocumentController(QObject):
if self.document is None:
raise ValueError("Open or create a document first")
number = len(self.document.roots) + 1
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
component = Component(
id=str(uuid4()),
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
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={
"declarations": "",
"initialEquations": "",
"equations": "",
}
if kind == "text"
else {},
)
self.undo_stack.push(AddComponentCommand(self, None, component))
self.activate_component(component.id)
@@ -169,12 +187,19 @@ class DocumentController(QObject):
if owner is None or owner.implementation_kind != "graph":
raise ValueError("Children can only be added to graph components")
number = len(owner.graph.blocks) + 1
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
component = Component(
id=str(uuid4()),
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
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={
"declarations": "",
"initialEquations": "",
"equations": "",
}
if kind == "text"
else {},
)
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
return component.id
@@ -209,6 +234,9 @@ class DocumentController(QObject):
if self.active_component is None or self.active_component.implementation_kind != "graph":
raise ValueError("Open a graph component before placing components")
component = clone_component(source)
component.name = self._available_component_name(
source.name, self.active_component.graph.blocks.values(), 0
)
component.x, component.y = position.x(), position.y()
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
return component.id
@@ -219,6 +247,20 @@ class DocumentController(QObject):
MoveComponentCommand(self, self.active_component_id, component_id, old, new)
)
def move_junction(self, junction_id: str, old: QPointF, new: QPointF) -> None:
if old != new and self.active_component_id is not None:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"junction_geometry",
junction_id,
{"x": old.x(), "y": old.y()},
{"x": new.x(), "y": new.y()},
"Move connection junction",
)
)
def rotate_components(self, component_ids: set[str]) -> None:
if self.active_component is None or self.active_component_id is None:
return
@@ -235,7 +277,6 @@ class DocumentController(QObject):
source: Endpoint,
target: Endpoint,
*,
routing: str = "angled",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None:
@@ -246,20 +287,72 @@ class DocumentController(QObject):
raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if routing not in {"direct", "angled", "spline"}:
raise ValueError(f"Unknown connection routing: {routing}")
if not self.endpoint_accepts_connection(source, "source"):
raise ValueError(
f"Port {source_port.name!r} already has a connection; enable multiple connections first"
)
if not self.endpoint_accepts_connection(target, "target"):
raise ValueError(
f"Port {target_port.name!r} already has a connection; enable multiple connections first"
)
connection = Connection(
str(uuid4()),
source,
target,
properties={
"routing": routing,
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
)
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
return connection.id
def split_connection(
self,
connection_id: str,
position: QPointF,
first_waypoints: list[QPointF],
second_waypoints: list[QPointF],
) -> str:
if self.active_component_id is None:
raise ValueError("There is no active graph")
original = self.active_graph.connections.get(connection_id)
if original is None:
raise ValueError("The connection no longer exists")
port_type = self.connection_port_type(original)
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
first_properties = deepcopy(original.properties)
first_properties["waypoints"] = [
{"x": point.x(), "y": point.y()} for point in first_waypoints
]
second_properties = {
"waypoints": [{"x": point.x(), "y": point.y()} for point in second_waypoints]
}
first = Connection(
str(uuid4()),
original.source,
Endpoint(junction=junction.id),
original.name,
first_properties,
)
second = Connection(
str(uuid4()),
Endpoint(junction=junction.id),
original.target,
"",
second_properties,
)
self.undo_stack.push(
SplitConnectionCommand(
self,
self.active_component_id,
original,
junction,
first,
second,
)
)
return junction.id
def add_annotation(
self,
kind: str,
@@ -267,7 +360,6 @@ class DocumentController(QObject):
end: QPointF,
*,
text: str = "",
routing: str = "angled",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None:
@@ -297,16 +389,81 @@ class DocumentController(QObject):
layer=-1,
properties={
**style,
"routing": routing,
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
)
self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
return annotation.id
def set_route_waypoints(
self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
def edit_simulation_settings(self, settings: dict) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before editing simulation settings")
old = deepcopy(component.graph.simulation_settings)
new = deepcopy(settings)
if old != new:
self.undo_stack.push(
EditSimulationSettingsCommand(self, component.id, old, new)
)
def edit_graph_parameter_values(
self, root_id: str, values: dict[str, dict[str, str]]
) -> None:
root = self.document.find_component(root_id) if self.document else None
if root is None:
return
subtree_ids = {component.id for component in self._component_subtree(root)}
if not set(values) <= subtree_ids:
raise ValueError("Parameter changes contain a component outside the active graph")
old: dict[str, list[dict]] = {}
new: dict[str, list[dict]] = {}
for component_id, parameter_values in values.items():
component = self.document.find_component(component_id)
known_ids = {parameter.id for parameter in component.parameters}
if not set(parameter_values) <= known_ids:
raise ValueError(f"Component {component.name!r} contains an unknown parameter")
updated = deepcopy(component.parameters)
for parameter in updated:
if parameter.id in parameter_values:
parameter.value = parameter_values[parameter.id]
old[component_id] = [parameter.to_dict() for parameter in component.parameters]
new[component_id] = [parameter.to_dict() for parameter in updated]
if old != new:
self.undo_stack.push(EditGraphParametersCommand(self, old, new))
def compose_active_graph(self) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before composing")
self.simulation.compose(component.to_dict())
def compose_active_graph_source(self) -> tuple[str, str]:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before exporting a model")
return self.simulation.compose_source(component.to_dict())
def run_simulation(
self,
progress_callback=None,
message_callback=None,
callback=None,
error_callback=None,
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "graph":
raise ValueError("Open a graph component before running a simulation")
self.simulation.run_simulation(
component.to_dict(),
progress_callback,
message_callback,
callback,
error_callback,
)
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
item = (
self.active_graph.connections
if item_kind == "connection"
@@ -316,8 +473,6 @@ class DocumentController(QObject):
return
old = deepcopy(item.properties)
new = deepcopy(old)
if routing is not None:
new["routing"] = routing
new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
if old != new:
self.undo_stack.push(
@@ -406,6 +561,16 @@ class DocumentController(QObject):
owner = self.active_component
if owner is None:
return None
if endpoint.junction is not None:
junction = owner.graph.junctions.get(endpoint.junction)
if junction is None:
return None
return Port(
junction.id,
"Junction",
type=junction.type,
allows_multiple_connections=role == "source",
)
if endpoint.interface is not None:
ports = owner.inputs if role == "source" else owner.outputs
else:
@@ -421,6 +586,17 @@ class DocumentController(QObject):
port = self._port_for_endpoint(connection.source, "source")
return port.type if port is not None else "signal"
def endpoint_accepts_connection(self, endpoint: Endpoint, role: str) -> bool:
port = self._port_for_endpoint(endpoint, role)
if port is None:
return False
if role == "source" or port.allows_multiple_connections:
return True
return not any(
endpoint == (connection.source if role == "source" else connection.target)
for connection in self.active_graph.connections.values()
)
def add_interface_port(self, direction: str, position: QPointF) -> str:
component = self.active_component
if component is None or component.implementation_kind != "graph":
@@ -478,7 +654,10 @@ class DocumentController(QObject):
self,
inputs: list[Port],
outputs: list[Port],
source: dict,
declarations: str,
initial_equations: str,
equations: str,
parameters: list[Parameter],
) -> None:
component = self.active_component
if component is None or component.implementation_kind != "text":
@@ -487,6 +666,13 @@ class DocumentController(QObject):
output_ids = [port.id for port in outputs]
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
raise ValueError("Input and output IDs must be unique")
if any(not port.name.strip() for port in (*inputs, *outputs)):
raise ValueError("Every port must have a name")
parameter_ids = [parameter.id for parameter in parameters]
if len(set(parameter_ids)) != len(parameter_ids):
raise ValueError("Parameter IDs must be unique")
if any(not parameter.name.strip() for parameter in parameters):
raise ValueError("Every parameter must have a name")
if self.document is not None:
parent = self.document.find_parent(component.id)
if parent is not None:
@@ -509,13 +695,26 @@ 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": deepcopy(source),
"source": {
"equations": equations,
"declarations": declarations,
"initialEquations": initial_equations,
},
"parameters": [parameter.to_dict() for parameter in parameters],
}
if old != new:
candidate = deepcopy(self.document)
candidate_component = candidate.find_component(component.id)
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))
def edit_component_appearance(
@@ -526,25 +725,36 @@ class DocumentController(QObject):
inputs: list[Port],
outputs: list[Port],
show_subtree: bool,
show_name: bool,
) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is None:
return
siblings = self._component_siblings(component_id)
if any(item.id != component_id and item.name == name for item in siblings):
raise ValueError(f"A component named {name!r} already exists at this level")
old = {
"name": component.name,
"icon": component.icon.to_dict(),
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
properties = deepcopy(component.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = {
"name": name,
"icon": icon.to_dict(),
"inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs],
"show_subtree": show_subtree,
"properties": properties,
}
if old != new:
candidate = deepcopy(self.document)
@@ -553,9 +763,75 @@ class DocumentController(QObject):
candidate_component.icon = Icon.from_dict(icon.to_dict())
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.properties = deepcopy(properties)
candidate.validate()
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
def move_component_name_label(self, component_id: str, position: QPointF) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is None:
return
old = deepcopy(component.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditComponentPropertiesCommand(
self, component_id, old, new, "Move component name"
)
)
def edit_connection_options(
self, connection_id: str, name: str, show_name: bool
) -> None:
owner = self.active_component
if owner is None or self.active_component_id is None:
return
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
old = {"name": connection.name, "properties": deepcopy(connection.properties)}
properties = deepcopy(connection.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = {"name": name, "properties": properties}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection_data",
connection_id,
old,
new,
"Edit connection options",
)
)
def move_connection_name_label(self, connection_id: str, position: QPointF) -> None:
connection = self.active_graph.connections.get(connection_id)
if connection is None or self.active_component_id is None:
return
old = deepcopy(connection.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection",
connection_id,
old,
new,
"Move connection name",
)
)
def edit_component_ports(
self, component_id: str, inputs: list[Port], outputs: list[Port]
) -> None:
@@ -595,6 +871,7 @@ class DocumentController(QObject):
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
}
new = {
**old,
@@ -604,6 +881,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],
@@ -659,10 +957,13 @@ class DocumentController(QObject):
pairs = [(source, clone_component(source)) for source in source_components]
id_map = {source.id: clone.id for source, clone in pairs}
blocks = {}
for _source, clone in pairs:
used = list(owner.graph.blocks.values())
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
clone.x += offset.x()
clone.y += offset.y()
blocks[clone.id] = clone
used.append(clone)
connections = {}
for source in source_connections:
if source.source.block not in id_map or source.target.block not in id_map:
@@ -684,6 +985,68 @@ class DocumentController(QObject):
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def paste_components_to(
self,
owner_id: str | None,
source_components: list[Component],
source_connections: list[Connection] | None = None,
) -> list[str]:
"""Clone components into the document root or a graph at origin."""
if self.document is None:
raise ValueError("Open or create a document before pasting components")
if owner_id is None:
siblings = self.document.roots.values()
else:
owner = self.document.find_component(owner_id)
if owner is None or owner.implementation_kind != "graph":
raise ValueError("Components can only be pasted into a graph")
siblings = owner.graph.blocks.values()
pairs = [(source, clone_component(source)) for source in source_components]
if not pairs:
return []
id_map = {source.id: clone.id for source, clone in pairs}
used = list(siblings)
minimum_x = min(source.x for source, _clone in pairs)
minimum_y = min(source.y for source, _clone in pairs)
blocks: dict[str, Component] = {}
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
if owner_id is not None:
clone.x = source.x - minimum_x
clone.y = source.y - minimum_y
blocks[clone.id] = clone
used.append(clone)
connections: dict[str, Connection] = {}
if owner_id is not None:
for source in source_connections or []:
if source.source.block not in id_map or source.target.block not in id_map:
continue
properties = deepcopy(source.properties)
for point in properties.get("waypoints", []):
if isinstance(point, dict):
point["x"] = float(point.get("x", 0)) - minimum_x
point["y"] = float(point.get("y", 0)) - minimum_y
connection = Connection(
id=str(uuid4()),
source=Endpoint(
block=id_map[source.source.block], port=source.source.port
),
target=Endpoint(
block=id_map[source.target.block], port=source.target.port
),
name=source.name,
properties=properties,
)
connections[connection.id] = connection
self.undo_stack.push(
PasteSelectionCommand(self, owner_id, blocks, connections)
)
return list(blocks)
def _graph_for(self, owner_id: str):
if self.document is None:
raise ValueError("There is no open document")
@@ -692,6 +1055,26 @@ class DocumentController(QObject):
raise ValueError("The containing component is no longer in the document")
return owner.graph
@staticmethod
def _available_component_name(
base: str, components, start: int = 0
) -> str:
used = {component.name for component in components}
number = start
while f"{base}{number}" in used:
number += 1
return f"{base}{number}"
def _component_siblings(self, component_id: str):
if self.document is None:
return ()
parent = self.document.find_parent(component_id)
return (
parent.graph.blocks.values()
if parent is not None
else self.document.roots.values()
)
def _insert_component(self, owner_id: str | None, component: Component) -> None:
if self.document is None:
raise ValueError("There is no open document")
@@ -738,6 +1121,36 @@ class DocumentController(QObject):
self.connectionAdded.emit(connection.id)
self.documentReset.emit()
def _split_connection(
self,
owner_id: str,
original_id: str,
junction: Junction,
first: Connection,
second: Connection,
) -> None:
graph = self._graph_for(owner_id)
graph.connections.pop(original_id, None)
graph.junctions[junction.id] = junction
graph.connections[first.id] = first
graph.connections[second.id] = second
self.documentReset.emit()
def _restore_split_connection(
self,
owner_id: str,
original: Connection,
junction_id: str,
first_id: str,
second_id: str,
) -> None:
graph = self._graph_for(owner_id)
graph.connections.pop(first_id, None)
graph.connections.pop(second_id, None)
graph.junctions.pop(junction_id, None)
graph.connections[original.id] = original
self.documentReset.emit()
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
self._graph_for(owner_id).connections.pop(connection_id, None)
if owner_id == self.active_component_id:
@@ -760,7 +1173,16 @@ class DocumentController(QObject):
self, owner_id: str, item_kind: str, item_id: str, values: dict
) -> None:
graph = self._graph_for(owner_id)
if item_kind == "connection":
if item_kind == "junction_geometry":
item = graph.junctions.get(item_id)
if item is not None:
item.x, item.y = float(values["x"]), float(values["y"])
elif item_kind == "connection_data":
item = graph.connections.get(item_id)
if item is not None:
item.name = values["name"]
item.properties = deepcopy(values["properties"])
elif item_kind == "connection":
item = graph.connections.get(item_id)
if item is not None:
item.properties = deepcopy(values)
@@ -784,6 +1206,28 @@ class DocumentController(QObject):
if owner_id == self.active_component_id:
self.graphItemChanged.emit(item_kind, item_id)
def _set_simulation_settings(self, owner_id: str, settings: dict) -> None:
owner = self.document.find_component(owner_id) if self.document else None
if owner is not None and owner.implementation_kind == "graph":
owner.graph.simulation_settings = deepcopy(settings)
self.documentReset.emit()
def _set_graph_parameters(self, values: dict[str, list[dict]]) -> None:
if self.document is None:
return
changed_text_components: list[str] = []
for component_id, parameters in values.items():
component = self.document.find_component(component_id)
if component is None:
continue
component.parameters = [Parameter.from_dict(item) for item in parameters]
if component.implementation_kind == "text":
changed_text_components.append(component_id)
self.document.validate()
self.documentReset.emit()
for component_id in changed_text_components:
self.textDefinitionChanged.emit(component_id)
def _replace_component(self, old_id: str, replacement: Component) -> None:
if self.document is None:
return
@@ -877,10 +1321,30 @@ class DocumentController(QObject):
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
component.show_subtree_in_library = values["show_subtree"]
component.properties = deepcopy(values["properties"])
self.documentReset.emit()
if component_id == self.active_component_id:
self.activeGraphChanged.emit()
def _set_component_properties(self, component_id: str, properties: dict) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is not None:
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,
@@ -956,7 +1420,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()
if component_id == self.active_component_id:
self.activeGraphChanged.emit()
self.textDefinitionChanged.emit(component_id)

View File

@@ -17,6 +17,7 @@ class ComponentOptionsDialog(QDialog):
self.ui.nameEdit.setText(component.name)
self.ui.editIconButton.clicked.connect(self.edit_icon)
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
self.ui.showNameCheckBox.setChecked(bool(component.properties.get("showName", False)))
def edit_icon(self) -> None:
working = Component.from_dict(self.component.to_dict())

View File

@@ -0,0 +1,18 @@
from PySide6.QtWidgets import QDialog
from bedit.gui.generated.ui_connection_chooser_dialog import Ui_ConnectionChooserDialog
class ConnectionChooserDialog(QDialog):
def __init__(self, labels: list[str], default_index: int = 0, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_ConnectionChooserDialog()
self.ui.setupUi(self)
self.ui.connectionList.addItems(labels)
if labels:
self.ui.connectionList.setCurrentRow(max(0, min(default_index, len(labels) - 1)))
self.ui.buttonBox.button(self.ui.buttonBox.StandardButton.Ok).setEnabled(bool(labels))
@property
def selected_index(self) -> int:
return self.ui.connectionList.currentRow()

View File

@@ -0,0 +1,93 @@
from copy import deepcopy
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QDialog, QStyledItemDelegate, QTreeWidgetItem
from bedit.core.model import Component
from bedit.gui.generated.ui_graph_parameters_dialog import Ui_GraphParametersDialog
COMPONENT_ID_ROLE = Qt.ItemDataRole.UserRole
PARAMETER_ID_ROLE = Qt.ItemDataRole.UserRole + 1
class ValueColumnDelegate(QStyledItemDelegate):
"""Allow editing only in the parameter value column."""
def createEditor(self, parent, option, index): # noqa: N802 (Qt API name)
parameter_id = index.siblingAtColumn(0).data(PARAMETER_ID_ROLE)
if index.column() != 2 or parameter_id is None:
return None
return super().createEditor(parent, option, index)
class GraphParametersDialog(QDialog):
"""Edit every parameter value in an active component subtree."""
def __init__(self, root: Component, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_GraphParametersDialog()
self.ui.setupUi(self)
self.ui.parameterTree.setItemDelegate(ValueColumnDelegate(self.ui.parameterTree))
self.setWindowTitle(f"Graph Parameters — {root.name}")
self._parameters = {
component.id: deepcopy(component.parameters)
for component in self._walk(root)
}
self._populate(root)
self.ui.parameterTree.expandAll()
self.ui.parameterTree.resizeColumnToContents(0)
self.ui.parameterTree.resizeColumnToContents(1)
@staticmethod
def _walk(component: Component):
yield component
if component.implementation_kind == "graph":
for child in component.graph.blocks.values():
yield from GraphParametersDialog._walk(child)
def _populate(self, root: Component) -> None:
self.ui.parameterTree.clear()
def add_component(component: Component, parent: QTreeWidgetItem | None) -> None:
item = QTreeWidgetItem([component.name, "", ""])
item.setData(0, COMPONENT_ID_ROLE, component.id)
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
if parent is None:
self.ui.parameterTree.addTopLevelItem(item)
else:
parent.addChild(item)
for parameter in self._parameters[component.id]:
parameter_item = QTreeWidgetItem(
[parameter.name, parameter.type, parameter.value]
)
parameter_item.setData(0, COMPONENT_ID_ROLE, component.id)
parameter_item.setData(0, PARAMETER_ID_ROLE, parameter.id)
parameter_item.setFlags(
parameter_item.flags() | Qt.ItemFlag.ItemIsEditable
)
item.addChild(parameter_item)
if component.implementation_kind == "graph":
for child in component.graph.blocks.values():
add_component(child, item)
add_component(root, None)
@property
def parameter_values(self) -> dict[str, dict[str, str]]:
values: dict[str, dict[str, str]] = {}
iterator = self.ui.parameterTree.invisibleRootItem()
def collect(parent: QTreeWidgetItem) -> None:
for index in range(parent.childCount()):
item = parent.child(index)
parameter_id = item.data(0, PARAMETER_ID_ROLE)
if parameter_id is not None:
component_id = item.data(0, COMPONENT_ID_ROLE)
values.setdefault(component_id, {})[parameter_id] = item.text(2)
collect(item)
collect(iterator)
return values

View File

@@ -1,6 +1,7 @@
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QCheckBox,
QFormLayout,
QLineEdit,
QMessageBox,
@@ -11,7 +12,15 @@ from PySide6.QtWidgets import (
class ItemOptionsDialog(QDialog):
"""Small, extensible options dialog shared by ports and connections."""
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None:
def __init__(
self,
title: str,
name: str,
parent=None,
*,
name_required: bool = True,
show_name: bool | None = None,
) -> None:
super().__init__(parent)
self.name_required = name_required
self.setWindowTitle(title)
@@ -21,6 +30,11 @@ class ItemOptionsDialog(QDialog):
self.form = QFormLayout()
self.name_edit = QLineEdit(name, self)
self.form.addRow("Name:", self.name_edit)
self.show_name_check = None
if show_name is not None:
self.show_name_check = QCheckBox("Show name below connection", self)
self.show_name_check.setChecked(show_name)
self.form.addRow("", self.show_name_check)
layout.addLayout(self.form)
buttons = QDialogButtonBox(
@@ -35,6 +49,10 @@ class ItemOptionsDialog(QDialog):
def name(self) -> str:
return self.name_edit.text().strip()
@property
def show_name(self) -> bool:
return bool(self.show_name_check and self.show_name_check.isChecked())
def accept(self) -> None:
if self.name_required and not self.name:
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")

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

@@ -36,6 +36,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.textEdited.connect(self._store_current)
self.ui.typeCombo.currentIndexChanged.connect(self._store_current)
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
self.ui.multipleConnectionsCheckBox.toggled.connect(self._store_current)
self.ui.portSplitter.setSizes([250, 370])
if read_only:
self.ui.addPortButton.setEnabled(False)
@@ -43,6 +44,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setReadOnly(True)
self.ui.typeCombo.setEnabled(False)
self.ui.orientationCombo.setEnabled(False)
self.ui.multipleConnectionsCheckBox.setEnabled(False)
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
self._rebuild_list(0 if self.ports else -1)
@@ -72,6 +74,9 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setText(port.name)
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation))
self.ui.multipleConnectionsCheckBox.setChecked(
port.allows_multiple_connections
)
else:
self.ui.nameEdit.clear()
self._loading = False
@@ -83,6 +88,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setEnabled(enabled)
self.ui.typeCombo.setEnabled(enabled)
self.ui.orientationCombo.setEnabled(enabled)
self.ui.multipleConnectionsCheckBox.setEnabled(enabled)
def _store_current(self) -> None:
row = self.ui.portList.currentRow()
@@ -91,6 +97,7 @@ class PortOptionsDialog(QDialog):
port, _orientation = self.ports[row]
port.name = self.ui.nameEdit.text()
port.type = self.ui.typeCombo.currentData()
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
self.ports[row] = (port, self.ui.orientationCombo.currentData())
self.ui.portList.item(row).setText(
f"{port.name} [{self.ports[row][1]}, {port.type}]"

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 default_library_paths
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):
@@ -24,7 +26,9 @@ class SettingsDialog(QDialog):
self.ui.addLibraryFileButton.clicked.connect(self._add_library_file)
self.ui.addLibraryFolderButton.clicked.connect(self._add_library_folder)
self.ui.removeLibraryPathButton.clicked.connect(self._remove_library_path)
self.ui.browseOpenModelicaButton.clicked.connect(self._browse_openmodelica)
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 +47,48 @@ 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.ui.openModelicaPathEdit.setText(self.openmodelica_path(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):
@@ -56,8 +100,22 @@ class SettingsDialog(QDialog):
settings = settings if settings is not None else application_settings()
value = settings.value("libraries/paths", default_library_paths())
if isinstance(value, str):
return [value]
return [str(path) for path in value]
paths = [value]
else:
paths = [str(path) for path in value]
bundled = bundled_library_path()
migrated = [
str(bundled)
if not Path(path).exists()
and Path(path).parent == bundled.parent
and Path(path).suffix.lower() == ".json"
else path
for path in paths
]
if migrated != paths:
settings.setValue("libraries/paths", migrated)
settings.sync()
return migrated
@staticmethod
def graph_grid_size(settings: QSettings | None = None) -> int:
@@ -74,12 +132,31 @@ class SettingsDialog(QDialog):
settings = settings if settings is not None else application_settings()
return settings.value("grid/iconSize", 8, type=int)
@staticmethod
def openmodelica_path(settings: QSettings | None = None) -> str:
settings = settings if settings is not None else application_settings()
return str(settings.value("simulation/openModelicaPath", "") or "")
def _browse_openmodelica(self) -> None:
current = self.ui.openModelicaPathEdit.text().strip()
start = str(Path(current).expanduser().parent) if current else ""
path, _ = QFileDialog.getOpenFileName(
self,
"Select OpenModelica executable",
start,
"OpenModelica compiler (omc omc.exe);;All files (*)",
)
if path:
# Keep symlink paths such as ~/.local/bin/omc intact; resolving them
# could turn the selected launcher into an unrelated container script.
self.ui.openModelicaPathEdit.setText(str(Path(path).expanduser().absolute()))
def _add_library_file(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
"Add library",
"",
"BEdit libraries (*.json);;All files (*)",
"BEdit libraries (*.beb *.bedit.json *.json);;All files (*)",
)
if path:
self._append_unique_path(path)
@@ -120,6 +197,28 @@ 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())
self.settings.setValue(
"simulation/openModelicaPath",
self.ui.openModelicaPathEdit.text().strip(),
)
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,60 @@
from copy import deepcopy
from PySide6.QtWidgets import QButtonGroup, QDialog
from bedit.gui.generated.ui_simulation_settings_dialog import Ui_SimulationSettingsDialog
class SimulationSettingsDialog(QDialog):
def __init__(self, settings: dict, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationSettingsDialog()
self.ui.setupUi(self)
self._settings = deepcopy(settings)
self.interval_mode_group = QButtonGroup(self)
self.interval_mode_group.setExclusive(True)
self.interval_mode_group.addButton(self.ui.numberOfIntervalsRadioButton)
self.interval_mode_group.addButton(self.ui.intervalTimeRadioButton)
self.ui.startTimeSpinBox.setValue(float(settings.get("startTime", 0.0)))
self.ui.stopTimeSpinBox.setValue(float(settings.get("stopTime", 1.0)))
self.ui.numberOfIntervalsSpinBox.setValue(
int(settings.get("numberOfIntervals", 500))
)
self.ui.intervalTimeSpinBox.setValue(float(settings.get("intervalTime", 0.002)))
interval_mode = settings.get("intervalMode", "numberOfIntervals")
if interval_mode == "intervalTime":
self.ui.intervalTimeRadioButton.setChecked(True)
else:
self.ui.numberOfIntervalsRadioButton.setChecked(True)
self.ui.numberOfIntervalsRadioButton.toggled.connect(
self._update_interval_fields
)
self.ui.intervalTimeRadioButton.toggled.connect(self._update_interval_fields)
self._update_interval_fields()
def _update_interval_fields(self) -> None:
use_number = self.ui.numberOfIntervalsRadioButton.isChecked()
self.ui.numberOfIntervalsSpinBox.setEnabled(use_number)
self.ui.intervalTimeSpinBox.setEnabled(not use_number)
@property
def settings(self) -> dict:
values = deepcopy(self._settings)
values.update(
{
"startTime": self.ui.startTimeSpinBox.value(),
"stopTime": self.ui.stopTimeSpinBox.value(),
"intervalMode": (
"numberOfIntervals"
if self.ui.numberOfIntervalsRadioButton.isChecked()
else "intervalTime"
),
"numberOfIntervals": self.ui.numberOfIntervalsSpinBox.value(),
"intervalTime": self.ui.intervalTimeSpinBox.value(),
}
)
return values

View File

@@ -0,0 +1 @@
"""Reusable editing widgets."""

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

@@ -0,0 +1,239 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
from bedit.core.model import Parameter, Port
from bedit.core.port_types import PortTypeRegistry
from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor
ID_ROLE = Qt.ItemDataRole.UserRole
PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
class TextDefinitionEditor(QWidget):
"""Editor for a text component's Modelica source, ports, and parameters."""
modifiedChanged = Signal(bool)
definitionEdited = Signal()
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_TextDefinitionEditor()
self.ui.setupUi(self)
self._modified = False
self._loading = False
self.ui.portsTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
self.ui.parametersTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
self.ui.columnSplitter.setSizes([560, 340])
self.ui.sourceSplitter.setSizes([180, 180, 240])
self.ui.definitionSplitter.setSizes([300, 300])
self.ui.declarationsEdit.textChanged.connect(self._mark_modified)
self.ui.initialEquationsEdit.textChanged.connect(self._mark_modified)
self.ui.equationsEdit.textChanged.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)
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
self.ui.portsTable.itemSelectionChanged.connect(self._update_buttons)
self.ui.parametersTable.itemSelectionChanged.connect(self._update_buttons)
self._update_buttons()
@property
def is_modified(self) -> bool:
return self._modified
@property
def equations(self) -> str:
return self.ui.equationsEdit.toPlainText()
@property
def declarations(self) -> str:
return self.ui.declarationsEdit.toPlainText()
@property
def initial_equations(self) -> str:
return self.ui.initialEquationsEdit.toPlainText()
@property
def ports(self) -> tuple[list[Port], list[Port]]:
inputs: list[Port] = []
outputs: list[Port] = []
for row in range(self.ui.portsTable.rowCount()):
name_item = self.ui.portsTable.item(row, 0)
type_combo = self.ui.portsTable.cellWidget(row, 1)
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
multiple_check = self.ui.portsTable.cellWidget(row, 3)
port = Port(
id=name_item.data(ID_ROLE),
name=name_item.text().strip(),
type=type_combo.currentData(),
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
allows_multiple_connections=multiple_check.isChecked(),
)
target = inputs if orientation_combo.currentData() == "input" else outputs
target.append(port)
return inputs, outputs
@property
def parameters(self) -> list[Parameter]:
table = self.ui.parametersTable
return [
Parameter(
id=table.item(row, 0).data(ID_ROLE),
name=table.item(row, 0).text().strip(),
type=table.item(row, 1).text().strip(),
value=table.item(row, 2).text(),
)
for row in range(table.rowCount())
]
def set_definition(
self,
declarations: str,
initial_equations: str,
equations: str,
inputs: list[Port],
outputs: list[Port],
parameters: list[Parameter],
) -> None:
self._loading = True
self.ui.declarationsEdit.setPlainText(declarations)
self.ui.initialEquationsEdit.setPlainText(initial_equations)
self.ui.equationsEdit.setPlainText(equations)
self.ui.portsTable.setRowCount(0)
for port in inputs:
self._append_port(port, "input")
for port in outputs:
self._append_port(port, "output")
self.ui.parametersTable.setRowCount(0)
for parameter in parameters:
self._append_parameter(parameter)
self._set_editor_symbols(inputs, outputs, parameters)
self._loading = False
self.set_modified(False)
self._update_buttons()
def set_modified(self, modified: bool) -> None:
if self._modified != modified:
self._modified = modified
self.modifiedChanged.emit(modified)
def _mark_modified(self, *_args) -> None:
if not self._loading:
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._set_editor_symbols(inputs, outputs, self.parameters)
def _set_editor_symbols(
self,
inputs: list[Port],
outputs: list[Port],
parameters: list[Parameter],
) -> None:
symbols = (
[port.name for port in inputs],
[port.name for port in outputs],
[parameter.name for parameter in parameters],
)
self.ui.declarationsEdit.set_symbols(*symbols)
self.ui.initialEquationsEdit.set_symbols(*symbols)
self.ui.equationsEdit.set_symbols(*symbols)
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._symbols_modified)
return combo
def _append_port(self, port: Port, orientation: str) -> None:
table = self.ui.portsTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(port.name)
name.setData(ID_ROLE, port.id)
name.setData(PROPERTIES_ROLE, deepcopy(port.properties))
table.setItem(row, 0, name)
types = [(item.display_name, item.id) for item in PortTypeRegistry.all()]
table.setCellWidget(row, 1, self._new_combo(types, port.type))
table.setCellWidget(
row,
2,
self._new_combo([("Input", "input"), ("Output", "output")], orientation),
)
multiple = QCheckBox("Any", self)
multiple.setChecked(port.allows_multiple_connections)
multiple.toggled.connect(self._symbols_modified)
table.setCellWidget(row, 3, multiple)
def _append_parameter(self, parameter: Parameter) -> None:
table = self.ui.parametersTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(parameter.name)
name.setData(ID_ROLE, parameter.id)
table.setItem(row, 0, name)
table.setItem(row, 1, QTableWidgetItem(parameter.type))
table.setItem(row, 2, QTableWidgetItem(parameter.value))
def add_port(self) -> None:
port = Port(
id=f"port-{uuid4().hex[:8]}",
name=f"Port {self.ui.portsTable.rowCount() + 1}",
type="signal",
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
)
self._loading = True
self._append_port(port, "input")
self._loading = False
self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1)
self._symbols_modified()
def remove_port(self) -> None:
row = self.ui.portsTable.currentRow()
if row >= 0:
self.ui.portsTable.removeRow(row)
self._symbols_modified()
self._update_buttons()
def add_parameter(self) -> None:
parameter = Parameter(
id=f"parameter-{uuid4().hex[:8]}",
name=f"Parameter {self.ui.parametersTable.rowCount() + 1}",
)
self._loading = True
self._append_parameter(parameter)
self._loading = False
self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1)
self._symbols_modified()
def remove_parameter(self) -> None:
row = self.ui.parametersTable.currentRow()
if row >= 0:
self.ui.parametersTable.removeRow(row)
self._symbols_modified()
self._update_buttons()
def _update_buttons(self) -> None:
self.ui.removePortButton.setEnabled(self.ui.portsTable.currentRow() >= 0)
self.ui.removeParameterButton.setEnabled(
self.ui.parametersTable.currentRow() >= 0
)

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,11 @@ class Ui_ComponentOptionsDialog(object):
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
self.showNameCheckBox = QCheckBox(ComponentOptionsDialog)
self.showNameCheckBox.setObjectName(u"showNameCheckBox")
self.optionsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.showNameCheckBox)
self.dialogLayout.addLayout(self.optionsForm)
@@ -85,5 +90,6 @@ class Ui_ComponentOptionsDialog(object):
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
#endif // QT_CONFIG(tooltip)
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
self.showNameCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show name below component", None))
# retranslateUi

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'connection_chooser_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,
QLabel, QListWidget, QListWidgetItem, QSizePolicy,
QVBoxLayout, QWidget)
class Ui_ConnectionChooserDialog(object):
def setupUi(self, ConnectionChooserDialog):
if not ConnectionChooserDialog.objectName():
ConnectionChooserDialog.setObjectName(u"ConnectionChooserDialog")
ConnectionChooserDialog.resize(460, 280)
self.dialogLayout = QVBoxLayout(ConnectionChooserDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.promptLabel = QLabel(ConnectionChooserDialog)
self.promptLabel.setObjectName(u"promptLabel")
self.dialogLayout.addWidget(self.promptLabel)
self.connectionList = QListWidget(ConnectionChooserDialog)
self.connectionList.setObjectName(u"connectionList")
self.connectionList.setAlternatingRowColors(True)
self.dialogLayout.addWidget(self.connectionList)
self.buttonBox = QDialogButtonBox(ConnectionChooserDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(ConnectionChooserDialog)
self.buttonBox.accepted.connect(ConnectionChooserDialog.accept)
self.buttonBox.rejected.connect(ConnectionChooserDialog.reject)
self.connectionList.itemDoubleClicked.connect(ConnectionChooserDialog.accept)
QMetaObject.connectSlotsByName(ConnectionChooserDialog)
# setupUi
def retranslateUi(self, ConnectionChooserDialog):
ConnectionChooserDialog.setWindowTitle(QCoreApplication.translate("ConnectionChooserDialog", u"Select a Connection", None))
self.promptLabel.setText(QCoreApplication.translate("ConnectionChooserDialog", u"Select a connection:", None))
# retranslateUi

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'graph_parameters_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,
QHeaderView, QLabel, QSizePolicy, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_GraphParametersDialog(object):
def setupUi(self, GraphParametersDialog):
if not GraphParametersDialog.objectName():
GraphParametersDialog.setObjectName(u"GraphParametersDialog")
GraphParametersDialog.resize(620, 480)
self.dialogLayout = QVBoxLayout(GraphParametersDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.descriptionLabel = QLabel(GraphParametersDialog)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.dialogLayout.addWidget(self.descriptionLabel)
self.parameterTree = QTreeWidget(GraphParametersDialog)
self.parameterTree.setObjectName(u"parameterTree")
self.parameterTree.setAlternatingRowColors(True)
self.parameterTree.setRootIsDecorated(True)
self.parameterTree.setColumnCount(3)
self.dialogLayout.addWidget(self.parameterTree)
self.buttonBox = QDialogButtonBox(GraphParametersDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(GraphParametersDialog)
self.buttonBox.accepted.connect(GraphParametersDialog.accept)
self.buttonBox.rejected.connect(GraphParametersDialog.reject)
QMetaObject.connectSlotsByName(GraphParametersDialog)
# setupUi
def retranslateUi(self, GraphParametersDialog):
GraphParametersDialog.setWindowTitle(QCoreApplication.translate("GraphParametersDialog", u"Graph Parameters", None))
self.descriptionLabel.setText(QCoreApplication.translate("GraphParametersDialog", u"Edit parameter values throughout the active component tree.", None))
___qtreewidgetitem = self.parameterTree.headerItem()
___qtreewidgetitem.setText(2, QCoreApplication.translate("GraphParametersDialog", u"Value", None))
___qtreewidgetitem.setText(1, QCoreApplication.translate("GraphParametersDialog", u"Type", None))
___qtreewidgetitem.setText(0, QCoreApplication.translate("GraphParametersDialog", u"Component / Parameter", None))
# retranslateUi

View File

@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogBu
QWidget)
from bedit.gui.graphics.icon_canvas import IconCanvasView
from . import resources_rc
class Ui_IconEditorDialog(object):
def setupUi(self, IconEditorDialog):
@@ -33,6 +34,9 @@ class Ui_IconEditorDialog(object):
self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout")
self.pointerButton = QToolButton(IconEditorDialog)
self.pointerButton.setObjectName(u"pointerButton")
icon = QIcon()
icon.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerButton.setIcon(icon)
self.pointerButton.setCheckable(True)
self.pointerButton.setChecked(True)
@@ -45,36 +49,54 @@ class Ui_IconEditorDialog(object):
self.addRectangleButton = QToolButton(IconEditorDialog)
self.addRectangleButton.setObjectName(u"addRectangleButton")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addRectangleButton.setIcon(icon1)
self.addRectangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addRectangleButton)
self.addCircleButton = QToolButton(IconEditorDialog)
self.addCircleButton.setObjectName(u"addCircleButton")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addCircleButton.setIcon(icon2)
self.addCircleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addCircleButton)
self.addEllipseButton = QToolButton(IconEditorDialog)
self.addEllipseButton.setObjectName(u"addEllipseButton")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/draw-ellipse.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addEllipseButton.setIcon(icon3)
self.addEllipseButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addEllipseButton)
self.addLineButton = QToolButton(IconEditorDialog)
self.addLineButton.setObjectName(u"addLineButton")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addLineButton.setIcon(icon4)
self.addLineButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addLineButton)
self.addTriangleButton = QToolButton(IconEditorDialog)
self.addTriangleButton.setObjectName(u"addTriangleButton")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/draw-triangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addTriangleButton.setIcon(icon5)
self.addTriangleButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTriangleButton)
self.addTextButton = QToolButton(IconEditorDialog)
self.addTextButton.setObjectName(u"addTextButton")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.addTextButton.setIcon(icon6)
self.addTextButton.setCheckable(True)
self.shapeToolbarLayout.addWidget(self.addTextButton)
@@ -85,16 +107,25 @@ class Ui_IconEditorDialog(object):
self.zoomInButton = QToolButton(IconEditorDialog)
self.zoomInButton.setObjectName(u"zoomInButton")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.zoomInButton.setIcon(icon7)
self.shapeToolbarLayout.addWidget(self.zoomInButton)
self.zoomOutButton = QToolButton(IconEditorDialog)
self.zoomOutButton.setObjectName(u"zoomOutButton")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.zoomOutButton.setIcon(icon8)
self.shapeToolbarLayout.addWidget(self.zoomOutButton)
self.centerButton = QToolButton(IconEditorDialog)
self.centerButton.setObjectName(u"centerButton")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.centerButton.setIcon(icon9)
self.shapeToolbarLayout.addWidget(self.centerButton)
@@ -142,18 +173,18 @@ class Ui_IconEditorDialog(object):
self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None))
self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None))
self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None))
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
#if QT_CONFIG(tooltip)
self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None))
#endif // QT_CONFIG(tooltip)
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None))
#if QT_CONFIG(tooltip)
self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None))
#endif // QT_CONFIG(tooltip)
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None))
#if QT_CONFIG(tooltip)
self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None))
#endif // QT_CONFIG(tooltip)
self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None))
self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None))
self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None))
# retranslateUi

View File

@@ -18,10 +18,11 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
QHeaderView, QLabel, QMainWindow, QMenu,
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy,
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
QMenuBar, QPlainTextEdit, QSizePolicy, QSpacerItem,
QSplitter, QStackedWidget, QTabWidget, QToolBar,
QToolButton, QTreeView, QVBoxLayout, QWidget)
from bedit.gui.editors.text_definition import TextDefinitionEditor
from bedit.gui.graphics.workspace import GraphWorkspaceView
from . import resources_rc
@@ -30,75 +31,107 @@ class Ui_MainWindow(object):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(1209, 777)
self.actionSimulationSettings = QAction(MainWindow)
self.actionSimulationSettings.setObjectName(u"actionSimulationSettings")
icon = QIcon()
icon.addFile(u":/icons/icons/preferences-system.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationSettings.setIcon(icon)
self.actionGraphParameters = QAction(MainWindow)
self.actionGraphParameters.setObjectName(u"actionGraphParameters")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/view-form-table.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionGraphParameters.setIcon(icon1)
self.actionCompose = QAction(MainWindow)
self.actionCompose.setObjectName(u"actionCompose")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/run-build.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCompose.setIcon(icon2)
self.actionRunSimulation = QAction(MainWindow)
self.actionRunSimulation.setObjectName(u"actionRunSimulation")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/media-playback-start.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRunSimulation.setIcon(icon3)
self.actionSimulationWindow = QAction(MainWindow)
self.actionSimulationWindow.setObjectName(u"actionSimulationWindow")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSimulationWindow.setIcon(icon4)
self.actionExportModel = QAction(MainWindow)
self.actionExportModel.setObjectName(u"actionExportModel")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionExportModel.setIcon(icon5)
self.actionNew = QAction(MainWindow)
self.actionNew.setObjectName(u"actionNew")
icon = QIcon()
icon.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon)
icon6 = QIcon()
icon6.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionNew.setIcon(icon6)
self.actionRotateClockwise = QAction(MainWindow)
self.actionRotateClockwise.setObjectName(u"actionRotateClockwise")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon1)
icon7 = QIcon()
icon7.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRotateClockwise.setIcon(icon7)
self.actionZoomIn = QAction(MainWindow)
self.actionZoomIn.setObjectName(u"actionZoomIn")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon2)
icon8 = QIcon()
icon8.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomIn.setIcon(icon8)
self.actionZoomOut = QAction(MainWindow)
self.actionZoomOut.setObjectName(u"actionZoomOut")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon3)
icon9 = QIcon()
icon9.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionZoomOut.setIcon(icon9)
self.actionCenterView = QAction(MainWindow)
self.actionCenterView.setObjectName(u"actionCenterView")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon4)
icon10 = QIcon()
icon10.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCenterView.setIcon(icon10)
self.actionOpen = QAction(MainWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon5)
icon11 = QIcon()
icon11.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon11)
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")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon6)
icon12 = QIcon()
icon12.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon12)
self.actionSaveAs = QAction(MainWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon7)
self.actionSaveAs.setIcon(icon5)
self.actionExit = QAction(MainWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionClose = QAction(MainWindow)
self.actionClose.setObjectName(u"actionClose")
self.actionUndo = QAction(MainWindow)
self.actionUndo.setObjectName(u"actionUndo")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon8)
icon13 = QIcon()
icon13.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon13)
self.actionRedo = QAction(MainWindow)
self.actionRedo.setObjectName(u"actionRedo")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon9)
icon14 = QIcon()
icon14.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon14)
self.actionCut = QAction(MainWindow)
self.actionCut.setObjectName(u"actionCut")
icon10 = QIcon()
icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon10)
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCut.setIcon(icon15)
self.actionCopy = QAction(MainWindow)
self.actionCopy.setObjectName(u"actionCopy")
icon11 = QIcon()
icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon11)
icon16 = QIcon()
icon16.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCopy.setIcon(icon16)
self.actionPaste = QAction(MainWindow)
self.actionPaste.setObjectName(u"actionPaste")
icon12 = QIcon()
icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon12)
icon17 = QIcon()
icon17.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionPaste.setIcon(icon17)
self.actionSelectAll = QAction(MainWindow)
self.actionSelectAll.setObjectName(u"actionSelectAll")
self.actionDelete = QAction(MainWindow)
@@ -188,18 +221,18 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2)
self.navigateUpButton = QToolButton(self.workspaceHeader)
self.navigateUpButton.setObjectName(u"navigateUpButton")
icon13 = QIcon()
icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon13)
icon18 = QIcon()
icon18.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateUpButton.setIcon(icon18)
self.workspaceHeaderLayout.addWidget(self.navigateUpButton)
self.navigateDownButton = QToolButton(self.workspaceHeader)
self.navigateDownButton.setObjectName(u"navigateDownButton")
self.navigateDownButton.setEnabled(False)
icon14 = QIcon()
icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon14)
icon19 = QIcon()
icon19.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.navigateDownButton.setIcon(icon19)
self.workspaceHeaderLayout.addWidget(self.navigateDownButton)
@@ -217,17 +250,11 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
self.applyJsonButton = QPushButton(self.workspaceHeader)
self.applyJsonButton.setObjectName(u"applyJsonButton")
self.applyJsonButton.setVisible(False)
self.workspaceHeaderLayout.addWidget(self.applyJsonButton)
self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton")
icon15 = QIcon()
icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon15)
icon20 = QIcon()
icon20.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.pointerToolButton.setIcon(icon20)
self.pointerToolButton.setCheckable(True)
self.pointerToolButton.setChecked(True)
@@ -235,71 +262,54 @@ class Ui_MainWindow(object):
self.connectToolButton = QToolButton(self.workspaceHeader)
self.connectToolButton.setObjectName(u"connectToolButton")
icon21 = QIcon()
icon21.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.connectToolButton.setIcon(icon21)
self.connectToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.connectToolButton)
self.boxToolButton = QToolButton(self.workspaceHeader)
self.boxToolButton.setObjectName(u"boxToolButton")
icon16 = QIcon()
icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon16)
icon22 = QIcon()
icon22.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.boxToolButton.setIcon(icon22)
self.boxToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.boxToolButton)
self.lineToolButton = QToolButton(self.workspaceHeader)
self.lineToolButton.setObjectName(u"lineToolButton")
icon17 = QIcon()
icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon17)
icon23 = QIcon()
icon23.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.lineToolButton.setIcon(icon23)
self.lineToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.lineToolButton)
self.textToolButton = QToolButton(self.workspaceHeader)
self.textToolButton.setObjectName(u"textToolButton")
icon18 = QIcon()
icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon18)
icon24 = QIcon()
icon24.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.textToolButton.setIcon(icon24)
self.textToolButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.textToolButton)
self.rotateToolButton = QToolButton(self.workspaceHeader)
self.rotateToolButton.setObjectName(u"rotateToolButton")
self.rotateToolButton.setIcon(icon1)
self.rotateToolButton.setIcon(icon7)
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
self.routingLabel = QLabel(self.workspaceHeader)
self.routingLabel.setObjectName(u"routingLabel")
self.workspaceHeaderLayout.addWidget(self.routingLabel)
self.directRoutingButton = QToolButton(self.workspaceHeader)
self.directRoutingButton.setObjectName(u"directRoutingButton")
self.directRoutingButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.directRoutingButton)
self.angledRoutingButton = QToolButton(self.workspaceHeader)
self.angledRoutingButton.setObjectName(u"angledRoutingButton")
self.angledRoutingButton.setCheckable(True)
self.angledRoutingButton.setChecked(True)
self.workspaceHeaderLayout.addWidget(self.angledRoutingButton)
self.splineRoutingButton = QToolButton(self.workspaceHeader)
self.splineRoutingButton.setObjectName(u"splineRoutingButton")
self.splineRoutingButton.setCheckable(True)
self.workspaceHeaderLayout.addWidget(self.splineRoutingButton)
self.workspaceEditorLayout.addWidget(self.workspaceHeader)
self.workspaceStack = QStackedWidget(self.workspace)
self.workspaceVerticalSplitter = QSplitter(self.workspace)
self.workspaceVerticalSplitter.setObjectName(u"workspaceVerticalSplitter")
self.workspaceVerticalSplitter.setOrientation(Qt.Orientation.Vertical)
self.workspaceVerticalSplitter.setChildrenCollapsible(False)
self.workspaceStack = QStackedWidget(self.workspaceVerticalSplitter)
self.workspaceStack.setObjectName(u"workspaceStack")
self.graphPage = QWidget()
self.graphPage.setObjectName(u"graphPage")
@@ -312,18 +322,17 @@ class Ui_MainWindow(object):
self.graphPageLayout.addWidget(self.graphView)
self.workspaceStack.addWidget(self.graphPage)
self.jsonPage = QWidget()
self.jsonPage.setObjectName(u"jsonPage")
self.jsonPageLayout = QVBoxLayout(self.jsonPage)
self.jsonPageLayout.setObjectName(u"jsonPageLayout")
self.jsonPageLayout.setContentsMargins(0, 0, 0, 0)
self.jsonEditor = QPlainTextEdit(self.jsonPage)
self.jsonEditor.setObjectName(u"jsonEditor")
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.textPage = QWidget()
self.textPage.setObjectName(u"textPage")
self.textPageLayout = QVBoxLayout(self.textPage)
self.textPageLayout.setObjectName(u"textPageLayout")
self.textPageLayout.setContentsMargins(0, 0, 0, 0)
self.textDefinitionEditor = TextDefinitionEditor(self.textPage)
self.textDefinitionEditor.setObjectName(u"textDefinitionEditor")
self.jsonPageLayout.addWidget(self.jsonEditor)
self.textPageLayout.addWidget(self.textDefinitionEditor)
self.workspaceStack.addWidget(self.jsonPage)
self.workspaceStack.addWidget(self.textPage)
self.emptyPage = QWidget()
self.emptyPage.setObjectName(u"emptyPage")
self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;")
@@ -337,8 +346,26 @@ class Ui_MainWindow(object):
self.emptyPageLayout.addWidget(self.emptyWorkspaceLabel)
self.workspaceStack.addWidget(self.emptyPage)
self.workspaceVerticalSplitter.addWidget(self.workspaceStack)
self.outputTabs = QTabWidget(self.workspaceVerticalSplitter)
self.outputTabs.setObjectName(u"outputTabs")
self.outputTabs.setMinimumSize(QSize(0, 100))
self.logTab = QWidget()
self.logTab.setObjectName(u"logTab")
self.logTabLayout = QVBoxLayout(self.logTab)
self.logTabLayout.setObjectName(u"logTabLayout")
self.logTabLayout.setContentsMargins(0, 0, 0, 0)
self.logOutput = QPlainTextEdit(self.logTab)
self.logOutput.setObjectName(u"logOutput")
self.logOutput.setReadOnly(True)
self.logOutput.setMaximumBlockCount(5000)
self.workspaceEditorLayout.addWidget(self.workspaceStack)
self.logTabLayout.addWidget(self.logOutput)
self.outputTabs.addTab(self.logTab, "")
self.workspaceVerticalSplitter.addWidget(self.outputTabs)
self.workspaceEditorLayout.addWidget(self.workspaceVerticalSplitter)
self.workspaceSplitter.addWidget(self.workspace)
@@ -360,6 +387,8 @@ class Ui_MainWindow(object):
self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menubar)
self.menuHelp.setObjectName(u"menuHelp")
self.menuSimulation = QMenu(self.menubar)
self.menuSimulation.setObjectName(u"menuSimulation")
MainWindow.setMenuBar(self.menubar)
self.fileToolbar = QToolBar(MainWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
@@ -378,13 +407,20 @@ class Ui_MainWindow(object):
self.cameraToolbar = QToolBar(MainWindow)
self.cameraToolbar.setObjectName(u"cameraToolbar")
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar)
self.simulationToolbar = QToolBar(MainWindow)
self.simulationToolbar.setObjectName(u"simulationToolbar")
self.simulationToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.simulationToolbar)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.menubar.addAction(self.menuView.menuAction())
self.menubar.addAction(self.menuSimulation.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())
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)
@@ -405,6 +441,12 @@ class Ui_MainWindow(object):
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.menuSimulation.addAction(self.actionSimulationSettings)
self.menuSimulation.addAction(self.actionGraphParameters)
self.menuSimulation.addAction(self.actionCompose)
self.menuSimulation.addAction(self.actionExportModel)
self.menuSimulation.addAction(self.actionSimulationWindow)
self.menuSimulation.addAction(self.actionRunSimulation)
self.fileToolbar.addAction(self.actionNew)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
@@ -417,6 +459,11 @@ class Ui_MainWindow(object):
self.cameraToolbar.addAction(self.actionZoomIn)
self.cameraToolbar.addAction(self.actionZoomOut)
self.cameraToolbar.addAction(self.actionCenterView)
self.simulationToolbar.addAction(self.actionSimulationSettings)
self.simulationToolbar.addAction(self.actionGraphParameters)
self.simulationToolbar.addAction(self.actionCompose)
self.simulationToolbar.addAction(self.actionSimulationWindow)
self.simulationToolbar.addAction(self.actionRunSimulation)
self.retranslateUi(MainWindow)
@@ -428,6 +475,36 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"BEdit", None))
self.actionSimulationSettings.setText(QCoreApplication.translate("MainWindow", u"Simulation Settings", None))
#if QT_CONFIG(statustip)
self.actionSimulationSettings.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit settings stored in the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionGraphParameters.setText(QCoreApplication.translate("MainWindow", u"Graph Parameters", None))
#if QT_CONFIG(statustip)
self.actionGraphParameters.setStatusTip(QCoreApplication.translate("MainWindow", u"Edit parameters throughout the active graph", None))
#endif // QT_CONFIG(statustip)
self.actionCompose.setText(QCoreApplication.translate("MainWindow", u"Compose", None))
#if QT_CONFIG(statustip)
self.actionCompose.setStatusTip(QCoreApplication.translate("MainWindow", u"Compose the active graph as an OpenModelica model", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionCompose.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut)
self.actionRunSimulation.setText(QCoreApplication.translate("MainWindow", u"Run", None))
#if QT_CONFIG(statustip)
self.actionRunSimulation.setStatusTip(QCoreApplication.translate("MainWindow", u"Run the simulation", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionRunSimulation.setShortcut(QCoreApplication.translate("MainWindow", u"F6", None))
#endif // QT_CONFIG(shortcut)
self.actionSimulationWindow.setText(QCoreApplication.translate("MainWindow", u"Simulation Window", None))
#if QT_CONFIG(statustip)
self.actionSimulationWindow.setStatusTip(QCoreApplication.translate("MainWindow", u"Show the simulation results window", None))
#endif // QT_CONFIG(statustip)
self.actionExportModel.setText(QCoreApplication.translate("MainWindow", u"Export Model\u2026", None))
#if QT_CONFIG(statustip)
self.actionExportModel.setStatusTip(QCoreApplication.translate("MainWindow", u"Save the composed OpenModelica model to a file", None))
#endif // QT_CONFIG(statustip)
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))
@@ -460,6 +537,20 @@ class Ui_MainWindow(object):
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionReloadLibraries.setText(QCoreApplication.translate("MainWindow", u"Reload &Libraries", None))
#if QT_CONFIG(statustip)
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"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)
@@ -526,7 +617,6 @@ class Ui_MainWindow(object):
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
#if QT_CONFIG(tooltip)
@@ -545,20 +635,19 @@ class Ui_MainWindow(object):
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
#endif // QT_CONFIG(tooltip)
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None))
self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None))
self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None))
self.splineRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Spline", None))
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
self.logOutput.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Application messages appear here.", None))
self.outputTabs.setTabText(self.outputTabs.indexOf(self.logTab), QCoreApplication.translate("MainWindow", u"Log", None))
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("MainWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("MainWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None))
self.menuSimulation.setTitle(QCoreApplication.translate("MainWindow", u"Simulation", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None))
self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None))
self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None))
self.simulationToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Simulation", None))
# retranslateUi

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,10 +15,11 @@ 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, QComboBox, QDialog,
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
QLineEdit, QListWidget, QListWidgetItem, QPushButton,
QSizePolicy, QSplitter, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLabel, QLineEdit, QListWidget, QListWidgetItem,
QPushButton, QSizePolicy, QSplitter, QVBoxLayout,
QWidget)
class Ui_PortOptionsDialog(object):
def setupUi(self, PortOptionsDialog):
@@ -94,11 +95,16 @@ class Ui_PortOptionsDialog(object):
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
self.multipleConnectionsCheckBox = QCheckBox(self.portDetailsPanel)
self.multipleConnectionsCheckBox.setObjectName(u"multipleConnectionsCheckBox")
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.multipleConnectionsCheckBox)
self.positionHintLabel = QLabel(self.portDetailsPanel)
self.positionHintLabel.setObjectName(u"positionHintLabel")
self.positionHintLabel.setWordWrap(True)
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
self.portSplitter.addWidget(self.portDetailsPanel)
@@ -130,6 +136,7 @@ class Ui_PortOptionsDialog(object):
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None))
self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None))
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", 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,
QWidget)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLineEdit, 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,76 @@ class Ui_SettingsDialog(object):
self.generalLayout.addItem(self.generalSpacer)
self.settingsTabs.addTab(self.generalTab, "")
self.simulationTab = QWidget()
self.simulationTab.setObjectName(u"simulationTab")
self.simulationTabLayout = QVBoxLayout(self.simulationTab)
self.simulationTabLayout.setObjectName(u"simulationTabLayout")
self.openModelicaGroupBox = QGroupBox(self.simulationTab)
self.openModelicaGroupBox.setObjectName(u"openModelicaGroupBox")
self.openModelicaLayout = QVBoxLayout(self.openModelicaGroupBox)
self.openModelicaLayout.setObjectName(u"openModelicaLayout")
self.openModelicaPathLabel = QLabel(self.openModelicaGroupBox)
self.openModelicaPathLabel.setObjectName(u"openModelicaPathLabel")
self.openModelicaLayout.addWidget(self.openModelicaPathLabel)
self.openModelicaPathLayout = QHBoxLayout()
self.openModelicaPathLayout.setObjectName(u"openModelicaPathLayout")
self.openModelicaPathEdit = QLineEdit(self.openModelicaGroupBox)
self.openModelicaPathEdit.setObjectName(u"openModelicaPathEdit")
self.openModelicaPathLayout.addWidget(self.openModelicaPathEdit)
self.browseOpenModelicaButton = QPushButton(self.openModelicaGroupBox)
self.browseOpenModelicaButton.setObjectName(u"browseOpenModelicaButton")
self.openModelicaPathLayout.addWidget(self.browseOpenModelicaButton)
self.openModelicaLayout.addLayout(self.openModelicaPathLayout)
self.openModelicaHintLabel = QLabel(self.openModelicaGroupBox)
self.openModelicaHintLabel.setObjectName(u"openModelicaHintLabel")
self.openModelicaHintLabel.setWordWrap(True)
self.openModelicaLayout.addWidget(self.openModelicaHintLabel)
self.simulationTabLayout.addWidget(self.openModelicaGroupBox)
self.simulationSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.simulationTabLayout.addItem(self.simulationSpacer)
self.settingsTabs.addTab(self.simulationTab, "")
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,7 +247,23 @@ 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.libraryPathsLabel.setText(QCoreApplication.translate("SettingsDialog", u"Load library JSON files from these files or folders at startup:", None))
self.openModelicaGroupBox.setTitle(QCoreApplication.translate("SettingsDialog", u"OpenModelica", None))
self.openModelicaPathLabel.setText(QCoreApplication.translate("SettingsDialog", u"OpenModelica executable:", None))
self.openModelicaPathEdit.setPlaceholderText(QCoreApplication.translate("SettingsDialog", u"Leave empty to find omc on PATH", None))
self.browseOpenModelicaButton.setText(QCoreApplication.translate("SettingsDialog", u"Browse\u2026", None))
self.openModelicaHintLabel.setText(QCoreApplication.translate("SettingsDialog", u"Select the omc executable, for example ~/.local/bin/omc.", None))
self.settingsTabs.setTabText(self.settingsTabs.indexOf(self.simulationTab), QCoreApplication.translate("SettingsDialog", u"Simulation", 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))
self.removeLibraryPathButton.setText(QCoreApplication.translate("SettingsDialog", u"Remove", None))

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'simulation_settings_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, QAbstractSpinBox, QApplication, QDialog,
QDialogButtonBox, QDoubleSpinBox, QFormLayout, QGroupBox,
QLabel, QRadioButton, QSizePolicy, QSpacerItem,
QSpinBox, QVBoxLayout, QWidget)
class Ui_SimulationSettingsDialog(object):
def setupUi(self, SimulationSettingsDialog):
if not SimulationSettingsDialog.objectName():
SimulationSettingsDialog.setObjectName(u"SimulationSettingsDialog")
SimulationSettingsDialog.resize(420, 384)
self.dialogLayout = QVBoxLayout(SimulationSettingsDialog)
self.dialogLayout.setObjectName(u"dialogLayout")
self.settingsGroup = QGroupBox(SimulationSettingsDialog)
self.settingsGroup.setObjectName(u"settingsGroup")
self.settingsLayout = QVBoxLayout(self.settingsGroup)
self.settingsLayout.setObjectName(u"settingsLayout")
self.simulationInterval = QGroupBox(self.settingsGroup)
self.simulationInterval.setObjectName(u"simulationInterval")
self.simulationInterval.setEnabled(True)
self.formLayout = QFormLayout(self.simulationInterval)
self.formLayout.setObjectName(u"formLayout")
self.label = QLabel(self.simulationInterval)
self.label.setObjectName(u"label")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label)
self.startTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.startTimeSpinBox.setObjectName(u"startTimeSpinBox")
self.startTimeSpinBox.setEnabled(True)
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.startTimeSpinBox)
self.label_2 = QLabel(self.simulationInterval)
self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2)
self.stopTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.stopTimeSpinBox.setObjectName(u"stopTimeSpinBox")
self.stopTimeSpinBox.setValue(1.000000000000000)
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.stopTimeSpinBox)
self.numberOfIntervalsRadioButton = QRadioButton(self.simulationInterval)
self.numberOfIntervalsRadioButton.setObjectName(u"numberOfIntervalsRadioButton")
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.numberOfIntervalsRadioButton)
self.intervalTimeRadioButton = QRadioButton(self.simulationInterval)
self.intervalTimeRadioButton.setObjectName(u"intervalTimeRadioButton")
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.intervalTimeRadioButton)
self.numberOfIntervalsSpinBox = QSpinBox(self.simulationInterval)
self.numberOfIntervalsSpinBox.setObjectName(u"numberOfIntervalsSpinBox")
self.numberOfIntervalsSpinBox.setMaximum(999999999)
self.numberOfIntervalsSpinBox.setValue(500)
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.numberOfIntervalsSpinBox)
self.intervalTimeSpinBox = QDoubleSpinBox(self.simulationInterval)
self.intervalTimeSpinBox.setObjectName(u"intervalTimeSpinBox")
self.intervalTimeSpinBox.setDecimals(5)
self.intervalTimeSpinBox.setStepType(QAbstractSpinBox.StepType.AdaptiveDecimalStepType)
self.intervalTimeSpinBox.setValue(0.002000000000000)
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.intervalTimeSpinBox)
self.settingsLayout.addWidget(self.simulationInterval)
self.settingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.settingsLayout.addItem(self.settingsSpacer)
self.dialogLayout.addWidget(self.settingsGroup)
self.buttonBox = QDialogButtonBox(SimulationSettingsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.dialogLayout.addWidget(self.buttonBox)
self.retranslateUi(SimulationSettingsDialog)
self.buttonBox.accepted.connect(SimulationSettingsDialog.accept)
self.buttonBox.rejected.connect(SimulationSettingsDialog.reject)
QMetaObject.connectSlotsByName(SimulationSettingsDialog)
# setupUi
def retranslateUi(self, SimulationSettingsDialog):
SimulationSettingsDialog.setWindowTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Simulation Settings", None))
self.settingsGroup.setTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Settings", None))
self.simulationInterval.setTitle(QCoreApplication.translate("SimulationSettingsDialog", u"Simulation Interval", None))
self.label.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Start time:", None))
self.startTimeSpinBox.setPrefix("")
self.startTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
self.label_2.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Stop time:", None))
self.stopTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
self.numberOfIntervalsRadioButton.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Number of intervals:", None))
self.intervalTimeRadioButton.setText(QCoreApplication.translate("SimulationSettingsDialog", u"Interval:", None))
self.intervalTimeSpinBox.setSuffix(QCoreApplication.translate("SimulationSettingsDialog", u"s", None))
# retranslateUi

View File

@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'simulation_window.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 (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QDockWidget, QHBoxLayout,
QHeaderView, QLabel, QListWidget, QListWidgetItem,
QMainWindow, QMenu, QMenuBar, QProgressBar,
QSizePolicy, QTabWidget, QToolBar, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
from . import resources_rc
class Ui_SimulationWindow(object):
def setupUi(self, SimulationWindow):
if not SimulationWindow.objectName():
SimulationWindow.setObjectName(u"SimulationWindow")
SimulationWindow.resize(900, 650)
icon = QIcon()
icon.addFile(u":/icons/icons/office-chart-line.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
SimulationWindow.setWindowIcon(icon)
self.actionOpen = QAction(SimulationWindow)
self.actionOpen.setObjectName(u"actionOpen")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon1)
self.actionSave = QAction(SimulationWindow)
self.actionSave.setObjectName(u"actionSave")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon2)
self.actionClear = QAction(SimulationWindow)
self.actionClear.setObjectName(u"actionClear")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionClear.setIcon(icon3)
self.actionSaveAs = QAction(SimulationWindow)
self.actionSaveAs.setObjectName(u"actionSaveAs")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSaveAs.setIcon(icon4)
self.actionExit = QAction(SimulationWindow)
self.actionExit.setObjectName(u"actionExit")
self.actionAbout = QAction(SimulationWindow)
self.actionAbout.setObjectName(u"actionAbout")
self.actionAboutQt = QAction(SimulationWindow)
self.actionAboutQt.setObjectName(u"actionAboutQt")
self.actionToggleResults = QAction(SimulationWindow)
self.actionToggleResults.setObjectName(u"actionToggleResults")
self.actionToggleResults.setCheckable(True)
self.actionToggleResults.setChecked(True)
self.actionAddGraph = QAction(SimulationWindow)
self.actionAddGraph.setObjectName(u"actionAddGraph")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/list-add.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAddGraph.setIcon(icon5)
self.actionRemoveGraph = QAction(SimulationWindow)
self.actionRemoveGraph.setObjectName(u"actionRemoveGraph")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/list-remove.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRemoveGraph.setIcon(icon6)
self.centralWidget = QWidget(SimulationWindow)
self.centralWidget.setObjectName(u"centralWidget")
self.resultsLayout = QVBoxLayout(self.centralWidget)
self.resultsLayout.setObjectName(u"resultsLayout")
self.graphTabs = QTabWidget(self.centralWidget)
self.graphTabs.setObjectName(u"graphTabs")
self.graphTabs.setTabsClosable(False)
self.graphTabs.setMovable(True)
self.resultsLayout.addWidget(self.graphTabs)
SimulationWindow.setCentralWidget(self.centralWidget)
self.menuBar = QMenuBar(SimulationWindow)
self.menuBar.setObjectName(u"menuBar")
self.menuBar.setGeometry(QRect(0, 0, 900, 24))
self.menuFile = QMenu(self.menuBar)
self.menuFile.setObjectName(u"menuFile")
self.menuView = QMenu(self.menuBar)
self.menuView.setObjectName(u"menuView")
self.menuPanels = QMenu(self.menuView)
self.menuPanels.setObjectName(u"menuPanels")
self.menuToolbars = QMenu(self.menuView)
self.menuToolbars.setObjectName(u"menuToolbars")
self.menuHelp = QMenu(self.menuBar)
self.menuHelp.setObjectName(u"menuHelp")
self.menuGraph = QMenu(self.menuBar)
self.menuGraph.setObjectName(u"menuGraph")
SimulationWindow.setMenuBar(self.menuBar)
self.workspaceToolbar = QToolBar(SimulationWindow)
self.workspaceToolbar.setObjectName(u"workspaceToolbar")
self.workspaceToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.workspaceToolbar)
self.fileToolbar = QToolBar(SimulationWindow)
self.fileToolbar.setObjectName(u"fileToolbar")
self.fileToolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
SimulationWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.fileToolbar)
self.statusDock = QDockWidget(SimulationWindow)
self.statusDock.setObjectName(u"statusDock")
self.statusDockContents = QWidget()
self.statusDockContents.setObjectName(u"statusDockContents")
self.horizontalLayout = QHBoxLayout(self.statusDockContents)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.timeLabel = QLabel(self.statusDockContents)
self.timeLabel.setObjectName(u"timeLabel")
self.horizontalLayout.addWidget(self.timeLabel)
self.progressBar = QProgressBar(self.statusDockContents)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMaximum(10000)
self.progressBar.setValue(0)
self.horizontalLayout.addWidget(self.progressBar)
self.statusLabel = QLabel(self.statusDockContents)
self.statusLabel.setObjectName(u"statusLabel")
self.horizontalLayout.addWidget(self.statusLabel)
self.statusDock.setWidget(self.statusDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.statusDock)
self.logDock = QDockWidget(SimulationWindow)
self.logDock.setObjectName(u"logDock")
self.logDock.setFloating(False)
self.logDock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetFloatable|QDockWidget.DockWidgetFeature.DockWidgetMovable)
self.logDockContents = QWidget()
self.logDockContents.setObjectName(u"logDockContents")
self.logLayout = QVBoxLayout(self.logDockContents)
self.logLayout.setObjectName(u"logLayout")
self.messageList = QListWidget(self.logDockContents)
self.messageList.setObjectName(u"messageList")
self.messageList.setAlternatingRowColors(True)
self.logLayout.addWidget(self.messageList)
self.logDock.setWidget(self.logDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.logDock)
self.signalsDock = QDockWidget(SimulationWindow)
self.signalsDock.setObjectName(u"signalsDock")
self.signalsDockContents = QWidget()
self.signalsDockContents.setObjectName(u"signalsDockContents")
self.signalsLayout = QVBoxLayout(self.signalsDockContents)
self.signalsLayout.setObjectName(u"signalsLayout")
self.signalsTree = QTreeWidget(self.signalsDockContents)
self.signalsTree.setObjectName(u"signalsTree")
self.signalsTree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.signalsTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.signalsTree.setHeaderHidden(True)
self.signalsLayout.addWidget(self.signalsTree)
self.signalsDock.setWidget(self.signalsDockContents)
SimulationWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.signalsDock)
self.menuBar.addAction(self.menuFile.menuAction())
self.menuBar.addAction(self.menuView.menuAction())
self.menuBar.addAction(self.menuGraph.menuAction())
self.menuBar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionClear)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExit)
self.menuView.addAction(self.menuPanels.menuAction())
self.menuView.addAction(self.menuToolbars.menuAction())
self.menuHelp.addAction(self.actionAbout)
self.menuHelp.addAction(self.actionAboutQt)
self.menuGraph.addAction(self.actionAddGraph)
self.menuGraph.addAction(self.actionRemoveGraph)
self.workspaceToolbar.addAction(self.actionAddGraph)
self.workspaceToolbar.addAction(self.actionRemoveGraph)
self.fileToolbar.addAction(self.actionClear)
self.fileToolbar.addAction(self.actionOpen)
self.fileToolbar.addAction(self.actionSave)
self.fileToolbar.addAction(self.actionSaveAs)
self.retranslateUi(SimulationWindow)
QMetaObject.connectSlotsByName(SimulationWindow)
# setupUi
def retranslateUi(self, SimulationWindow):
SimulationWindow.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Simulation", None))
self.actionOpen.setText(QCoreApplication.translate("SimulationWindow", u"&Open\u2026", None))
#if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("SimulationWindow", u"&Save\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+S", None))
#endif // QT_CONFIG(shortcut)
self.actionClear.setText(QCoreApplication.translate("SimulationWindow", u"&Clear", None))
self.actionSaveAs.setText(QCoreApplication.translate("SimulationWindow", u"Save &As\u2026", None))
#if QT_CONFIG(shortcut)
self.actionSaveAs.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionExit.setText(QCoreApplication.translate("SimulationWindow", u"E&xit", None))
#if QT_CONFIG(shortcut)
self.actionExit.setShortcut(QCoreApplication.translate("SimulationWindow", u"Ctrl+W", None))
#endif // QT_CONFIG(shortcut)
self.actionAbout.setText(QCoreApplication.translate("SimulationWindow", u"&About Simulation Window", None))
self.actionAboutQt.setText(QCoreApplication.translate("SimulationWindow", u"About &Qt", None))
self.actionToggleResults.setText(QCoreApplication.translate("SimulationWindow", u"Results", None))
self.actionAddGraph.setText(QCoreApplication.translate("SimulationWindow", u"Add Graph", None))
#if QT_CONFIG(statustip)
self.actionAddGraph.setStatusTip(QCoreApplication.translate("SimulationWindow", u"Add a graph workspace tab", None))
#endif // QT_CONFIG(statustip)
self.actionRemoveGraph.setText(QCoreApplication.translate("SimulationWindow", u"Remove Current Graph", None))
#if QT_CONFIG(statustip)
self.actionRemoveGraph.setStatusTip(QCoreApplication.translate("SimulationWindow", u"Remove the current graph workspace tab", None))
#endif // QT_CONFIG(statustip)
self.menuFile.setTitle(QCoreApplication.translate("SimulationWindow", u"&File", None))
self.menuView.setTitle(QCoreApplication.translate("SimulationWindow", u"&View", None))
self.menuPanels.setTitle(QCoreApplication.translate("SimulationWindow", u"&Panels", None))
self.menuToolbars.setTitle(QCoreApplication.translate("SimulationWindow", u"&Toolbars", None))
self.menuHelp.setTitle(QCoreApplication.translate("SimulationWindow", u"&Help", None))
self.menuGraph.setTitle(QCoreApplication.translate("SimulationWindow", u"&Graph", None))
self.workspaceToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Workspace", None))
self.fileToolbar.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"File", None))
self.statusDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Status", None))
self.timeLabel.setText(QCoreApplication.translate("SimulationWindow", u"Time: 0 s", None))
self.progressBar.setFormat(QCoreApplication.translate("SimulationWindow", u"%p%", None))
self.statusLabel.setText(QCoreApplication.translate("SimulationWindow", u"No simulation has been run yet.", None))
self.logDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Log", None))
self.signalsDock.setWindowTitle(QCoreApplication.translate("SimulationWindow", u"Signals", None))
___qtreewidgetitem = self.signalsTree.headerItem()
___qtreewidgetitem.setText(0, QCoreApplication.translate("SimulationWindow", u"Signal", None))
# retranslateUi

View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'text_definition_editor.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 (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
QHeaderView, QPushButton, QSizePolicy, QSpacerItem,
QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
from bedit.gui.editors.openmodelica import OpenModelicaEditor
class Ui_TextDefinitionEditor(object):
def setupUi(self, TextDefinitionEditor):
if not TextDefinitionEditor.objectName():
TextDefinitionEditor.setObjectName(u"TextDefinitionEditor")
TextDefinitionEditor.resize(900, 600)
self.editorLayout = QHBoxLayout(TextDefinitionEditor)
self.editorLayout.setObjectName(u"editorLayout")
self.editorLayout.setContentsMargins(6, 6, 6, 6)
self.columnSplitter = QSplitter(TextDefinitionEditor)
self.columnSplitter.setObjectName(u"columnSplitter")
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
self.columnSplitter.setChildrenCollapsible(False)
self.sourceSplitter = QSplitter(self.columnSplitter)
self.sourceSplitter.setObjectName(u"sourceSplitter")
self.sourceSplitter.setOrientation(Qt.Orientation.Vertical)
self.sourceSplitter.setChildrenCollapsible(False)
self.declarationsGroup = QGroupBox(self.sourceSplitter)
self.declarationsGroup.setObjectName(u"declarationsGroup")
self.declarationsLayout = QVBoxLayout(self.declarationsGroup)
self.declarationsLayout.setObjectName(u"declarationsLayout")
self.declarationsEdit = OpenModelicaEditor(self.declarationsGroup)
self.declarationsEdit.setObjectName(u"declarationsEdit")
self.declarationsLayout.addWidget(self.declarationsEdit)
self.sourceSplitter.addWidget(self.declarationsGroup)
self.initialEquationsGroup = QGroupBox(self.sourceSplitter)
self.initialEquationsGroup.setObjectName(u"initialEquationsGroup")
self.initialEquationsLayout = QVBoxLayout(self.initialEquationsGroup)
self.initialEquationsLayout.setObjectName(u"initialEquationsLayout")
self.initialEquationsEdit = OpenModelicaEditor(self.initialEquationsGroup)
self.initialEquationsEdit.setObjectName(u"initialEquationsEdit")
self.initialEquationsLayout.addWidget(self.initialEquationsEdit)
self.sourceSplitter.addWidget(self.initialEquationsGroup)
self.equationsGroup = QGroupBox(self.sourceSplitter)
self.equationsGroup.setObjectName(u"equationsGroup")
self.equationsLayout = QVBoxLayout(self.equationsGroup)
self.equationsLayout.setObjectName(u"equationsLayout")
self.equationsEdit = OpenModelicaEditor(self.equationsGroup)
self.equationsEdit.setObjectName(u"equationsEdit")
self.equationsLayout.addWidget(self.equationsEdit)
self.sourceSplitter.addWidget(self.equationsGroup)
self.columnSplitter.addWidget(self.sourceSplitter)
self.definitionSplitter = QSplitter(self.columnSplitter)
self.definitionSplitter.setObjectName(u"definitionSplitter")
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
self.definitionSplitter.setChildrenCollapsible(False)
self.portsGroup = QGroupBox(self.definitionSplitter)
self.portsGroup.setObjectName(u"portsGroup")
self.portsLayout = QVBoxLayout(self.portsGroup)
self.portsLayout.setObjectName(u"portsLayout")
self.portsTable = QTableWidget(self.portsGroup)
if (self.portsTable.columnCount() < 4):
self.portsTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
self.portsTable.setObjectName(u"portsTable")
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.portsTable.setColumnCount(4)
self.portsLayout.addWidget(self.portsTable)
self.portButtonsLayout = QHBoxLayout()
self.portButtonsLayout.setObjectName(u"portButtonsLayout")
self.addPortButton = QPushButton(self.portsGroup)
self.addPortButton.setObjectName(u"addPortButton")
self.portButtonsLayout.addWidget(self.addPortButton)
self.removePortButton = QPushButton(self.portsGroup)
self.removePortButton.setObjectName(u"removePortButton")
self.portButtonsLayout.addWidget(self.removePortButton)
self.portButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.portButtonsLayout.addItem(self.portButtonSpacer)
self.portsLayout.addLayout(self.portButtonsLayout)
self.definitionSplitter.addWidget(self.portsGroup)
self.parametersGroup = QGroupBox(self.definitionSplitter)
self.parametersGroup.setObjectName(u"parametersGroup")
self.parametersLayout = QVBoxLayout(self.parametersGroup)
self.parametersLayout.setObjectName(u"parametersLayout")
self.parametersTable = QTableWidget(self.parametersGroup)
if (self.parametersTable.columnCount() < 3):
self.parametersTable.setColumnCount(3)
__qtablewidgetitem4 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4)
__qtablewidgetitem5 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5)
__qtablewidgetitem6 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6)
self.parametersTable.setObjectName(u"parametersTable")
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.parametersTable.setColumnCount(3)
self.parametersLayout.addWidget(self.parametersTable)
self.parameterButtonsLayout = QHBoxLayout()
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
self.addParameterButton = QPushButton(self.parametersGroup)
self.addParameterButton.setObjectName(u"addParameterButton")
self.parameterButtonsLayout.addWidget(self.addParameterButton)
self.removeParameterButton = QPushButton(self.parametersGroup)
self.removeParameterButton.setObjectName(u"removeParameterButton")
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
self.parameterButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.parameterButtonsLayout.addItem(self.parameterButtonSpacer)
self.parametersLayout.addLayout(self.parameterButtonsLayout)
self.definitionSplitter.addWidget(self.parametersGroup)
self.columnSplitter.addWidget(self.definitionSplitter)
self.editorLayout.addWidget(self.columnSplitter)
self.retranslateUi(TextDefinitionEditor)
QMetaObject.connectSlotsByName(TextDefinitionEditor)
# setupUi
def retranslateUi(self, TextDefinitionEditor):
self.declarationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Declarations", None))
self.initialEquationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Initial Equations", None))
self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None))
___qtablewidgetitem3 = self.portsTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Multiple", None))
self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None))
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0)
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1)
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2)
___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
pass
# retranslateUi

View File

@@ -34,50 +34,37 @@ def _snap(value: float) -> float:
class ResizeHandle(QGraphicsEllipseItem):
def __init__(self, owner: "ShapeItem") -> None:
CURSORS = {
"n": Qt.CursorShape.SizeVerCursor,
"s": Qt.CursorShape.SizeVerCursor,
"e": Qt.CursorShape.SizeHorCursor,
"w": Qt.CursorShape.SizeHorCursor,
"nw": Qt.CursorShape.SizeFDiagCursor,
"se": Qt.CursorShape.SizeFDiagCursor,
"ne": Qt.CursorShape.SizeBDiagCursor,
"sw": Qt.CursorShape.SizeBDiagCursor,
}
def __init__(self, owner: "ShapeItem", role: str = "se") -> None:
super().__init__(-4, -4, 8, 8, owner)
self.owner = owner
self.role = role
self.setBrush(QColor("#ffffff"))
self.setPen(QPen(QColor("#2563eb"), 1.5))
self.setCursor(Qt.CursorShape.SizeFDiagCursor)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
self.setCursor(self.CURSORS[role])
self.setZValue(20)
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
maximum = (
self.owner.scene().sceneRect().bottomRight() - self.owner.pos()
if self.owner.scene()
else QPointF(128, 128)
)
if self.owner.element.get("type") == "line":
minimum = (
self.owner.scene().sceneRect().topLeft() - self.owner.pos()
if self.owner.scene()
else QPointF(-128, -128)
)
value = QPointF(
min(maximum.x(), max(minimum.x(), _snap(value.x()))),
min(maximum.y(), max(minimum.y(), _snap(value.y()))),
)
self.owner.prepareGeometryChange()
self.owner.element["width"] = value.x()
self.owner.element["height"] = value.y()
self.owner.update()
return super().itemChange(change, value)
value = QPointF(
min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))),
min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))),
)
if self.owner.element.get("type") == "circle":
side = min(maximum.x(), maximum.y(), max(value.x(), value.y()))
value = QPointF(side, side)
self.owner.prepareGeometryChange()
self.owner.element["width"] = value.x()
self.owner.element["height"] = value.y()
self.owner.update()
return super().itemChange(change, value)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.begin_resize()
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.resize_from_handle(self.role, event.scenePos())
event.accept()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.owner.finish_resize()
event.accept()
class ShapeOptionsDialog(QDialog):
@@ -178,9 +165,16 @@ class ShapeItem(QGraphicsObject):
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
self.resize_handle = ResizeHandle(self)
self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20)))
self.resize_handle.hide()
roles = (
("n", "ne", "e", "se", "s", "sw", "w", "nw")
if element.get("type") == "rectangle"
else ("se",)
)
self.resize_handles = [ResizeHandle(self, role) for role in roles]
self.resize_start: dict | None = None
self._position_resize_handles()
for handle in self.resize_handles:
handle.hide()
def boundingRect(self) -> QRectF: # noqa: N802
margin = max(3.0, float(self.element.get("lineWidth", 1.5)))
@@ -233,7 +227,8 @@ class ShapeItem(QGraphicsObject):
elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
self.element["x"], self.element["y"] = value.x(), value.y()
elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged:
self.resize_handle.setVisible(bool(value))
for handle in self.resize_handles:
handle.setVisible(bool(value))
return super().itemChange(change, value)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
@@ -251,6 +246,74 @@ class ShapeItem(QGraphicsObject):
max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))),
)
def _position_resize_handles(self) -> None:
width = float(self.element.get("width", 20))
height = float(self.element.get("height", 20))
positions = {
"n": QPointF(width / 2, 0),
"ne": QPointF(width, 0),
"e": QPointF(width, height / 2),
"se": QPointF(width, height),
"s": QPointF(width / 2, height),
"sw": QPointF(0, height),
"w": QPointF(0, height / 2),
"nw": QPointF(0, 0),
}
for handle in self.resize_handles:
handle.setPos(positions[handle.role])
def begin_resize(self) -> None:
self.resize_start = {
"left": self.pos().x(),
"top": self.pos().y(),
"right": self.pos().x() + float(self.element.get("width", 20)),
"bottom": self.pos().y() + float(self.element.get("height", 20)),
}
def resize_from_handle(self, role: str, scene_position: QPointF) -> None:
if self.resize_start is None:
self.begin_resize()
if self.element.get("type") == "line":
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
endpoint = QPointF(
min(bounds.right(), max(bounds.left(), _snap(scene_position.x()))),
min(bounds.bottom(), max(bounds.top(), _snap(scene_position.y()))),
)
self.prepareGeometryChange()
self.element["width"] = endpoint.x() - self.pos().x()
self.element["height"] = endpoint.y() - self.pos().y()
self._position_resize_handles()
self.update()
return
edges = dict(self.resize_start)
point = QPointF(_snap(scene_position.x()), _snap(scene_position.y()))
if "w" in role:
edges["left"] = min(point.x(), edges["right"] - _icon_grid_size())
if "e" in role:
edges["right"] = max(point.x(), edges["left"] + _icon_grid_size())
if "n" in role:
edges["top"] = min(point.y(), edges["bottom"] - _icon_grid_size())
if "s" in role:
edges["bottom"] = max(point.y(), edges["top"] + _icon_grid_size())
bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128)
edges["left"] = max(bounds.left(), edges["left"])
edges["top"] = max(bounds.top(), edges["top"])
edges["right"] = min(bounds.right(), edges["right"])
edges["bottom"] = min(bounds.bottom(), edges["bottom"])
if self.element.get("type") == "circle":
side = min(edges["right"] - edges["left"], edges["bottom"] - edges["top"])
edges["right"] = edges["left"] + side
edges["bottom"] = edges["top"] + side
self.prepareGeometryChange()
self.element["width"] = edges["right"] - edges["left"]
self.element["height"] = edges["bottom"] - edges["top"]
self.setPos(edges["left"], edges["top"])
self._position_resize_handles()
self.update()
def finish_resize(self) -> None:
self.resize_start = None
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
menu = QMenu()
options = menu.addAction("Shape Options…")
@@ -270,10 +333,7 @@ class ShapeItem(QGraphicsObject):
if self.element.get("type") == "line":
self.element["width"] *= width_sign
self.element["height"] *= height_sign
self.resize_handle.setPos(
float(self.element.get("width", 20)),
float(self.element.get("height", 20)),
)
self._position_resize_handles()
self.update()
elif chosen is delete and self.scene() is not None:
self.scene().removeItem(self)

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,11 @@ import json
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import Qt, Slot
from PySide6.QtCore import QByteArray, QMimeData, Qt, Slot
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QButtonGroup,
QApplication,
QFileDialog,
QMainWindow,
QMenu,
@@ -13,23 +14,34 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component, Port
from bedit.core.serializer import JsonDocumentSerializer
from bedit.core.model import Component, Connection
from bedit.core.application_log import get_logger
from bedit.core.serializer import DocumentSerializer
from bedit.core.simulation import Simulation
from bedit.gui.controllers.document import DocumentController
from bedit.gui.dialogs.component_options import ComponentOptionsDialog
from bedit.gui.dialogs.graph_parameters import GraphParametersDialog
from bedit.gui.dialogs.item_options import ItemOptionsDialog
from bedit.gui.models.library_repository import LibraryRepository
from bedit.gui.models.library_tree import (
COMPONENT_MIME_TYPE,
COMPONENT_ROLE,
COMPONENT_ID_ROLE,
COMPONENT_INSTANCE_ROLE,
ITEM_KIND_ROLE,
DocumentTreeModel,
LibraryTreeModel,
)
from bedit.gui.graphics.workspace import SELECTION_MIME_TYPE
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.simulation_window import SimulationWindow
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
class MainWindow(QMainWindow):
@@ -39,10 +51,24 @@ class MainWindow(QMainWindow):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.application_logger = get_logger()
self.log = get_logger("ui")
self.log_handler = ApplicationLogHandler(self.ui.logOutput)
self.application_logger.addHandler(self.log_handler)
self.application_logger.setLevel("INFO")
self.ui.workspaceVerticalSplitter.setSizes([580, 160])
self.log.info("BEdit started")
self.settings = application_settings()
self._applying_text_definition = False
# Keep a Python-owned top-level window. Giving it MainWindow as its Qt
# parent makes some Linux window managers inherit the BEdit window icon.
self._simulation_window = SimulationWindow()
self.libraries = LibraryRepository(self)
self.document_controller = DocumentController(self)
self.simulation = Simulation(
openmodelica_path=SettingsDialog.openmodelica_path(self.settings)
)
self.document_controller = DocumentController(self, simulation=self.simulation)
self.library_tree_model = LibraryTreeModel(
self.libraries,
self.document_controller,
@@ -50,6 +76,7 @@ class MainWindow(QMainWindow):
)
self.document_tree_model = DocumentTreeModel(self.document_controller, self)
self._configure_models()
QApplication.clipboard().dataChanged.connect(self._update_edit_actions)
self._connect_actions()
self._populate_view_menu()
self._restore_window_geometry()
@@ -83,15 +110,23 @@ class MainWindow(QMainWindow):
self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu)
self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component)
self.ui.documentTreeView.clicked.connect(
lambda _index: self._update_edit_actions()
)
self.ui.treeView.clicked.connect(lambda _index: self._update_edit_actions())
self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll)
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(
lambda _available: self._update_edit_actions()
)
self.ui.graphView.toolModeShortcutRequested.connect(self.set_graph_tool)
self.mode_button_group = QButtonGroup(self)
self.mode_button_group.setExclusive(True)
self.mode_button_group.addButton(self.ui.pointerToolButton)
@@ -99,14 +134,6 @@ class MainWindow(QMainWindow):
self.mode_button_group.addButton(self.ui.boxToolButton)
self.mode_button_group.addButton(self.ui.lineToolButton)
self.mode_button_group.addButton(self.ui.textToolButton)
self.routing_button_group = QButtonGroup(self)
self.routing_button_group.setExclusive(True)
for button in (
self.ui.directRoutingButton,
self.ui.angledRoutingButton,
self.ui.splineRoutingButton,
):
self.routing_button_group.addButton(button)
self.ui.navigateUpButton.clicked.connect(self.navigate_up)
self.ui.navigateDownButton.clicked.connect(self.navigate_down)
self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer"))
@@ -115,15 +142,19 @@ class MainWindow(QMainWindow):
self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
self.ui.directRoutingButton.clicked.connect(lambda: self.set_connection_routing("direct"))
self.ui.angledRoutingButton.clicked.connect(lambda: self.set_connection_routing("angled"))
self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline"))
self.ui.applyJsonButton.clicked.connect(self.apply_json)
self.ui.textDefinitionEditor.definitionEdited.connect(
self.apply_text_definition
)
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
self.document_controller.textDefinitionChanged.connect(
self._text_definition_changed
)
def _connect_actions(self) -> None:
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)
@@ -136,14 +167,23 @@ class MainWindow(QMainWindow):
self.ui.actionUndo.triggered.connect(self.document_controller.undo_stack.undo)
self.ui.actionRedo.triggered.connect(self.document_controller.undo_stack.redo)
self.ui.actionDelete.triggered.connect(self.ui.graphView.delete_selected)
self.ui.actionCopy.triggered.connect(self.ui.graphView.copy_selection)
self.ui.actionCut.triggered.connect(self.ui.graphView.cut_selection)
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionCopy.triggered.connect(self.copy_selection)
self.ui.actionCut.triggered.connect(self.cut_selection)
self.ui.actionPaste.triggered.connect(self.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
self.ui.actionSimulationSettings.triggered.connect(
self.show_simulation_settings
)
self.ui.actionGraphParameters.triggered.connect(self.show_graph_parameters)
self.ui.actionCompose.triggered.connect(self.compose_active_graph)
self.ui.actionExportModel.triggered.connect(self.export_model)
self.ui.actionSimulationWindow.triggered.connect(self.show_simulation_window)
self.ui.actionRunSimulation.triggered.connect(self.run_simulation)
self.document_controller.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled)
self.document_controller.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled)
self.document_controller.modifiedChanged.connect(lambda _modified: self._update_title())
@@ -161,19 +201,114 @@ class MainWindow(QMainWindow):
self.ui.fileToolbar,
self.ui.editToolbar,
self.ui.cameraToolbar,
self.ui.simulationToolbar,
):
self.ui.menuToolbars.addAction(toolbar.toggleViewAction())
def reload_libraries(self) -> None:
self.libraries.load_paths(SettingsDialog.library_paths(self.settings))
self.ui.treeView.expandAll()
self.log.info("Reloaded %d libraries", len(self.libraries.libraries))
if self.libraries.load_warnings:
for warning in self.libraries.load_warnings:
self.log.warning("Library load failed: %s", warning)
QMessageBox.warning(
self,
"Some libraries could not be loaded",
"\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
if component is None or component.implementation_kind != "graph":
return
dialog = SimulationSettingsDialog(component.graph.simulation_settings, self)
if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.edit_simulation_settings(dialog.settings)
@Slot()
def show_graph_parameters(self) -> None:
component = self.document_controller.active_component
if component is None or component.implementation_kind != "graph":
return
dialog = GraphParametersDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
try:
self.document_controller.edit_graph_parameter_values(
component.id, dialog.parameter_values
)
except ValueError as error:
self.log.error("Could not change graph parameters: %s", error)
QMessageBox.warning(self, "Cannot change graph parameters", str(error))
@Slot()
def compose_active_graph(self) -> None:
try:
self.document_controller.compose_active_graph()
except ValueError as error:
self.log.error("Composition failed: %s", error)
QMessageBox.warning(self, "Cannot compose", str(error))
@Slot()
def export_model(self) -> None:
try:
model_name, source = self.document_controller.compose_active_graph_source()
except ValueError as error:
self.log.error("Model export composition failed: %s", error)
QMessageBox.warning(self, "Cannot Export Model", str(error))
return
file_name, _selected_filter = QFileDialog.getSaveFileName(
self,
"Export OpenModelica Model",
f"{model_name}.mo",
"Modelica Models (*.mo);;All Files (*)",
)
if not file_name:
return
path = Path(file_name)
if not path.suffix:
path = path.with_suffix(".mo")
temporary_path = path.with_suffix(path.suffix + ".tmp")
try:
temporary_path.write_text(source, encoding="utf-8")
temporary_path.replace(path)
except OSError as error:
self.log.error("Could not export model %s: %s", path, error)
QMessageBox.critical(self, "Cannot Export Model", str(error))
return
self.log.info("Exported OpenModelica model to %s", path)
@Slot()
def show_simulation_window(self) -> None:
self._simulation_window.show()
self._simulation_window.raise_()
self._simulation_window.activateWindow()
@Slot()
def run_simulation(self) -> None:
window = self._simulation_window
callbacks = window.begin_run()
self.show_simulation_window()
try:
self.document_controller.run_simulation(*callbacks)
window.prepare_run_model(self.simulation.model_name)
except Exception as error:
self.log.exception("Simulation run failed")
window.report_start_error(error)
def _restore_window_geometry(self) -> None:
geometry = self.settings.value("window/geometry")
if geometry is not None:
@@ -198,8 +333,12 @@ class MainWindow(QMainWindow):
self.ui.navigateUpButton.setEnabled(False)
self.ui.workspaceModeLabel.setText("")
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
self.ui.applyJsonButton.setVisible(False)
self._set_graph_controls_visible(False)
self.ui.actionSimulationSettings.setEnabled(False)
self.ui.actionGraphParameters.setEnabled(False)
self.ui.actionCompose.setEnabled(False)
self.ui.actionExportModel.setEnabled(False)
self.ui.actionRunSimulation.setEnabled(False)
self._update_edit_actions()
return
self.ui.graphBreadcrumbLabel.setText(" ".join(self.document_controller.breadcrumb()))
@@ -207,14 +346,20 @@ class MainWindow(QMainWindow):
self.document_controller.document.find_parent(component.id) is not None
)
is_graph = component.implementation_kind == "graph"
self.ui.actionSimulationSettings.setEnabled(is_graph)
self.ui.actionGraphParameters.setEnabled(is_graph)
self.ui.actionCompose.setEnabled(is_graph)
self.ui.actionExportModel.setEnabled(is_graph)
self.ui.actionRunSimulation.setEnabled(is_graph)
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
self.ui.applyJsonButton.setVisible(not is_graph)
self.ui.workspaceStack.setCurrentWidget(
self.ui.graphPage if is_graph else self.ui.textPage
)
self._set_graph_controls_visible(is_graph)
if is_graph:
self.set_graph_tool("pointer")
else:
self._load_source_json()
self._load_text_definition()
self._update_edit_actions()
def _update_edit_actions(self) -> None:
@@ -223,8 +368,24 @@ class MainWindow(QMainWindow):
has_selection = bool(
self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()
)
for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete):
action.setEnabled(is_graph and has_selection)
document_component_selected = bool(
self.ui.documentTreeView.currentIndex().data(COMPONENT_ID_ROLE)
)
library_component_selected = isinstance(
self.ui.treeView.currentIndex().data(COMPONENT_ROLE), dict
)
document_has_focus = self._view_has_focus(self.ui.documentTreeView)
library_has_focus = self._view_has_focus(self.ui.treeView)
self.ui.actionCopy.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
or (library_has_focus and library_component_selected)
)
self.ui.actionCut.setEnabled(
(is_graph and has_selection)
or (document_has_focus and document_component_selected)
)
self.ui.actionDelete.setEnabled(is_graph and has_selection)
self.ui.actionRotateClockwise.setEnabled(
is_graph and self.ui.graphView.has_selected_components()
)
@@ -232,11 +393,114 @@ class MainWindow(QMainWindow):
is_graph and self.ui.graphView.has_selected_components()
)
self.ui.actionSelectAll.setEnabled(is_graph)
self.ui.actionPaste.setEnabled(is_graph)
can_paste_component = QApplication.clipboard().mimeData().hasFormat(
COMPONENT_MIME_TYPE
) or QApplication.clipboard().mimeData().hasFormat(SELECTION_MIME_TYPE)
self.ui.actionPaste.setEnabled(
(is_graph and not document_has_focus and not library_has_focus)
or (
document_has_focus
and self.document_controller.document is not None
and can_paste_component
)
)
self.ui.navigateDownButton.setEnabled(
is_graph and self.ui.graphView.has_single_selected_component()
)
def copy_selection(self) -> bool:
if self._view_has_focus(self.ui.documentTreeView):
return self._copy_tree_component(self.ui.documentTreeView)
if self._view_has_focus(self.ui.treeView):
return self._copy_tree_component(self.ui.treeView)
return self.ui.graphView.copy_selection()
def cut_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
index = self.ui.documentTreeView.currentIndex()
component_id = index.data(COMPONENT_ID_ROLE)
if component_id and self._copy_tree_component(self.ui.documentTreeView):
self.document_controller.delete_component(component_id)
return
if self._view_has_focus(self.ui.treeView):
self._copy_tree_component(self.ui.treeView)
return
self.ui.graphView.cut_selection()
def paste_selection(self) -> None:
if self._view_has_focus(self.ui.documentTreeView):
self._paste_into_document_tree()
return
self.ui.graphView.paste_selection()
def _copy_tree_component(self, tree) -> bool:
component = tree.currentIndex().data(COMPONENT_ROLE)
if not isinstance(component, dict):
return False
mime_data = QMimeData()
mime_data.setData(
COMPONENT_MIME_TYPE,
QByteArray(json.dumps(component).encode("utf-8")),
)
QApplication.clipboard().setMimeData(mime_data)
return True
def _paste_into_document_tree(self) -> None:
index = self.ui.documentTreeView.currentIndex()
kind = index.data(ITEM_KIND_ROLE)
owner_id = None
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
component = (
self.document_controller.document.find_component(component_id)
if self.document_controller.document
else None
)
if component is None or component.implementation_kind != "graph":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
owner_id = component.id
elif kind != "current-document":
QMessageBox.warning(
self, "Cannot Paste", "Select a graph component or the document root."
)
return
try:
components, connections = self._clipboard_components()
self.document_controller.paste_components_to(
owner_id, components, connections
)
except ValueError as error:
QMessageBox.warning(self, "Cannot Paste", str(error))
@staticmethod
def _clipboard_components() -> tuple[list[Component], list]:
mime_data = QApplication.clipboard().mimeData()
try:
if mime_data.hasFormat(SELECTION_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(SELECTION_MIME_TYPE)).decode("utf-8")
)
return (
[Component.from_dict(item) for item in payload.get("components", [])],
[Connection.from_dict(item) for item in payload.get("connections", [])],
)
if mime_data.hasFormat(COMPONENT_MIME_TYPE):
payload = json.loads(
bytes(mime_data.data(COMPONENT_MIME_TYPE)).decode("utf-8")
)
return [Component.from_dict(payload)], []
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
raise ValueError("The clipboard does not contain a valid component") from error
raise ValueError("The clipboard does not contain a BEdit component")
@staticmethod
def _view_has_focus(view) -> bool:
focus = QApplication.focusWidget()
return focus is view or (focus is not None and view.isAncestorOf(focus))
@Slot()
def navigate_up(self) -> None:
if self._resolve_source_edits():
@@ -255,10 +519,6 @@ class MainWindow(QMainWindow):
self.ui.lineToolButton,
self.ui.textToolButton,
self.ui.rotateToolButton,
self.ui.routingLabel,
self.ui.directRoutingButton,
self.ui.angledRoutingButton,
self.ui.splineRoutingButton,
):
widget.setVisible(visible)
@@ -272,71 +532,74 @@ class MainWindow(QMainWindow):
"text": self.ui.textToolButton,
}[mode].setChecked(True)
def set_connection_routing(self, routing: str) -> None:
self.ui.graphView.set_connection_routing(routing)
buttons = {
"direct": self.ui.directRoutingButton,
"angled": self.ui.angledRoutingButton,
"spline": self.ui.splineRoutingButton,
}
buttons[routing].setChecked(True)
def _load_source_json(self) -> None:
def _load_text_definition(self) -> None:
component = self.document_controller.active_component
if component is None:
return
text = json.dumps(
{
"inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs],
"source": component.source,
},
indent=2,
self.ui.textDefinitionEditor.set_definition(
component.source.get("declarations", ""),
component.source.get("initialEquations", ""),
component.source.get("equations", ""),
component.inputs,
component.outputs,
component.parameters,
)
self.ui.jsonEditor.setPlainText(text)
self.ui.jsonEditor.document().setModified(False)
def _resolve_source_edits(self) -> bool:
component = self.document_controller.active_component
if (
component is None
or component.implementation_kind != "text"
or not self.ui.jsonEditor.document().isModified()
or not self.ui.textDefinitionEditor.is_modified
):
return True
answer = QMessageBox.question(
self,
"Apply text component changes?",
"The text component has unapplied input, output, or source changes.",
"The text component has unapplied declaration, initial-equation, "
"equation, port, or parameter changes.",
QMessageBox.StandardButton.Apply
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel,
)
if answer == QMessageBox.StandardButton.Apply:
return self.apply_json()
return self.apply_text_definition()
return answer == QMessageBox.StandardButton.Discard
@Slot()
def apply_json(self) -> bool:
def apply_text_definition(self) -> bool:
try:
data = json.loads(self.ui.jsonEditor.toPlainText())
if not isinstance(data, dict):
raise ValueError("The text component JSON must be an object")
if not isinstance(data.get("inputs"), list):
raise ValueError("'inputs' must be a list")
if not isinstance(data.get("outputs"), list):
raise ValueError("'outputs' must be a list")
if not isinstance(data.get("source"), dict):
raise ValueError("'source' must be an object")
inputs = [Port.from_dict(item) for item in data["inputs"]]
outputs = [Port.from_dict(item) for item in data["outputs"]]
self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
except (TypeError, ValueError, json.JSONDecodeError) as error:
QMessageBox.critical(self, "Invalid text component JSON", str(error))
inputs, outputs = self.ui.textDefinitionEditor.ports
self._applying_text_definition = True
try:
self.document_controller.replace_active_text_definition(
inputs,
outputs,
self.ui.textDefinitionEditor.declarations,
self.ui.textDefinitionEditor.initial_equations,
self.ui.textDefinitionEditor.equations,
self.ui.textDefinitionEditor.parameters,
)
finally:
self._applying_text_definition = False
except (TypeError, ValueError) as error:
self.log.error("Text component update failed: %s", error)
QMessageBox.critical(self, "Invalid text component", str(error))
self._load_text_definition()
return False
self._load_source_json()
self.ui.textDefinitionEditor.set_modified(False)
return True
@Slot(str)
def _text_definition_changed(self, component_id: str) -> None:
component = self.document_controller.active_component
if (
component is not None
and component.id == component_id
and not self._applying_text_definition
):
self._load_text_definition()
def _maybe_save(self) -> bool:
if self.document_controller.document is None:
return True
@@ -358,24 +621,33 @@ class MainWindow(QMainWindow):
def new_document(self) -> None:
if self._resolve_source_edits() and self._maybe_save():
self.document_controller.new_document()
self.log.info("Created a new document")
@Slot()
def close_document(self) -> None:
if self._resolve_source_edits() and self._maybe_save():
path = self.document_controller.file_path
self.document_controller.close_document()
self.log.info("Closed document%s", f" {path}" if path else "")
@Slot()
def open_document(self) -> None:
if not self._resolve_source_edits() or not self._maybe_save():
return
filename, _ = QFileDialog.getOpenFileName(
self, "Open graph", "", "BEdit graphs (*.bedit.json *.json);;All files (*)"
self,
"Open graph",
"",
"BEdit documents (*.bedit.json *.json *.beb);;"
"BEdit JSON (*.bedit.json *.json);;BEdit Binary (*.beb);;All files (*)",
)
if not filename:
return
try:
self.document_controller.load(Path(filename))
self.log.info("Opened document %s", filename)
except (OSError, ValueError) as error:
self.log.error("Could not open document %s: %s", filename, error)
QMessageBox.critical(self, "Could not open graph", str(error))
@Slot()
@@ -385,8 +657,10 @@ class MainWindow(QMainWindow):
if self.document_controller.file_path is None:
return self.save_document_as()
try:
self.document_controller.save()
path = self.document_controller.save()
self.log.info("Saved document %s", path)
except OSError as error:
self.log.error("Could not save document: %s", error)
QMessageBox.critical(self, "Could not save graph", str(error))
return False
return True
@@ -395,17 +669,24 @@ class MainWindow(QMainWindow):
def save_document_as(self) -> bool:
if self.document_controller.document is None:
return False
filename, _ = QFileDialog.getSaveFileName(
filename, selected_filter = QFileDialog.getSaveFileName(
self,
"Save graph",
"untitled.bedit.json",
"BEdit graphs (*.bedit.json);;JSON files (*.json);;All files (*)",
"untitled.beb",
"BEdit Binary (*.beb);;BEdit JSON (*.bedit.json *.json);;All files (*)",
)
if not filename:
return False
path = Path(filename)
if path.suffix.lower() not in {".json", ".beb"}:
path = path.with_suffix(
".beb" if "Binary" in selected_filter else ".bedit.json"
)
try:
self.document_controller.save(Path(filename))
path = self.document_controller.save(path)
self.log.info("Saved document %s", path)
except OSError as error:
self.log.error("Could not save document %s: %s", filename, error)
QMessageBox.critical(self, "Could not save graph", str(error))
return False
return True
@@ -415,13 +696,22 @@ class MainWindow(QMainWindow):
dialog = SettingsDialog(self)
dialog.settingsChanged.connect(self.reload_libraries)
dialog.settingsChanged.connect(self.refresh_editor_settings)
dialog.settingsChanged.connect(self.refresh_simulation_preferences)
dialog.exec()
def refresh_simulation_preferences(self) -> None:
self.simulation.openmodelica_path = SettingsDialog.openmodelica_path(
self.settings
)
def refresh_editor_settings(self) -> None:
scene = self.ui.graphView.scene()
if scene is not None:
scene.update()
self.ui.graphView.viewport().update()
self.ui.textDefinitionEditor.ui.declarationsEdit.reload_highlighting()
self.ui.textDefinitionEditor.ui.initialEquationsEdit.reload_highlighting()
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):
@@ -440,6 +730,8 @@ class MainWindow(QMainWindow):
def show_library_context_menu(self, position) -> None:
tree_view = self.ui.documentTreeView
index = tree_view.indexAt(position)
if index.isValid():
tree_view.setCurrentIndex(index)
kind = index.data(ITEM_KIND_ROLE)
if kind == "current-component":
component_id = index.data(COMPONENT_ID_ROLE)
@@ -456,16 +748,31 @@ class MainWindow(QMainWindow):
menu.addSeparator()
options_action = menu.addAction("Component Options…")
ports_action = menu.addAction("Port Options…")
parameters_action = menu.addAction("Parameter Options…")
menu.addSeparator()
copy_action = menu.addAction("Copy")
cut_action = menu.addAction("Cut")
paste_action = menu.addAction("Paste")
paste_action.setEnabled(component.implementation_kind == "graph")
delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action:
if graph_action is not None and selected is graph_action:
self.document_controller.add_child(component_id, "graph")
elif selected is text_action:
elif text_action is not None and selected is text_action:
self.document_controller.add_child(component_id, "text")
elif selected is options_action:
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 copy_action:
self._copy_tree_component(tree_view)
elif selected is cut_action:
if self._copy_tree_component(tree_view):
self.document_controller.delete_component(component_id)
elif selected is paste_action:
self._paste_into_document_tree()
elif selected is delete_action:
answer = QMessageBox.question(
self,
@@ -481,22 +788,34 @@ class MainWindow(QMainWindow):
menu = QMenu(self)
graph_action = menu.addAction("New Graph Block")
text_action = menu.addAction("New Text Block")
menu.addSeparator()
paste_action = menu.addAction("Paste")
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action:
self.document_controller.add_root("graph")
elif selected is text_action:
self.document_controller.add_root("text")
elif selected is paste_action:
self._paste_into_document_tree()
@Slot(object)
def show_external_library_context_menu(self, position) -> None:
tree = self.ui.treeView
index = tree.indexAt(position)
if index.isValid():
tree.setCurrentIndex(index)
component = index.data(COMPONENT_INSTANCE_ROLE)
if not isinstance(component, Component):
return
menu = QMenu(self)
copy_action = menu.addAction("Copy")
menu.addSeparator()
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 copy_action:
self._copy_tree_component(tree)
elif selected is ports_action:
dialog = PortOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
@@ -513,11 +832,38 @@ class MainWindow(QMainWindow):
try:
if library is not None:
library.document.validate()
JsonDocumentSerializer.save(library.document, Path(library.source_path))
DocumentSerializer.save(library.document, Path(library.source_path))
except (OSError, ValueError) as error:
component.inputs, component.outputs = old_inputs, old_outputs
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:
@@ -533,8 +879,26 @@ class MainWindow(QMainWindow):
component_id, dialog.inputs, dialog.outputs
)
except ValueError as error:
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
@@ -543,14 +907,19 @@ class MainWindow(QMainWindow):
return
dialog = ComponentOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.edit_component_appearance(
component_id,
dialog.ui.nameEdit.text().strip(),
dialog.edited_icon,
dialog.edited_inputs,
dialog.edited_outputs,
dialog.ui.showSubtreeCheckBox.isChecked(),
)
try:
self.document_controller.edit_component_appearance(
component_id,
dialog.ui.nameEdit.text().strip(),
dialog.edited_icon,
dialog.edited_inputs,
dialog.edited_outputs,
dialog.ui.showSubtreeCheckBox.isChecked(),
dialog.ui.showNameCheckBox.isChecked(),
)
except ValueError as error:
self.log.error("Could not rename component: %s", error)
QMessageBox.warning(self, "Cannot rename component", str(error))
@Slot(str, str)
def show_port_options(self, port_id: str, direction: str) -> None:
@@ -573,9 +942,17 @@ class MainWindow(QMainWindow):
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
dialog = ItemOptionsDialog(
"Connection Options",
connection.name,
self,
name_required=False,
show_name=bool(connection.properties.get("showName", False)),
)
if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.rename_connection(connection_id, dialog.name)
self.document_controller.edit_connection_options(
connection_id, dialog.name, dialog.show_name
)
@Slot()
def show_about(self) -> None:
@@ -590,4 +967,8 @@ class MainWindow(QMainWindow):
event.ignore()
return
self.settings.setValue("window/geometry", self.saveGeometry())
self._simulation_window.close()
self.simulation.shutdown()
self.log.info("BEdit closed")
self.application_logger.removeHandler(self.log_handler)
event.accept()

View File

@@ -1,4 +1,3 @@
import json
from pathlib import Path
from PySide6.QtCore import QObject, Signal
@@ -23,7 +22,7 @@ class LibraryRepository(QObject):
for candidate in library_candidates(path):
try:
libraries.append(load_library_file(candidate))
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as error:
except (OSError, ValueError, KeyError, TypeError) as error:
warnings.append(f"{candidate}: {error}")
self.libraries = libraries
self.load_warnings = warnings

View File

@@ -86,7 +86,9 @@ class DocumentTreeModel(LibraryTreeModel):
self.clear()
self.setHorizontalHeaderLabels(["Document"])
if self.controller.document is not None:
current_root = QStandardItem("Current Document")
current_root = QStandardItem(
str(self.controller.document.metadata.get("name") or "Current Document")
)
current_root.setDragEnabled(False)
current_root.setData("current-document", ITEM_KIND_ROLE)
for component in self.controller.document.roots.values():

View File

@@ -0,0 +1,36 @@
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)
saved_state.pop("openmodelica", None)
current.shutdown(wait=True)
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)
replacement.openmodelica.configure(replacement.openmodelica_path)
return replacement

View File

@@ -0,0 +1,726 @@
import re
from pathlib import Path
from matplotlib.backend_bases import MouseButton
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QFileDialog,
QInputDialog,
QMainWindow,
QMenu,
QMessageBox,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from bedit.core.simulation.openmodelica import SimulationMessage, SimulationProgress
from bedit.core.simulation.results import (
SimulationExecutionResult,
SimulationGraph,
SimulationResults,
SimulationTrace,
load_simulation_results,
save_simulation_results,
)
from bedit.gui.generated.ui_simulation_window import Ui_SimulationWindow
from bedit.gui.preferences import application_settings
class SimulationWindow(QMainWindow):
"""Persistent viewer for live and previously saved simulation results."""
progressReceived = Signal(object)
messageReceived = Signal(object)
simulationFinished = Signal(object)
simulationFailed = Signal(str)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_SimulationWindow()
self.ui.setupUi(self)
self.settings = application_settings()
self._running = False
self._run_generation = 0
self._file_path: Path | None = None
self._rebuilding_graph_tabs = False
self._updating_signal_checks = False
self.results = SimulationResults()
self._connect_actions()
self._populate_view_menu()
self.splitDockWidget(
self.ui.statusDock, self.ui.logDock, Qt.Orientation.Vertical
)
self.ui.statusDock.setFixedHeight(self.ui.statusDock.sizeHint().height())
self._restore_window_layout()
self.progressReceived.connect(self._show_progress)
self.messageReceived.connect(self._show_message)
self.simulationFinished.connect(self._show_finished)
self.simulationFailed.connect(self._show_error)
def _connect_actions(self) -> None:
self.ui.actionOpen.triggered.connect(self.open_results)
self.ui.actionSave.triggered.connect(self.save_results)
self.ui.actionSaveAs.triggered.connect(self.save_results_as)
self.ui.actionClear.triggered.connect(self.clear)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionAbout.triggered.connect(self.show_about)
self.ui.actionAboutQt.triggered.connect(
lambda: QMessageBox.aboutQt(self, "About Qt Framework")
)
self.ui.actionToggleResults.toggled.connect(self.ui.centralWidget.setVisible)
self.ui.actionAddGraph.triggered.connect(self.add_graph_tab)
self.ui.actionRemoveGraph.triggered.connect(self.remove_current_graph_tab)
self.ui.graphTabs.tabBarDoubleClicked.connect(self.rename_graph_tab)
self.ui.graphTabs.tabBar().tabMoved.connect(self._move_graph_tab)
self.ui.graphTabs.currentChanged.connect(self._current_graph_changed)
self.ui.signalsTree.itemChanged.connect(self._signal_check_changed)
self.ui.signalsTree.customContextMenuRequested.connect(
self.show_signal_context_menu
)
def _populate_view_menu(self) -> None:
self.ui.menuPanels.addAction(self.ui.actionToggleResults)
for panel in (self.ui.statusDock, self.ui.logDock, self.ui.signalsDock):
self.ui.menuPanels.addAction(panel.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.fileToolbar.toggleViewAction())
self.ui.menuToolbars.addAction(self.ui.workspaceToolbar.toggleViewAction())
def _restore_window_layout(self) -> None:
geometry = self.settings.value("simulationWindow/geometry")
if geometry is not None:
self.restoreGeometry(geometry)
state = self.settings.value("simulationWindow/state")
if state is not None:
self.restoreState(state)
results_visible = self.settings.value(
"simulationWindow/resultsVisible", True, type=bool
)
self.ui.actionToggleResults.setChecked(results_visible)
def _save_window_layout(self) -> None:
self.settings.setValue("simulationWindow/geometry", self.saveGeometry())
self.settings.setValue("simulationWindow/state", self.saveState())
self.settings.setValue(
"simulationWindow/resultsVisible",
self.ui.actionToggleResults.isChecked(),
)
self.settings.sync()
def begin_run(self) -> tuple:
"""Reset transient run state while retaining the current workspace."""
self._run_generation += 1
generation = self._run_generation
self._running = True
self._file_path = None
self.results.status = {}
self.results.messages.clear()
self.results.metadata.clear()
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.ui.statusLabel.setText("Preparing simulation…")
return (
lambda progress: self._report_progress(generation, progress),
lambda message: self._report_message(generation, message),
lambda result: self._report_finished(generation, result),
lambda error: self._report_error(generation, error),
)
def prepare_run_model(self, model_name: str | None) -> None:
"""Retain graph configuration only when rerunning the same model."""
if not model_name:
return
if self.results.model_name != model_name:
self.results = SimulationResults(model_name=model_name)
self.clear_result_views()
self.load_result_views()
else:
self.results.model_name = model_name
self.ui.statusLabel.setText("Preparing simulation…")
self._update_title()
def clear(self, checked: bool = False, *, model_name: str = "") -> None:
"""Discard the displayed run and prepare an empty results document."""
del checked
self._run_generation += 1
self._running = False
self._file_path = None
self.results = SimulationResults(model_name=model_name)
self.ui.statusLabel.setText("No simulation has been run yet.")
self.ui.progressBar.setValue(0)
self.ui.timeLabel.setText("Time: 0 s")
self.ui.messageList.clear()
self.clear_result_views()
self.load_result_views()
self._update_title()
def clear_result_views(self) -> None:
"""Clear custom plots before a run or loaded document is displayed.
Future graph widgets should be placed in the Designer-owned
``resultsLayout`` and reset here.
"""
self._rebuilding_graph_tabs = True
self._clear_graph_tab_widgets()
self._rebuilding_graph_tabs = False
self.ui.signalsTree.clear()
def load_result_views(self) -> None:
"""Populate custom plots from ``self.results.data`` and traces.
This is the intended integration point for a future plotting widget.
"""
self._rebuild_graph_tabs()
self._rebuild_signal_tree()
def _rebuild_graph_tabs(self) -> None:
current_page = self.ui.graphTabs.currentWidget()
current_graph_id = (
current_page.graph_id
if isinstance(current_page, GraphWorkspacePage)
else None
)
self._rebuilding_graph_tabs = True
self._clear_graph_tab_widgets()
for graph in self.results.graphs:
self.ui.graphTabs.addTab(
GraphWorkspacePage(graph, self.results), graph.title
)
if current_graph_id is not None:
for index, graph in enumerate(self.results.graphs):
if graph.id == current_graph_id:
self.ui.graphTabs.setCurrentIndex(index)
break
self._rebuilding_graph_tabs = False
self._update_graph_actions()
self._sync_signal_checks()
def _clear_graph_tab_widgets(self) -> None:
while self.ui.graphTabs.count():
page = self.ui.graphTabs.widget(0)
self.ui.graphTabs.removeTab(0)
page.deleteLater()
def add_graph_tab(self) -> None:
used_titles = {graph.title for graph in self.results.graphs}
number = 1
while f"Graph {number}" in used_titles:
number += 1
graph = SimulationGraph(title=f"Graph {number}")
self.results.graphs.append(graph)
index = self.ui.graphTabs.addTab(
GraphWorkspacePage(graph, self.results), graph.title
)
self.ui.graphTabs.setCurrentIndex(index)
self._update_graph_actions()
def remove_current_graph_tab(self) -> None:
index = self.ui.graphTabs.currentIndex()
if index < 0 or index >= len(self.results.graphs):
return
self.results.graphs.pop(index)
page = self.ui.graphTabs.widget(index)
self.ui.graphTabs.removeTab(index)
page.deleteLater()
self._update_graph_actions()
self._sync_signal_checks()
def rename_graph_tab(self, index: int) -> None:
if index < 0 or index >= len(self.results.graphs):
return
graph = self.results.graphs[index]
title, accepted = QInputDialog.getText(
self, "Rename Graph", "Title:", text=graph.title
)
title = title.strip()
if accepted and title:
graph.title = title
self.ui.graphTabs.setTabText(index, title)
page = self.ui.graphTabs.widget(index)
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def _move_graph_tab(self, old_index: int, new_index: int) -> None:
if self._rebuilding_graph_tabs or old_index == new_index:
return
graph = self.results.graphs.pop(old_index)
self.results.graphs.insert(new_index, graph)
self._sync_signal_checks()
def _update_graph_actions(self) -> None:
self.ui.actionRemoveGraph.setEnabled(bool(self.results.graphs))
def _rebuild_signal_tree(self) -> None:
"""Build a hierarchy while retaining each leaf's complete signal name."""
tree = self.ui.signalsTree
self._updating_signal_checks = True
try:
tree.clear()
items: dict[tuple[str, ...], QTreeWidgetItem] = {}
for signal_name in sorted(self.results.data, key=str.casefold):
parts = _signal_tree_parts(signal_name)
if not parts:
continue
parent = tree.invisibleRootItem()
for depth, part in enumerate(parts, start=1):
path = parts[:depth]
item = items.get(path)
if item is None:
item = QTreeWidgetItem(parent, [part])
items[path] = item
parent = item
parent.setData(0, Qt.ItemDataRole.UserRole, signal_name)
parent.setToolTip(0, signal_name)
parent.setFlags(parent.flags() | Qt.ItemFlag.ItemIsUserCheckable)
parent.setCheckState(0, Qt.CheckState.Unchecked)
tree.expandToDepth(0)
finally:
self._updating_signal_checks = False
self._sync_signal_checks()
def _current_graph_changed(self, _index: int) -> None:
if not self._rebuilding_graph_tabs:
self._sync_signal_checks()
def _current_graph(self) -> SimulationGraph | None:
index = self.ui.graphTabs.currentIndex()
if 0 <= index < len(self.results.graphs):
return self.results.graphs[index]
return None
def _sync_signal_checks(self) -> None:
graph = self._current_graph()
enabled = {trace.name for trace in graph.traces} if graph else set()
self._updating_signal_checks = True
try:
root = self.ui.signalsTree.invisibleRootItem()
pending = [root.child(index) for index in range(root.childCount())]
while pending:
item = pending.pop()
pending.extend(
item.child(index) for index in range(item.childCount())
)
signal_name = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(signal_name, str):
item.setCheckState(
0,
Qt.CheckState.Checked
if signal_name in enabled
else Qt.CheckState.Unchecked,
)
finally:
self._updating_signal_checks = False
self.ui.signalsTree.setEnabled(graph is not None)
def _signal_check_changed(self, item: QTreeWidgetItem, _column: int) -> None:
if self._updating_signal_checks:
return
signal_name = item.data(0, Qt.ItemDataRole.UserRole)
graph = self._current_graph()
if not isinstance(signal_name, str) or graph is None:
return
enabled = item.checkState(0) == Qt.CheckState.Checked
existing = next(
(trace for trace in graph.traces if trace.name == signal_name), None
)
if enabled and existing is None:
graph.traces.append(SimulationTrace(name=signal_name))
elif not enabled and existing is not None:
graph.traces.remove(existing)
page = self.ui.graphTabs.currentWidget()
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def selected_signal_names(self) -> list[str]:
"""Return full column names selected for future plotting actions."""
names = []
for item in self.ui.signalsTree.selectedItems():
name = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(name, str):
names.append(name)
return names
def show_signal_context_menu(self, position) -> None:
item = self.ui.signalsTree.itemAt(position)
signal_name = (
item.data(0, Qt.ItemDataRole.UserRole) if item is not None else None
)
graph = self._current_graph()
if not isinstance(signal_name, str) or graph is None:
return
menu = QMenu(self)
use_as_x_action = menu.addAction("Use as X Axis")
use_as_x_action.setCheckable(True)
use_as_x_action.setChecked(graph.x_axis == signal_name)
selected = menu.exec(
self.ui.signalsTree.viewport().mapToGlobal(position)
)
if selected is use_as_x_action:
self.set_x_axis_signal(signal_name)
def set_x_axis_signal(self, signal_name: str) -> None:
"""Set the current graph's persisted horizontal data column."""
graph = self._current_graph()
if graph is None or signal_name not in self.results.data:
return
graph.x_axis = signal_name
page = self.ui.graphTabs.currentWidget()
if isinstance(page, GraphWorkspacePage):
page.refresh_chart()
def open_results(self) -> None:
file_name, _selected_filter = QFileDialog.getOpenFileName(
self,
"Open Simulation Results",
"",
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
try:
results = load_simulation_results(file_name)
except ValueError as error:
QMessageBox.warning(self, "Cannot Open Simulation Results", str(error))
return
self._run_generation += 1
self._running = False
self._file_path = Path(file_name)
self.results = results
self._display_results()
def save_results(self) -> None:
file_path = self._file_path
if file_path is None:
self.save_results_as()
return
self._save_results_to(file_path)
def save_results_as(self) -> None:
default_name = f"{_safe_file_stem(self.results.model_name)}-results.ber"
file_name, selected_filter = QFileDialog.getSaveFileName(
self,
"Save Simulation Results As",
default_name,
"BEdit Binary Simulation Results (*.ber);;JSON Simulation Results (*.json)",
)
if not file_name:
return
file_path = Path(file_name)
if file_path.suffix.lower() not in {".ber", ".json"}:
suffix = ".json" if selected_filter.startswith("JSON") else ".ber"
file_path = file_path.with_suffix(suffix)
self._save_results_to(file_path)
def _save_results_to(self, file_path: Path) -> None:
try:
save_simulation_results(file_path, self.results)
except OSError as error:
QMessageBox.warning(self, "Cannot Save Simulation Results", str(error))
return
self._file_path = file_path
self._update_title()
def _display_results(self) -> None:
status = self.results.status
self.ui.statusLabel.setText(str(status.get("phase", "Loaded results")))
self.ui.progressBar.setValue(int(status.get("progress", 0)))
self.ui.timeLabel.setText(f"Time: {float(status.get('time', 0)):g} s")
self.ui.messageList.clear()
for message in self.results.messages:
prefix = message.get("stream") or message.get("type") or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.get('text', '')}")
self.clear_result_views()
self.load_result_views()
self._update_title()
def _update_title(self) -> None:
name = self.results.model_name or "Simulation"
self.setWindowTitle(f"{name} — Simulation")
def _report_progress(self, generation: int, progress: SimulationProgress) -> None:
if generation == self._run_generation:
self.progressReceived.emit(progress)
def _report_message(self, generation: int, message: SimulationMessage) -> None:
if generation == self._run_generation:
self.messageReceived.emit(message)
def _report_finished(self, generation: int, result) -> None:
if generation == self._run_generation:
self.simulationFinished.emit(result)
def _report_error(self, generation: int, error: Exception) -> None:
if generation == self._run_generation:
self.simulationFailed.emit(str(error))
def report_start_error(self, error: Exception) -> None:
self.simulationFailed.emit(str(error))
def _show_progress(self, progress: SimulationProgress) -> None:
self.results.status = {
"phase": progress.phase,
"currentStepSize": progress.current_step_size,
"time": progress.time,
"progress": progress.progress,
}
self.ui.statusLabel.setText(progress.phase or "Running")
self.ui.timeLabel.setText(f"Time: {progress.time:g} s")
self.ui.progressBar.setValue(max(0, min(10000, progress.progress)))
def _show_message(self, message: SimulationMessage) -> None:
self.results.messages.append(
{"stream": message.stream, "type": message.type, "text": message.text}
)
prefix = message.stream or message.type or "OpenModelica"
self.ui.messageList.addItem(f"{prefix}: {message.text}")
self.ui.messageList.scrollToBottom()
def _show_finished(self, result) -> None:
self._running = False
self.results.status.update(phase="Simulation finished", progress=10000)
if isinstance(result, SimulationExecutionResult):
self.results.data = result.data
available_signals = set(result.data)
for graph in self.results.graphs:
graph.traces = [
trace
for trace in graph.traces
if trace.name in available_signals
]
if graph.x_axis not in available_signals:
graph.x_axis = (
"time"
if "time" in available_signals
else next(iter(result.data), "")
)
self.results.metadata["processReturnCode"] = result.return_code
self.results.metadata["sourceResultFile"] = Path(result.result_file).name
else:
self.results.metadata["processResult"] = result
self.ui.progressBar.setValue(10000)
self.ui.statusLabel.setText("Simulation finished")
self.load_result_views()
def _show_error(self, message: str) -> None:
self._running = False
self.results.status["phase"] = "Simulation failed"
self.results.messages.append(
{"stream": "BEdit", "type": "error", "text": message}
)
self.ui.statusLabel.setText("Simulation failed")
self.ui.messageList.addItem(f"Error: {message}")
def show_about(self) -> None:
QMessageBox.about(
self,
"About BEdit Simulation",
"<h3>BEdit Simulation</h3>"
"<p>View live progress and open or save simulation results.</p>",
)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt API name)
self._save_window_layout()
super().closeEvent(event)
@property
def is_running(self) -> bool:
return self._running
class GraphWorkspacePage(QWidget):
"""Matplotlib view of one persisted simulation graph definition."""
def __init__(
self, graph: SimulationGraph, results: SimulationResults, parent=None
) -> None:
super().__init__(parent)
self.graph_id = graph.id
self.graph = graph
self.results = results
self.plot_layout = QVBoxLayout(self)
self.figure = Figure(layout="constrained")
self.canvas = InteractiveFigureCanvas(self.figure)
self.axes = self.figure.add_subplot(111)
self.canvas.axes = self.axes
self.navigation_toolbar = GraphNavigationToolbar(self.canvas, self)
self.canvas.navigation_toolbar = self.navigation_toolbar
self.plot_layout.addWidget(self.navigation_toolbar)
self.plot_layout.addWidget(self.canvas)
self.refresh_chart()
def refresh_chart(self) -> None:
self.axes.clear()
x_values = self.results.data.get(self.graph.x_axis)
for trace in self.graph.traces:
y_values = self.results.data.get(trace.name)
if y_values is None:
continue
horizontal = x_values if x_values is not None else range(len(y_values))
sample_count = min(len(horizontal), len(y_values))
color = trace.properties.get("color")
self.axes.plot(
list(horizontal)[:sample_count],
y_values[:sample_count],
label=trace.name,
color=color if isinstance(color, str) and color else None,
)
self.axes.set_title(self.graph.title)
self.axes.set_xlabel(self.graph.x_axis if x_values is not None else "sample")
self.axes.grid(True, alpha=0.25)
if self.axes.lines:
self.axes.legend()
self.axes.relim()
self.axes.autoscale_view()
self.canvas.set_home_view()
self.canvas.draw_idle()
class GraphNavigationToolbar(NavigationToolbar2QT):
"""Navigation toolbar whose Home action includes direct canvas navigation."""
def home(self, *args) -> None:
del args
self.canvas.reset_home_view()
class InteractiveFigureCanvas(FigureCanvasQTAgg):
"""Matplotlib canvas with always-available wheel zoom and drag pan."""
def __init__(self, figure: Figure) -> None:
super().__init__(figure)
self.axes = None
self.navigation_toolbar = None
self._pan_start = None
self._home_view = None
self.mpl_connect("scroll_event", self._zoom_at_cursor)
self.mpl_connect("button_press_event", self._start_pan)
self.mpl_connect("motion_notify_event", self._pan)
self.mpl_connect("button_release_event", self._finish_pan)
def _toolbar_is_active(self) -> bool:
return bool(
self.navigation_toolbar is not None
and self.navigation_toolbar.mode
)
def set_home_view(self) -> None:
if self.axes is not None:
self._home_view = (self.axes.get_xlim(), self.axes.get_ylim())
def reset_home_view(self) -> None:
if self.axes is None or self._home_view is None:
return
x_limits, y_limits = self._home_view
self.axes.set_xlim(x_limits)
self.axes.set_ylim(y_limits)
self.draw_idle()
def _zoom_at_cursor(self, event) -> None:
if (
self.axes is None
or event.inaxes is not self.axes
or event.xdata is None
or event.ydata is None
or self._toolbar_is_active()
):
return
scale = 0.8 if event.button == "up" else 1.25
left, right = self.axes.get_xlim()
bottom, top = self.axes.get_ylim()
self.axes.set_xlim(
event.xdata - (event.xdata - left) * scale,
event.xdata + (right - event.xdata) * scale,
)
self.axes.set_ylim(
event.ydata - (event.ydata - bottom) * scale,
event.ydata + (top - event.ydata) * scale,
)
self.draw_idle()
def _start_pan(self, event) -> None:
if (
self.axes is None
or event.inaxes is not self.axes
or event.button != MouseButton.LEFT
or self._toolbar_is_active()
):
return
self._pan_start = (
event.x,
event.y,
self.axes.get_xlim(),
self.axes.get_ylim(),
)
def _pan(self, event) -> None:
if (
self.axes is None
or self._pan_start is None
or event.x is None
or event.y is None
):
return
start_x, start_y, x_limits, y_limits = self._pan_start
width = max(self.axes.bbox.width, 1.0)
height = max(self.axes.bbox.height, 1.0)
delta_x = (event.x - start_x) * (x_limits[1] - x_limits[0]) / width
delta_y = (event.y - start_y) * (y_limits[1] - y_limits[0]) / height
self.axes.set_xlim(x_limits[0] - delta_x, x_limits[1] - delta_x)
self.axes.set_ylim(y_limits[0] - delta_y, y_limits[1] - delta_y)
self.draw_idle()
def _finish_pan(self, _event) -> None:
self._pan_start = None
def _safe_file_stem(model_name: str) -> str:
stem = re.sub(r"[^A-Za-z0-9_.-]+", "_", model_name).strip("._")
return stem or "simulation"
def _signal_tree_parts(signal_name: str) -> tuple[str, ...]:
"""Split a result name into readable component, variable, and index levels.
OpenModelica emits state derivatives as names such as ``der(block.x)``.
Keep those traces available, but display them below a ``Derivatives`` group
on their owning component instead of creating a misleading top-level
``der(block`` branch.
"""
derivative = re.fullmatch(r"der\((.+)\)", signal_name)
if derivative is not None:
inner_name = derivative.group(1)
inner_parts = _signal_tree_parts(inner_name)
if inner_parts:
index_count = len(re.findall(r"\[[^\]]+\]", inner_name.rsplit(".", 1)[-1]))
variable_index = max(0, len(inner_parts) - index_count - 1)
return (
*inner_parts[:variable_index],
"Derivatives",
*inner_parts[variable_index:],
)
return ("Derivatives", signal_name)
parts: list[str] = []
for segment in signal_name.split("."):
if not segment:
continue
match = re.fullmatch(r"([^\[]+)((?:\[[^\]]+\])+)", segment)
if match is None:
parts.append(segment)
continue
parts.append(match.group(1))
parts.extend(re.findall(r"\[([^\]]+)\]", match.group(2)))
return tuple(parts)

View File

@@ -12,6 +12,7 @@
<item row="1" column="0"><widget class="QLabel" name="iconLabel"><property name="text"><string>Icon:</string></property></widget></item>
<item row="1" column="1"><widget class="QPushButton" name="editIconButton"><property name="text"><string>Edit Icon…</string></property><property name="toolTip"><string>Open the vector icon and port-position editor</string></property></widget></item>
<item row="2" column="0" colspan="2"><widget class="QCheckBox" name="showSubtreeCheckBox"><property name="text"><string>Show contained components in the Libraries tree</string></property><property name="checked"><bool>true</bool></property></widget></item>
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="showNameCheckBox"><property name="text"><string>Show name below component</string></property></widget></item>
</layout>
</item>
<item><spacer name="optionsSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConnectionChooserDialog</class>
<widget class="QDialog" name="ConnectionChooserDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>460</width><height>280</height></rect></property>
<property name="windowTitle"><string>Select a Connection</string></property>
<layout class="QVBoxLayout" name="dialogLayout">
<item><widget class="QLabel" name="promptLabel"><property name="text"><string>Select a connection:</string></property></widget></item>
<item><widget class="QListWidget" name="connectionList"><property name="alternatingRowColors"><bool>true</bool></property></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>ConnectionChooserDialog</receiver><slot>accept()</slot><hints/></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>ConnectionChooserDialog</receiver><slot>reject()</slot><hints/></connection>
<connection><sender>connectionList</sender><signal>itemDoubleClicked(QListWidgetItem*)</signal><receiver>ConnectionChooserDialog</receiver><slot>accept()</slot><hints/></connection>
</connections>
</ui>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GraphParametersDialog</class>
<widget class="QDialog" name="GraphParametersDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>620</width><height>480</height></rect></property>
<property name="windowTitle"><string>Graph Parameters</string></property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QLabel" name="descriptionLabel">
<property name="text"><string>Edit parameter values throughout the active component tree.</string></property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="parameterTree">
<property name="alternatingRowColors"><bool>true</bool></property>
<property name="rootIsDecorated"><bool>true</bool></property>
<property name="columnCount"><number>3</number></property>
<column><property name="text"><string>Component / Parameter</string></property></column>
<column><property name="text"><string>Type</string></property></column>
<column><property name="text"><string>Value</string></property></column>
</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>GraphParametersDialog</receiver><slot>accept()</slot><hints/></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>GraphParametersDialog</receiver><slot>reject()</slot><hints/></connection>
</connections>
</ui>

View File

@@ -2,35 +2,260 @@
<ui version="4.0">
<class>IconEditorDialog</class>
<widget class="QDialog" name="IconEditorDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>850</width><height>600</height></rect></property>
<property name="windowTitle"><string>Icon Editor</string></property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>850</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Icon Editor</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<layout class="QHBoxLayout" name="shapeToolbarLayout">
<item><widget class="QToolButton" name="pointerButton"><property name="text"><string>Pointer</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property></widget></item>
<item><widget class="QLabel" name="addShapeLabel"><property name="text"><string>Add:</string></property></widget></item>
<item><widget class="QToolButton" name="addRectangleButton"><property name="text"><string>Rectangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addCircleButton"><property name="text"><string>Circle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addEllipseButton"><property name="text"><string>Ellipse</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addLineButton"><property name="text"><string>Line</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addTriangleButton"><property name="text"><string>Triangle</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><widget class="QToolButton" name="addTextButton"><property name="text"><string>Text</string></property><property name="checkable"><bool>true</bool></property></widget></item>
<item><spacer name="toolbarSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item>
<item><widget class="QToolButton" name="zoomInButton"><property name="text"><string>+</string></property><property name="toolTip"><string>Zoom in</string></property></widget></item>
<item><widget class="QToolButton" name="zoomOutButton"><property name="text"><string></string></property><property name="toolTip"><string>Zoom out</string></property></widget></item>
<item><widget class="QToolButton" name="centerButton"><property name="text"><string>Fit</string></property><property name="toolTip"><string>Fit and center the icon canvas</string></property></widget></item>
<item><widget class="QPushButton" name="deleteSelectedButton"><property name="text"><string>Delete selected</string></property></widget></item>
<item>
<widget class="QToolButton" name="pointerButton">
<property name="text">
<string>Pointer</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/edit-select.png</normaloff>:/icons/icons/edit-select.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="addShapeLabel">
<property name="text">
<string>Add:</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addRectangleButton">
<property name="text">
<string>Rectangle</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-rectangle.png</normaloff>:/icons/icons/draw-rectangle.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addCircleButton">
<property name="text">
<string>Circle</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-circle.png</normaloff>:/icons/icons/draw-circle.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addEllipseButton">
<property name="text">
<string>Ellipse</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-ellipse.png</normaloff>:/icons/icons/draw-ellipse.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addLineButton">
<property name="text">
<string>Line</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-bezier-curves.png</normaloff>:/icons/icons/draw-bezier-curves.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addTriangleButton">
<property name="text">
<string>Triangle</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-triangle.png</normaloff>:/icons/icons/draw-triangle.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addTextButton">
<property name="text">
<string>Text</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/draw-text.png</normaloff>:/icons/icons/draw-text.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="toolbarSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="zoomInButton">
<property name="toolTip">
<string>Zoom in</string>
</property>
<property name="text">
<string>+</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-in.png</normaloff>:/icons/icons/zoom-in.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="zoomOutButton">
<property name="toolTip">
<string>Zoom out</string>
</property>
<property name="text">
<string></string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-out.png</normaloff>:/icons/icons/zoom-out.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="centerButton">
<property name="toolTip">
<string>Fit and center the icon canvas</string>
</property>
<property name="text">
<string>Fit</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/zoom-original.png</normaloff>:/icons/icons/zoom-original.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteSelectedButton">
<property name="text">
<string>Delete selected</string>
</property>
</widget>
</item>
</layout>
</item>
<item><widget class="IconCanvasView" name="iconView"><property name="dragMode"><enum>QGraphicsView::DragMode::RubberBandDrag</enum></property></widget></item>
<item><widget class="QLabel" name="portHintLabel"><property name="text"><string>Green points are inputs; red points are outputs. Drag them to place connection anchors.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
<item>
<widget class="IconCanvasView" name="iconView">
<property name="dragMode">
<enum>QGraphicsView::DragMode::RubberBandDrag</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="portHintLabel">
<property name="text">
<string>Green points are inputs; red points are outputs. Drag them to place connection anchors.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets><customwidget><class>IconCanvasView</class><extends>QGraphicsView</extends><header>bedit.gui.graphics.icon_canvas</header></customwidget></customwidgets>
<resources/>
<customwidgets>
<customwidget>
<class>IconCanvasView</class>
<extends>QGraphicsView</extends>
<header>bedit.gui.graphics.icon_canvas</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections>
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>IconEditorDialog</receiver><slot>accept()</slot><hints/></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>IconEditorDialog</receiver><slot>reject()</slot><hints/></connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>IconEditorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>IconEditorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -264,16 +264,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="applyJsonButton">
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Apply JSON</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="pointerToolButton">
<property name="text">
@@ -296,6 +286,10 @@
<property name="text">
<string>Connect</string>
</property>
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/network-connect.png</normaloff>:/icons/icons/network-connect.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
@@ -366,118 +360,119 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="routingLabel">
<property name="text">
<string>Line:</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="directRoutingButton">
<property name="text">
<string>Direct</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="angledRoutingButton">
<property name="text">
<string>Angled</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="splineRoutingButton">
<property name="text">
<string>Spline</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="workspaceStack">
<property name="currentIndex">
<number>0</number>
<widget class="QSplitter" name="workspaceVerticalSplitter">
<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="jsonPage">
<layout class="QVBoxLayout" name="jsonPageLayout">
<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="jsonEditor">
<property name="lineWrapMode">
<enum>QPlainTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="placeholderText">
<string>Component JSON</string>
</property>
</widget>
</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 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>
<widget class="QWidget" name="logTab">
<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>
</layout>
</widget>
</widget>
</widget>
</item>
@@ -502,6 +497,8 @@
</property>
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionReloadLibraries"/>
<addaction name="actionReloadSimulation"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
@@ -548,9 +545,21 @@
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<widget class="QMenu" name="menuSimulation">
<property name="title">
<string>Simulation</string>
</property>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompose"/>
<addaction name="actionExportModel"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
<addaction name="menuView"/>
<addaction name="menuSimulation"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="fileToolbar">
@@ -622,6 +631,103 @@
<addaction name="actionZoomOut"/>
<addaction name="actionCenterView"/>
</widget>
<widget class="QToolBar" name="simulationToolbar">
<property name="windowTitle">
<string>Simulation</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionSimulationSettings"/>
<addaction name="actionGraphParameters"/>
<addaction name="actionCompose"/>
<addaction name="actionSimulationWindow"/>
<addaction name="actionRunSimulation"/>
</widget>
<action name="actionSimulationSettings">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/preferences-system.png</normaloff>:/icons/icons/preferences-system.png</iconset>
</property>
<property name="text">
<string>Simulation Settings</string>
</property>
<property name="statusTip">
<string>Edit settings stored in the active graph</string>
</property>
</action>
<action name="actionGraphParameters">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/view-form-table.png</normaloff>:/icons/icons/view-form-table.png</iconset>
</property>
<property name="text">
<string>Graph Parameters</string>
</property>
<property name="statusTip">
<string>Edit parameters throughout the active graph</string>
</property>
</action>
<action name="actionCompose">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/run-build.png</normaloff>:/icons/icons/run-build.png</iconset>
</property>
<property name="text">
<string>Compose</string>
</property>
<property name="statusTip">
<string>Compose the active graph as an OpenModelica model</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
</action>
<action name="actionRunSimulation">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/media-playback-start.png</normaloff>:/icons/icons/media-playback-start.png</iconset>
</property>
<property name="text">
<string>Run</string>
</property>
<property name="statusTip">
<string>Run the simulation</string>
</property>
<property name="shortcut">
<string>F6</string>
</property>
</action>
<action name="actionSimulationWindow">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<property name="text">
<string>Simulation Window</string>
</property>
<property name="statusTip">
<string>Show the simulation results window</string>
</property>
</action>
<action name="actionExportModel">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Export Model…</string>
</property>
<property name="statusTip">
<string>Save the composed OpenModelica model to a file</string>
</property>
</action>
<action name="actionNew">
<property name="icon">
<iconset resource="../resources/resources.qrc">
@@ -703,6 +809,28 @@
<string>Ctrl+O</string>
</property>
</action>
<action name="actionReloadLibraries">
<property name="text">
<string>Reload &amp;Libraries</string>
</property>
<property name="statusTip">
<string>Reload configured library files from disk</string>
</property>
<property name="shortcut">
<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">
<property name="icon">
<iconset resource="../resources/resources.qrc">
@@ -842,6 +970,12 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>TextDefinitionEditor</class>
<extends>QWidget</extends>
<header>bedit.gui.editors.text_definition</header>
<container>1</container>
</customwidget>
<customwidget>
<class>GraphWorkspaceView</class>
<extends>QGraphicsView</extends>

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

@@ -27,7 +27,8 @@
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
<item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item>
<item row="3" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="multipleConnectionsCheckBox"><property name="text"><string>Allow multiple connections</string></property></widget></item>
<item row="4" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
</layout>
</widget>
</widget>

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,34 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="simulationTab">
<attribute name="title"><string>Simulation</string></attribute>
<layout class="QVBoxLayout" name="simulationTabLayout">
<item>
<widget class="QGroupBox" name="openModelicaGroupBox">
<property name="title"><string>OpenModelica</string></property>
<layout class="QVBoxLayout" name="openModelicaLayout">
<item><widget class="QLabel" name="openModelicaPathLabel"><property name="text"><string>OpenModelica executable:</string></property></widget></item>
<item>
<layout class="QHBoxLayout" name="openModelicaPathLayout">
<item><widget class="QLineEdit" name="openModelicaPathEdit"><property name="placeholderText"><string>Leave empty to find omc on PATH</string></property></widget></item>
<item><widget class="QPushButton" name="browseOpenModelicaButton"><property name="text"><string>Browse…</string></property></widget></item>
</layout>
</item>
<item><widget class="QLabel" name="openModelicaHintLabel"><property name="text"><string>Select the omc executable, for example ~/.local/bin/omc.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
</layout>
</widget>
</item>
<item><spacer name="simulationSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>
</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>
@@ -94,7 +122,7 @@
<item>
<widget class="QLabel" name="libraryPathsLabel">
<property name="text">
<string>Load library JSON files from these files or folders at startup:</string>
<string>Load library JSON or BEdit Binary files from these files or folders at startup:</string>
</property>
<property name="wordWrap">
<bool>true</bool>

View File

@@ -0,0 +1,186 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SimulationSettingsDialog</class>
<widget class="QDialog" name="SimulationSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>311</height>
</rect>
</property>
<property name="windowTitle">
<string>Simulation Settings</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QTabWidget" name="settingsGroup">
<widget class="QWidget" name="generalPage" native="true">
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="settingsLayout">
<item>
<widget class="QGroupBox" name="simulationInterval">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>Simulation Interval</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Start time:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Gui::DoubleSpinBox" name="startTime">
<property name="enabled">
<bool>true</bool>
</property>
<property name="prefix">
<string/>
</property>
<property name="suffix">
<string>s</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Stop time:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::DoubleSpinBox" name="stopTime">
<property name="suffix">
<string>s</string>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="interval_number">
<property name="text">
<string>Number of intervals:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QRadioButton" name="interval_time">
<property name="text">
<string>Interval:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Gui::IntSpinBox" name="numberOfIntervals">
<property name="maximum">
<number>999999999</number>
</property>
<property name="value">
<number>500</number>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="Gui::DoubleSpinBox" name="intervalTime">
<property name="suffix">
<string>s</string>
</property>
<property name="decimals">
<number>5</number>
</property>
<property name="stepType">
<enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
</property>
<property name="value">
<double>0.002000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="settingsSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</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>
<customwidgets>
<customwidget>
<class>Gui::IntSpinBox</class>
<extends>QSpinBox</extends>
<header>Gui/SpinBox.h</header>
</customwidget>
<customwidget>
<class>Gui::DoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>Gui/SpinBox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SimulationSettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SimulationSettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,322 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SimulationWindow</class>
<widget class="QMainWindow" name="SimulationWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Simulation</string>
</property>
<property name="windowIcon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/office-chart-line.png</normaloff>:/icons/icons/office-chart-line.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="resultsLayout">
<item>
<widget class="QTabWidget" name="graphTabs">
<property name="tabsClosable">
<bool>false</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>&amp;File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="separator"/>
<addaction name="actionClear"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>&amp;View</string>
</property>
<widget class="QMenu" name="menuPanels">
<property name="title">
<string>&amp;Panels</string>
</property>
</widget>
<widget class="QMenu" name="menuToolbars">
<property name="title">
<string>&amp;Toolbars</string>
</property>
</widget>
<addaction name="menuPanels"/>
<addaction name="menuToolbars"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>&amp;Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<widget class="QMenu" name="menuGraph">
<property name="title">
<string>&amp;Graph</string>
</property>
<addaction name="actionAddGraph"/>
<addaction name="actionRemoveGraph"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuGraph"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="workspaceToolbar">
<property name="windowTitle">
<string>Workspace</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionAddGraph"/>
<addaction name="actionRemoveGraph"/>
</widget>
<widget class="QToolBar" name="fileToolbar">
<property name="windowTitle">
<string>File</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionClear"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
</widget>
<widget class="QDockWidget" name="statusDock">
<property name="windowTitle">
<string>Status</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="statusDockContents">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time: 0 s</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="format">
<string>%p%</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>No simulation has been run yet.</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="logDock">
<property name="floating">
<bool>false</bool>
</property>
<property name="features">
<set>QDockWidget::DockWidgetFeature::DockWidgetFloatable|QDockWidget::DockWidgetFeature::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Log</string>
</property>
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="logDockContents">
<layout class="QVBoxLayout" name="logLayout">
<item>
<widget class="QListWidget" name="messageList">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="signalsDock">
<property name="windowTitle">
<string>Signals</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="signalsDockContents">
<layout class="QVBoxLayout" name="signalsLayout">
<item>
<widget class="QTreeWidget" name="signalsTree">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Signal</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="actionOpen">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
</property>
<property name="text">
<string>&amp;Open…</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionSave">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save.png</normaloff>:/icons/icons/document-save.png</iconset>
</property>
<property name="text">
<string>&amp;Save…</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionClear">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-new.png</normaloff>:/icons/icons/document-new.png</iconset>
</property>
<property name="text">
<string>&amp;Clear</string>
</property>
</action>
<action name="actionSaveAs">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Save &amp;As…</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>E&amp;xit</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>&amp;About Simulation Window</string>
</property>
</action>
<action name="actionAboutQt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionToggleResults">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Results</string>
</property>
</action>
<action name="actionAddGraph">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/list-add.png</normaloff>:/icons/icons/list-add.png</iconset>
</property>
<property name="text">
<string>Add Graph</string>
</property>
<property name="statusTip">
<string>Add a graph workspace tab</string>
</property>
</action>
<action name="actionRemoveGraph">
<property name="icon">
<iconset resource="../resources/resources.qrc">
<normaloff>:/icons/icons/list-remove.png</normaloff>:/icons/icons/list-remove.png</iconset>
</property>
<property name="text">
<string>Remove Current Graph</string>
</property>
<property name="statusTip">
<string>Remove the current graph workspace tab</string>
</property>
</action>
</widget>
<resources>
<include location="../resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextDefinitionEditor</class>
<widget class="QWidget" name="TextDefinitionEditor">
<property name="geometry"><rect><x>0</x><y>0</y><width>900</width><height>600</height></rect></property>
<layout class="QHBoxLayout" name="editorLayout">
<property name="leftMargin"><number>6</number></property>
<property name="topMargin"><number>6</number></property>
<property name="rightMargin"><number>6</number></property>
<property name="bottomMargin"><number>6</number></property>
<item>
<widget class="QSplitter" name="columnSplitter">
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QSplitter" name="sourceSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="declarationsGroup">
<property name="title"><string>Declarations</string></property>
<layout class="QVBoxLayout" name="declarationsLayout">
<item><widget class="OpenModelicaEditor" name="declarationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="initialEquationsGroup">
<property name="title"><string>Initial Equations</string></property>
<layout class="QVBoxLayout" name="initialEquationsLayout">
<item><widget class="OpenModelicaEditor" name="initialEquationsEdit"/></item>
</layout>
</widget>
<widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="OpenModelicaEditor" name="equationsEdit"/></item>
</layout>
</widget>
</widget>
<widget class="QSplitter" name="definitionSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="portsGroup">
<property name="title"><string>Ports</string></property>
<layout class="QVBoxLayout" name="portsLayout">
<item><widget class="QTableWidget" name="portsTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>4</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Orientation</string></property></column><column><property name="text"><string>Multiple</string></property></column></widget></item>
<item><layout class="QHBoxLayout" name="portButtonsLayout"><item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item><item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item><item><spacer name="portButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
</layout>
</widget>
<widget class="QGroupBox" name="parametersGroup">
<property name="title"><string>Parameters</string></property>
<layout class="QVBoxLayout" name="parametersLayout">
<item><widget class="QTableWidget" name="parametersTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>3</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Value</string></property></column></widget></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><item><spacer name="parameterButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>OpenModelicaEditor</class>
<extends>QPlainTextEdit</extends>
<header>bedit.gui.editors.openmodelica</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

File diff suppressed because it is too large Load Diff

44
Test.mo Normal file
View File

@@ -0,0 +1,44 @@
model model_6060fdd6_e816_4da9_8bb6_fde1a66d3e92
model model_2e6a192c_5aeb_45d6_80ee_d64a72392b76
output Real y;
parameter Real v = 1;
equation
y = v;
end model_2e6a192c_5aeb_45d6_80ee_d64a72392b76;
model f4a78255_8b73_4bb3_ae8e_24b566c72bc0
input Real u;
output Real y;
parameter Real k = 2.5;
equation
y = k*u;
end f4a78255_8b73_4bb3_ae8e_24b566c72bc0;
model model_0b29c706_eb1b_4839_8ad2_bbed64efc01e
output Real y;
parameter Real v = 4.8;
equation
y = v+time;
end model_0b29c706_eb1b_4839_8ad2_bbed64efc01e;
model model_43f739a0_e50f_44ae_bd95_0e0c1a10d1cf
input Real u;
output Real y;
parameter Real k = -5;
equation
y = k*u;
end model_43f739a0_e50f_44ae_bd95_0e0c1a10d1cf;
model model_59bb51be_8185_4b0b_a894_e38db61aff18
input Real u[2];
output Real y;
equation
y = sum(u[i] for i in 1:2 );
end model_59bb51be_8185_4b0b_a894_e38db61aff18;
model_2e6a192c_5aeb_45d6_80ee_d64a72392b76 Constant0;
f4a78255_8b73_4bb3_ae8e_24b566c72bc0 gain0;
model_0b29c706_eb1b_4839_8ad2_bbed64efc01e const_and_time;
model_43f739a0_e50f_44ae_bd95_0e0c1a10d1cf gain1;
model_59bb51be_8185_4b0b_a894_e38db61aff18 add0;
equation
gain0.u = Constant0.y;
gain1.u = const_and_time.y;
add0.u[1] = gain0.y;
add0.u[2] = gain1.y;
end model_6060fdd6_e816_4da9_8bb6_fde1a66d3e92;

44
m_Test.mo Normal file
View File

@@ -0,0 +1,44 @@
model m_Test
model m_Constant0
output Real y;
parameter Real v = 2;
equation
y = v;
end m_Constant0;
model m_gain0
input Real u;
output Real y;
parameter Real k = 2.5;
equation
y = k*u;
end m_gain0;
model m_const_and_time
output Real y;
parameter Real v = 4.8;
equation
y = v+sin(time);
end m_const_and_time;
model m_gain1
input Real u;
output Real y;
parameter Real k = -5;
equation
y = k*u;
end m_gain1;
model m_add0
input Real u[2];
output Real y;
equation
y = sum(u[i] for i in 1:2 );
end m_add0;
m_Constant0 Constant0;
m_gain0 gain0;
m_const_and_time const_and_time;
m_gain1 gain1;
m_add0 add0;
equation
gain0.u = Constant0.y;
gain1.u = const_and_time.y;
add0.u[1] = gain0.y;
add0.u[2] = gain1.y;
end m_Test;