new text mode editor
This commit is contained in:
@@ -322,3 +322,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
|
||||
|
||||
@@ -31,6 +31,7 @@ from bedit.core.model import (
|
||||
Endpoint,
|
||||
GraphDocument,
|
||||
Icon,
|
||||
Parameter,
|
||||
Port,
|
||||
clone_component,
|
||||
)
|
||||
@@ -47,6 +48,7 @@ class DocumentController(QObject):
|
||||
componentMoved = Signal(str, QPointF)
|
||||
componentRotated = Signal(str, float)
|
||||
componentPropertiesChanged = Signal(str)
|
||||
textDefinitionChanged = Signal(str)
|
||||
connectionAdded = Signal(str)
|
||||
connectionRemoved = Signal(str)
|
||||
graphItemChanged = Signal(str, str)
|
||||
@@ -153,12 +155,13 @@ 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={"equations": "", "parameters": []} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, None, component))
|
||||
self.activate_component(component.id)
|
||||
@@ -171,12 +174,13 @@ 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={"equations": "", "parameters": []} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||
return component.id
|
||||
@@ -211,6 +215,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
|
||||
@@ -470,7 +477,8 @@ class DocumentController(QObject):
|
||||
self,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
source: dict,
|
||||
equations: str,
|
||||
parameters: list[Parameter],
|
||||
) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "text":
|
||||
@@ -479,6 +487,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:
|
||||
@@ -505,7 +520,10 @@ class DocumentController(QObject):
|
||||
new = {
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"source": deepcopy(source),
|
||||
"source": {
|
||||
"equations": equations,
|
||||
"parameters": [parameter.to_dict() for parameter in parameters],
|
||||
},
|
||||
}
|
||||
if old != new:
|
||||
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
|
||||
@@ -525,6 +543,9 @@ class DocumentController(QObject):
|
||||
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(),
|
||||
@@ -726,10 +747,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:
|
||||
@@ -759,6 +783,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")
|
||||
@@ -1039,5 +1083,4 @@ class DocumentController(QObject):
|
||||
component.source = deepcopy(values["source"])
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
self.textDefinitionChanged.emit(component_id)
|
||||
|
||||
1
BEdit/src/bedit/gui/editors/__init__.py
Normal file
1
BEdit/src/bedit/gui/editors/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable editing widgets."""
|
||||
193
BEdit/src/bedit/gui/editors/text_definition.py
Normal file
193
BEdit/src/bedit/gui/editors/text_definition.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from copy import deepcopy
|
||||
from uuid import uuid4
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import 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 equations, 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.definitionSplitter.setSizes([300, 300])
|
||||
self.ui.equationsEdit.textChanged.connect(self._mark_modified)
|
||||
self.ui.portsTable.cellChanged.connect(self._mark_modified)
|
||||
self.ui.parametersTable.cellChanged.connect(self._mark_modified)
|
||||
self.ui.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 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)
|
||||
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 {}),
|
||||
)
|
||||
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,
|
||||
equations: str,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
parameters: list[Parameter],
|
||||
) -> None:
|
||||
self._loading = True
|
||||
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._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 _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
|
||||
combo = QComboBox(self)
|
||||
for label, value in values:
|
||||
combo.addItem(label, value)
|
||||
combo.setCurrentIndex(max(0, combo.findData(current)))
|
||||
combo.currentIndexChanged.connect(self._mark_modified)
|
||||
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),
|
||||
)
|
||||
|
||||
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._mark_modified()
|
||||
|
||||
def remove_port(self) -> None:
|
||||
row = self.ui.portsTable.currentRow()
|
||||
if row >= 0:
|
||||
self.ui.portsTable.removeRow(row)
|
||||
self._mark_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._mark_modified()
|
||||
|
||||
def remove_parameter(self) -> None:
|
||||
row = self.ui.parametersTable.currentRow()
|
||||
if row >= 0:
|
||||
self.ui.parametersTable.removeRow(row)
|
||||
self._mark_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
|
||||
)
|
||||
@@ -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,
|
||||
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||
QMenuBar, QSizePolicy, QSpacerItem, QSplitter,
|
||||
QStackedWidget, QToolBar, QToolButton, QTreeView,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
from bedit.gui.editors.text_definition import TextDefinitionEditor
|
||||
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||
from . import resources_rc
|
||||
|
||||
@@ -60,6 +61,8 @@ class Ui_MainWindow(object):
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon5)
|
||||
self.actionReloadLibraries = QAction(MainWindow)
|
||||
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
|
||||
self.actionSave = QAction(MainWindow)
|
||||
self.actionSave.setObjectName(u"actionSave")
|
||||
icon6 = QIcon()
|
||||
@@ -217,12 +220,6 @@ 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()
|
||||
@@ -291,18 +288,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;")
|
||||
@@ -364,6 +360,7 @@ class Ui_MainWindow(object):
|
||||
self.menubar.addAction(self.menuHelp.menuAction())
|
||||
self.menuFile.addAction(self.actionNew)
|
||||
self.menuFile.addAction(self.actionOpen)
|
||||
self.menuFile.addAction(self.actionReloadLibraries)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionSave)
|
||||
self.menuFile.addAction(self.actionSaveAs)
|
||||
@@ -439,6 +436,13 @@ 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"F5", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
@@ -505,7 +509,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)
|
||||
@@ -524,7 +527,6 @@ 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.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
||||
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
|
||||
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
||||
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
|
||||
|
||||
163
BEdit/src/bedit/gui/generated/ui_text_definition_editor.py
Normal file
163
BEdit/src/bedit/gui/generated/ui_text_definition_editor.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- 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, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSplitter, QTableWidget, QTableWidgetItem,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
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.equationsGroup = QGroupBox(self.columnSplitter)
|
||||
self.equationsGroup.setObjectName(u"equationsGroup")
|
||||
self.equationsLayout = QVBoxLayout(self.equationsGroup)
|
||||
self.equationsLayout.setObjectName(u"equationsLayout")
|
||||
self.equationsEdit = QPlainTextEdit(self.equationsGroup)
|
||||
self.equationsEdit.setObjectName(u"equationsEdit")
|
||||
self.equationsEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
|
||||
self.equationsLayout.addWidget(self.equationsEdit)
|
||||
|
||||
self.columnSplitter.addWidget(self.equationsGroup)
|
||||
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() < 3):
|
||||
self.portsTable.setColumnCount(3)
|
||||
__qtablewidgetitem = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
||||
__qtablewidgetitem1 = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
||||
__qtablewidgetitem2 = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
|
||||
self.portsTable.setObjectName(u"portsTable")
|
||||
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.portsTable.setColumnCount(3)
|
||||
|
||||
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)
|
||||
__qtablewidgetitem3 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem3)
|
||||
__qtablewidgetitem4 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem4)
|
||||
__qtablewidgetitem5 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem5)
|
||||
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.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
|
||||
self.equationsEdit.setPlaceholderText(QCoreApplication.translate("TextDefinitionEditor", u"Enter equations here\u2026", None))
|
||||
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
|
||||
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
|
||||
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
||||
___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))
|
||||
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))
|
||||
___qtablewidgetitem3 = self.parametersTable.horizontalHeaderItem(0)
|
||||
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
||||
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(1)
|
||||
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
||||
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(2)
|
||||
___qtablewidgetitem5.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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user