proper modelica text model editing
This commit is contained in:
@@ -344,11 +344,19 @@ class Component:
|
||||
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")
|
||||
|
||||
@@ -123,6 +123,14 @@ def emit_model(
|
||||
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)
|
||||
@@ -134,8 +142,9 @@ def emit_model(
|
||||
f"{body_indent}{junction_type} {_junction_name(junction['id'])};"
|
||||
)
|
||||
|
||||
lines.append(f"{indentation}equation")
|
||||
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", [])
|
||||
@@ -151,8 +160,21 @@ def emit_model(
|
||||
# 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()
|
||||
@@ -161,7 +183,6 @@ def emit_model(
|
||||
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."""
|
||||
|
||||
|
||||
@@ -47,13 +47,7 @@ class Simulation:
|
||||
"""Compose and retain the active graph's Modelica representation."""
|
||||
|
||||
self.model_path = None
|
||||
|
||||
# Create openmodelica model
|
||||
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
|
||||
self.compose_source(graph)
|
||||
|
||||
def _model_compiled(result):
|
||||
log.info("Compiling OK: %s", result)
|
||||
@@ -79,6 +73,17 @@ class Simulation:
|
||||
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],
|
||||
|
||||
@@ -168,7 +168,13 @@ class DocumentController(QObject):
|
||||
name=self._available_component_name(base_name, self.document.roots.values(), number),
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": ""} 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)
|
||||
@@ -187,7 +193,13 @@ class DocumentController(QObject):
|
||||
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": ""} if kind == "text" else {},
|
||||
source={
|
||||
"declarations": "",
|
||||
"initialEquations": "",
|
||||
"equations": "",
|
||||
}
|
||||
if kind == "text"
|
||||
else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||
return component.id
|
||||
@@ -427,6 +439,12 @@ class DocumentController(QObject):
|
||||
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,
|
||||
@@ -636,6 +654,8 @@ class DocumentController(QObject):
|
||||
self,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
declarations: str,
|
||||
initial_equations: str,
|
||||
equations: str,
|
||||
parameters: list[Parameter],
|
||||
) -> None:
|
||||
@@ -682,6 +702,8 @@ class DocumentController(QObject):
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"source": {
|
||||
"equations": equations,
|
||||
"declarations": declarations,
|
||||
"initialEquations": initial_equations,
|
||||
},
|
||||
"parameters": [parameter.to_dict() for parameter in parameters],
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||
|
||||
|
||||
class TextDefinitionEditor(QWidget):
|
||||
"""Editor for a text component's equations, ports, and parameters."""
|
||||
"""Editor for a text component's Modelica source, ports, and parameters."""
|
||||
|
||||
modifiedChanged = Signal(bool)
|
||||
definitionEdited = Signal()
|
||||
@@ -32,7 +32,10 @@ class TextDefinitionEditor(QWidget):
|
||||
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)
|
||||
@@ -52,6 +55,14 @@ class TextDefinitionEditor(QWidget):
|
||||
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] = []
|
||||
@@ -87,12 +98,16 @@ class TextDefinitionEditor(QWidget):
|
||||
|
||||
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:
|
||||
@@ -102,11 +117,7 @@ class TextDefinitionEditor(QWidget):
|
||||
self.ui.parametersTable.setRowCount(0)
|
||||
for parameter in parameters:
|
||||
self._append_parameter(parameter)
|
||||
self.ui.equationsEdit.set_symbols(
|
||||
[port.name for port in inputs],
|
||||
[port.name for port in outputs],
|
||||
[parameter.name for parameter in parameters],
|
||||
)
|
||||
self._set_editor_symbols(inputs, outputs, parameters)
|
||||
self._loading = False
|
||||
self.set_modified(False)
|
||||
self._update_buttons()
|
||||
@@ -128,11 +139,22 @@ class TextDefinitionEditor(QWidget):
|
||||
|
||||
def _refresh_editor_symbols(self) -> None:
|
||||
inputs, outputs = self.ports
|
||||
self.ui.equationsEdit.set_symbols(
|
||||
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 self.parameters],
|
||||
[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)
|
||||
|
||||
@@ -56,50 +56,53 @@ class Ui_MainWindow(object):
|
||||
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")
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/document-new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionNew.setIcon(icon5)
|
||||
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")
|
||||
icon6 = QIcon()
|
||||
icon6.addFile(u":/icons/icons/transform-rotate.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionRotateClockwise.setIcon(icon6)
|
||||
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")
|
||||
icon7 = QIcon()
|
||||
icon7.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionZoomIn.setIcon(icon7)
|
||||
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")
|
||||
icon8 = QIcon()
|
||||
icon8.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionZoomOut.setIcon(icon8)
|
||||
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")
|
||||
icon9 = QIcon()
|
||||
icon9.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionCenterView.setIcon(icon9)
|
||||
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")
|
||||
icon10 = QIcon()
|
||||
icon10.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon10)
|
||||
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")
|
||||
icon11 = QIcon()
|
||||
icon11.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSave.setIcon(icon11)
|
||||
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")
|
||||
icon12 = QIcon()
|
||||
icon12.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionSaveAs.setIcon(icon12)
|
||||
self.actionSaveAs.setIcon(icon5)
|
||||
self.actionExit = QAction(MainWindow)
|
||||
self.actionExit.setObjectName(u"actionExit")
|
||||
self.actionClose = QAction(MainWindow)
|
||||
@@ -295,7 +298,7 @@ class Ui_MainWindow(object):
|
||||
|
||||
self.rotateToolButton = QToolButton(self.workspaceHeader)
|
||||
self.rotateToolButton.setObjectName(u"rotateToolButton")
|
||||
self.rotateToolButton.setIcon(icon6)
|
||||
self.rotateToolButton.setIcon(icon7)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.rotateToolButton)
|
||||
|
||||
@@ -441,6 +444,7 @@ class Ui_MainWindow(object):
|
||||
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)
|
||||
@@ -496,6 +500,10 @@ class Ui_MainWindow(object):
|
||||
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)
|
||||
|
||||
@@ -34,7 +34,31 @@ class Ui_TextDefinitionEditor(object):
|
||||
self.columnSplitter.setObjectName(u"columnSplitter")
|
||||
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.columnSplitter.setChildrenCollapsible(False)
|
||||
self.equationsGroup = QGroupBox(self.columnSplitter)
|
||||
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")
|
||||
@@ -43,7 +67,8 @@ class Ui_TextDefinitionEditor(object):
|
||||
|
||||
self.equationsLayout.addWidget(self.equationsEdit)
|
||||
|
||||
self.columnSplitter.addWidget(self.equationsGroup)
|
||||
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)
|
||||
@@ -141,6 +166,8 @@ class Ui_TextDefinitionEditor(object):
|
||||
# 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)
|
||||
|
||||
@@ -181,6 +181,7 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
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)
|
||||
@@ -261,6 +262,35 @@ class MainWindow(QMainWindow):
|
||||
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()
|
||||
@@ -307,6 +337,7 @@ class MainWindow(QMainWindow):
|
||||
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
|
||||
@@ -318,6 +349,7 @@ class MainWindow(QMainWindow):
|
||||
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(
|
||||
@@ -505,6 +537,8 @@ class MainWindow(QMainWindow):
|
||||
if component is None:
|
||||
return
|
||||
self.ui.textDefinitionEditor.set_definition(
|
||||
component.source.get("declarations", ""),
|
||||
component.source.get("initialEquations", ""),
|
||||
component.source.get("equations", ""),
|
||||
component.inputs,
|
||||
component.outputs,
|
||||
@@ -522,7 +556,8 @@ class MainWindow(QMainWindow):
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Apply text component changes?",
|
||||
"The text component has unapplied equation, port, or parameter changes.",
|
||||
"The text component has unapplied declaration, initial-equation, "
|
||||
"equation, port, or parameter changes.",
|
||||
QMessageBox.StandardButton.Apply
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
@@ -540,6 +575,8 @@ class MainWindow(QMainWindow):
|
||||
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,
|
||||
)
|
||||
@@ -672,6 +709,8 @@ class MainWindow(QMainWindow):
|
||||
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:
|
||||
|
||||
@@ -691,7 +691,27 @@ def _safe_file_stem(model_name: str) -> str:
|
||||
|
||||
|
||||
def _signal_tree_parts(signal_name: str) -> tuple[str, ...]:
|
||||
"""Split dots and array indices into display hierarchy segments."""
|
||||
"""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("."):
|
||||
|
||||
Reference in New Issue
Block a user