Plotting signals in besim

This commit is contained in:
2026-08-02 11:22:28 +02:00
parent 2128fb00b4
commit 756a621ab1
7 changed files with 391 additions and 1 deletions

View File

@@ -0,0 +1,229 @@
from __future__ import annotations
from PySide6.QtCore import QObject, QPoint, Qt
from PySide6.QtGui import QUndoStack
from PySide6.QtWidgets import QInputDialog, QMenu, QTreeWidgetItem, QWidget
from bedit_gui.commands.simulation_plot_commands import AddSimulationPlotTabCommand, ChangeSimulationPlotSignalsCommand, RemoveSimulationPlotTabCommand, RenameSimulationPlotTabCommand
from bedit_gui.controllers.simulation_file_controller import SimulationFileController
from bedit_gui.services.application_logging import get_logger
from bedit_gui.simulation_models import SimulationPlotTab
from bedit_gui.views.simulation_plot_widget import SimulationPlotWidget
from bedit_gui.views.simulation_window import SimulationWindow
logger = get_logger(__name__)
class SimulationPlotController(QObject):
def __init__(self, window: SimulationWindow, files: SimulationFileController) -> None:
super().__init__(window)
self.window = window
self.files = files
self.undo_stack = QUndoStack(self)
self._updating = False
self._active_tab = -1
tabs = window.ui.resultsTabWidget
tree = window.ui.simulationTree
tabs.currentChanged.connect(self._tab_changed)
tabs.tabBar().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
tabs.tabBar().customContextMenuRequested.connect(self._show_tab_menu)
tree.itemChanged.connect(self._signal_changed)
files.runtime_changed.connect(self.load_root)
window.ui.actionUndo.triggered.connect(self.undo)
window.ui.actionRedo.triggered.connect(self.redo)
self.undo_stack.canUndoChanged.connect(window.ui.actionUndo.setEnabled)
self.undo_stack.canRedoChanged.connect(window.ui.actionRedo.setEnabled)
self.undo_stack.undoTextChanged.connect(self._set_undo_text)
self.undo_stack.redoTextChanged.connect(self._set_redo_text)
window.ui.actionUndo.setEnabled(False)
window.ui.actionRedo.setEnabled(False)
self.load_root()
def load_root(self) -> None:
self.undo_stack.clear()
self._active_tab = 0 if self.files.root is not None and self.files.root.plot_tabs else -1
self._rebuild_tabs(self._active_tab)
self._rebuild_tree()
def refresh_results(self) -> None:
self._rebuild_tree()
self._refresh_plots()
def add_tab(self) -> None:
root = self.files.root
if root is None:
return
used_names = {tab.name for tab in root.plot_tabs}
number = 1
while f"Plot {number}" in used_names:
number += 1
index = len(root.plot_tabs)
self.undo_stack.push(AddSimulationPlotTabCommand(root, SimulationPlotTab(f"Plot {number}"), index, self._tabs_changed))
def rename_tab(self, index: int, name: str) -> None:
root = self.files.root
name = name.strip()
if root is None or not 0 <= index < len(root.plot_tabs) or not name or root.plot_tabs[index].name == name:
return
self.undo_stack.push(RenameSimulationPlotTabCommand(root, index, name, self._tabs_changed))
def remove_tab(self, index: int) -> None:
root = self.files.root
if root is None or not 0 <= index < len(root.plot_tabs):
return
self.undo_stack.push(RemoveSimulationPlotTabCommand(root, index, self._tabs_changed))
def undo(self) -> None:
command = self.undo_stack.undoText()
self.undo_stack.undo()
logger.info("Undo: %s", command)
def redo(self) -> None:
command = self.undo_stack.redoText()
self.undo_stack.redo()
logger.info("Redo: %s", command)
def _tabs_changed(self, selected_index: int) -> None:
root = self.files.root
if root is None or not root.plot_tabs:
self._active_tab = -1
else:
self._active_tab = max(0, min(selected_index, len(root.plot_tabs) - 1))
self._rebuild_tabs(self._active_tab)
self._rebuild_tree()
def _selection_changed(self, index: int) -> None:
self._active_tab = index
self._sync_tree_checks()
self._refresh_plot(index)
def _rebuild_tabs(self, selected_index: int) -> None:
tabs = self.window.ui.resultsTabWidget
root = self.files.root
self._updating = True
try:
while tabs.count():
tabs.removeTab(0)
if root is not None:
for tab in root.plot_tabs:
tabs.addTab(SimulationPlotWidget(tabs), tab.name)
tabs.addTab(QWidget(tabs), "+")
tabs.setCurrentIndex(selected_index if selected_index >= 0 else tabs.count() - 1)
finally:
self._updating = False
self._refresh_plots()
def _rebuild_tree(self) -> None:
tree = self.window.ui.simulationTree
signals = self._available_signals()
self._updating = True
try:
tree.clear()
nodes: dict[tuple[str, ...], QTreeWidgetItem] = {}
for signal in signals:
parent = tree.invisibleRootItem()
parts = signal.split(".")
for depth, part in enumerate(parts, start=1):
path = tuple(parts[:depth])
item = nodes.get(path)
if item is None:
item = QTreeWidgetItem(parent, [part])
nodes[path] = item
parent = item
item.setData(0, Qt.ItemDataRole.UserRole, signal)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
self._sync_tree_checks()
finally:
self._updating = False
def _sync_tree_checks(self) -> None:
tree = self.window.ui.simulationTree
root = self.files.root
selected = set(root.plot_tabs[self._active_tab].signals) if root is not None and 0 <= self._active_tab < len(root.plot_tabs) else set()
previous = self._updating
self._updating = True
try:
iterator = tree.invisibleRootItem()
pending = [iterator.child(index) for index in range(iterator.childCount())]
while pending:
item = pending.pop()
signal = item.data(0, Qt.ItemDataRole.UserRole)
if signal is not None:
item.setCheckState(0, Qt.CheckState.Checked if signal in selected else Qt.CheckState.Unchecked)
pending.extend(item.child(index) for index in range(item.childCount()))
finally:
self._updating = previous
def _signal_changed(self, item: QTreeWidgetItem, _column: int) -> None:
if self._updating:
return
root = self.files.root
signal = item.data(0, Qt.ItemDataRole.UserRole)
if root is None or signal is None or not 0 <= self._active_tab < len(root.plot_tabs):
return
selected = set(root.plot_tabs[self._active_tab].signals)
if item.checkState(0) == Qt.CheckState.Checked:
selected.add(signal)
else:
selected.discard(signal)
ordered = [available for available in self._available_signals() if available in selected]
if ordered != root.plot_tabs[self._active_tab].signals:
self.undo_stack.push(ChangeSimulationPlotSignalsCommand(root, self._active_tab, ordered, self._selection_changed))
def _tab_changed(self, index: int) -> None:
if self._updating:
return
root = self.files.root
real_tab_count = len(root.plot_tabs) if root is not None else 0
if index == real_tab_count:
self.add_tab()
return
if 0 <= index < real_tab_count:
self._active_tab = index
self._sync_tree_checks()
def _show_tab_menu(self, position: QPoint) -> None:
root = self.files.root
tab_bar = self.window.ui.resultsTabWidget.tabBar()
index = tab_bar.tabAt(position)
if root is None or not 0 <= index < len(root.plot_tabs):
return
menu = QMenu(tab_bar)
rename_action = menu.addAction("Rename")
remove_action = menu.addAction("Remove")
selected = menu.exec(tab_bar.mapToGlobal(position))
if selected is rename_action:
name, accepted = QInputDialog.getText(self.window, "Rename Plot", "Name:", text=root.plot_tabs[index].name)
if accepted:
self.rename_tab(index, name)
elif selected is remove_action:
self.remove_tab(index)
def _available_signals(self) -> list[str]:
root = self.files.root
if root is None:
return []
return sorted({signal for result in root.results for signal in result.data})
def _refresh_plots(self) -> None:
root = self.files.root
if root is None:
return
for index in range(len(root.plot_tabs)):
self._refresh_plot(index)
def _refresh_plot(self, index: int) -> None:
root = self.files.root
if root is None or not 0 <= index < len(root.plot_tabs):
return
widget = self.window.ui.resultsTabWidget.widget(index)
if isinstance(widget, SimulationPlotWidget):
widget.set_plot(root.results, root.plot_tabs[index].signals)
def _set_undo_text(self, command: str) -> None:
self.window.ui.actionUndo.setText(f"Undo {command}" if command else "Undo")
def _set_redo_text(self, command: str) -> None:
self.window.ui.actionRedo.setText(f"Redo {command}" if command else "Redo")

View File

@@ -24,6 +24,7 @@ class SimulationRunController(QObject):
run_completed = Signal(object)
run_cancelled = Signal()
run_failed = Signal(object)
simulation_state_changed = Signal()
def __init__(self, window: SimulationWindow, files: SimulationFileController, session_factory: SessionFactory = _create_session) -> None:
super().__init__(window)
@@ -153,3 +154,4 @@ class SimulationRunController(QObject):
return
self.files.root.current_end_time = self.session.current_end_time
self.files.root.results = list(self.session.results)
self.simulation_state_changed.emit()