new text mode editor

This commit is contained in:
2026-07-20 14:24:47 +02:00
parent bf2512995f
commit a067c85994
14 changed files with 685 additions and 392 deletions

View File

@@ -1,4 +1,3 @@
import json
from copy import deepcopy
from pathlib import Path
@@ -13,7 +12,7 @@ from PySide6.QtWidgets import (
QTabWidget,
)
from bedit.core.model import Component, Port
from bedit.core.model import Component, Parameter
from bedit.core.serializer import JsonDocumentSerializer
from bedit.gui.controllers.document import DocumentController
from bedit.gui.dialogs.component_options import ComponentOptionsDialog
@@ -40,6 +39,7 @@ class MainWindow(QMainWindow):
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.settings = application_settings()
self._applying_text_definition = False
self.libraries = LibraryRepository(self)
self.document_controller = DocumentController(self)
@@ -108,12 +108,18 @@ 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.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.actionSave.triggered.connect(self.save_document)
self.ui.actionSaveAs.triggered.connect(self.save_document_as)
self.ui.actionClose.triggered.connect(self.close_document)
@@ -189,7 +195,6 @@ 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._update_edit_actions()
return
@@ -199,13 +204,14 @@ class MainWindow(QMainWindow):
)
is_graph = component.implementation_kind == "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:
@@ -259,62 +265,70 @@ class MainWindow(QMainWindow):
"text": self.ui.textToolButton,
}[mode].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("equations", ""),
component.inputs,
component.outputs,
[
Parameter.from_dict(parameter)
for parameter in component.source.get("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 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.equations,
self.ui.textDefinitionEditor.parameters,
)
finally:
self._applying_text_definition = False
except (TypeError, ValueError) as error:
QMessageBox.critical(self, "Invalid text component", str(error))
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
@@ -436,9 +450,9 @@ class MainWindow(QMainWindow):
ports_action = menu.addAction("Port Options…")
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)
@@ -521,15 +535,18 @@ 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(),
dialog.ui.showNameCheckBox.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:
QMessageBox.warning(self, "Cannot rename component", str(error))
@Slot(str, str)
def show_port_options(self, port_id: str, direction: str) -> None: