273 lines
12 KiB
Python
273 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from PySide6.QtCore import QObject, QPoint, Qt
|
|
from PySide6.QtGui import QUndoStack
|
|
from PySide6.QtWidgets import QDialog, QInputDialog, QMenu, QTreeWidgetItem, QWidget
|
|
|
|
from bedit_gui.commands.simulation_plot_commands import AddSimulationPlotTabCommand, ChangeSimulationPlotSettingsCommand, ChangeSimulationPlotSignalsCommand, ChangeSimulationPlotXAxisCommand, 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.dialogs.plot_settings_dialog import PlotSettingsDialog
|
|
from bedit_gui.views.simulation_window import SimulationWindow
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def _signal_tree_parts(signal: str) -> list[str]:
|
|
if signal.startswith("der(") and signal.endswith(")"):
|
|
parts = signal[4:-1].split(".")
|
|
if len(parts) > 1:
|
|
return [*parts[:-1], f"der({parts[-1]})"]
|
|
return signal.split(".")
|
|
|
|
|
|
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)
|
|
tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
tree.customContextMenuRequested.connect(self._show_signal_menu)
|
|
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 index, tab in enumerate(root.plot_tabs):
|
|
widget = SimulationPlotWidget(tabs)
|
|
widget.settings_requested.connect(lambda checked=False, tab_index=index: self._open_plot_settings(tab_index))
|
|
tabs.addTab(widget, 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_tree_parts(signal)
|
|
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 _show_signal_menu(self, position: QPoint) -> None:
|
|
root = self.files.root
|
|
tree = self.window.ui.simulationTree
|
|
item = tree.itemAt(position)
|
|
signal = item.data(0, Qt.ItemDataRole.UserRole) if item is not None else None
|
|
if root is None or signal is None or not 0 <= self._active_tab < len(root.plot_tabs):
|
|
return
|
|
tab = root.plot_tabs[self._active_tab]
|
|
menu = QMenu(tree)
|
|
use_signal = menu.addAction("Use as x axis")
|
|
use_signal.setEnabled(tab.x_axis != signal)
|
|
use_time = menu.addAction("Use time as x axis")
|
|
use_time.setEnabled(tab.x_axis is not None)
|
|
selected = menu.exec(tree.viewport().mapToGlobal(position))
|
|
if selected is use_signal:
|
|
self.undo_stack.push(ChangeSimulationPlotXAxisCommand(root, self._active_tab, signal, self._selection_changed))
|
|
elif selected is use_time:
|
|
self.undo_stack.push(ChangeSimulationPlotXAxisCommand(root, self._active_tab, None, self._selection_changed))
|
|
|
|
def _open_plot_settings(self, index: int) -> None:
|
|
root = self.files.root
|
|
if root is None or not 0 <= index < len(root.plot_tabs):
|
|
return
|
|
dialog = PlotSettingsDialog(root.plot_tabs[index].settings, root.plot_tabs[index].signals, self.window)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
settings = dialog.settings()
|
|
if settings != root.plot_tabs[index].settings:
|
|
self.undo_stack.push(ChangeSimulationPlotSettingsCommand(root, index, settings, self._selection_changed))
|
|
|
|
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):
|
|
tab = root.plot_tabs[index]
|
|
widget.set_plot(root.results, tab.signals, tab.x_axis, tab.settings)
|
|
|
|
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")
|