Plot settings and version

This commit is contained in:
2026-08-02 11:54:38 +02:00
parent 51d01ad208
commit feba2c9798
10 changed files with 469 additions and 18 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import sys
import argparse
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QApplication, QMessageBox
from bedit_gui.controllers.clipboard_controller import ClipboardController, DocumentTreeClipboardHandler, TextClipboardHandler
from bedit_gui.controllers.document_controller import DocumentController
@@ -19,6 +19,7 @@ from bedit_gui.documents import Document
from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.services.clipboard import ClipboardService
from bedit_gui.views.main_window import MainWindow
from bedit_gui.versions import BEDIT_VERSION
def parse_arguments():
parser = argparse.ArgumentParser(exit_on_error=False)
@@ -26,6 +27,7 @@ def parse_arguments():
parser.add_argument(
"-f", "--file", type=str, help="Path to a file to open", default=None, required=False
)
parser.add_argument("--version", action="version", version=f"BEdit {BEDIT_VERSION}")
return parser.parse_args()
@@ -40,10 +42,13 @@ def main() -> int:
app.setOrganizationName("BEdit")
app.setApplicationName("BEdit")
app.setApplicationVersion(BEDIT_VERSION)
settings = ApplicationSettings()
document = Document(app)
window = MainWindow()
window.ui.actionAbout.triggered.connect(lambda: QMessageBox.about(window, "About BEdit", f"BEdit {BEDIT_VERSION}"))
window.ui.actionAbout_QT.triggered.connect(app.aboutQt)
LogController(window, settings.log_level)
DocumentController(document, window)

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
from collections.abc import Callable
from copy import deepcopy
from PySide6.QtGui import QUndoCommand
from bedit_gui.simulation_models import SimulationPlotTab, SimulationRoot
from bedit_gui.simulation_models import SimulationPlotSettings, SimulationPlotTab, SimulationRoot
ChangedCallback = Callable[[int], None]
@@ -95,3 +96,21 @@ class ChangeSimulationPlotXAxisCommand(QUndoCommand):
def undo(self) -> None:
self.root.plot_tabs[self.index].x_axis = self.old_x_axis
self.changed(self.index)
class ChangeSimulationPlotSettingsCommand(QUndoCommand):
def __init__(self, root: SimulationRoot, index: int, settings: SimulationPlotSettings, changed: ChangedCallback) -> None:
super().__init__("Change plot settings")
self.root = root
self.index = index
self.old_settings = deepcopy(root.plot_tabs[index].settings)
self.settings = deepcopy(settings)
self.changed = changed
def redo(self) -> None:
self.root.plot_tabs[self.index].settings = deepcopy(self.settings)
self.changed(self.index)
def undo(self) -> None:
self.root.plot_tabs[self.index].settings = deepcopy(self.old_settings)
self.changed(self.index)

View File

@@ -2,13 +2,14 @@ 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 PySide6.QtWidgets import QDialog, QInputDialog, QMenu, QTreeWidgetItem, QWidget
from bedit_gui.commands.simulation_plot_commands import AddSimulationPlotTabCommand, ChangeSimulationPlotSignalsCommand, ChangeSimulationPlotXAxisCommand, RemoveSimulationPlotTabCommand, RenameSimulationPlotTabCommand
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__)
@@ -109,8 +110,10 @@ class SimulationPlotController(QObject):
while tabs.count():
tabs.removeTab(0)
if root is not None:
for tab in root.plot_tabs:
tabs.addTab(SimulationPlotWidget(tabs), tab.name)
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:
@@ -222,6 +225,16 @@ class SimulationPlotController(QObject):
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:
@@ -242,7 +255,7 @@ class SimulationPlotController(QObject):
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)
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")

View File

@@ -12,10 +12,12 @@ from bedit_gui.controllers.simulation_run_controller import SimulationRunControl
from bedit_gui.simulation_models import CompiledModel
from bedit_gui.services.application_settings import SimulationApplicationSettings
from bedit_gui.views.simulation_window import SimulationWindow
from bedit_gui.versions import BESIM_VERSION
def parse_arguments(arguments: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Open and run BEdit simulations")
parser.add_argument("--version", action="version", version=f"BEsim {BESIM_VERSION}")
parser.add_argument("file", nargs="?", help="BEdit (.beb/.json) or simulation (.bes/.json) file")
parser.add_argument("-f", "--file", dest="file_option", help=argparse.SUPPRESS)
parser.add_argument("--handoff", action="store_true", help=argparse.SUPPRESS)
@@ -38,8 +40,11 @@ def main(arguments: list[str] | None = None) -> int:
app.setOrganizationName("BEsim")
app.setApplicationName("BEsim")
app.setApplicationVersion(BESIM_VERSION)
window = SimulationWindow()
window.ui.actionAbout.triggered.connect(lambda: QMessageBox.about(window, "About BEsim", f"BEsim {BESIM_VERSION}"))
window.ui.actionAbout_QT.triggered.connect(app.aboutQt)
controller = SimulationFileController(window)
settings = SimulationApplicationSettings()

View File

@@ -25,21 +25,89 @@ class CompiledModel:
return {"model_name": self.model_name, "executable": self.executable, "working_directory": self.working_directory, "output": self.output, "errors": self.errors}
@dataclass
class SimulationTraceSettings:
visible: bool = True
label: str = ""
color: str = ""
line_style: str = "-"
line_width: float = 1.5
marker: str = ""
marker_size: float = 6.0
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationTraceSettings:
return cls(visible=bool(data.get("visible", True)), label=str(data.get("label", "")), color=str(data.get("color", "")), line_style=str(data.get("line_style", "-")), line_width=float(data.get("line_width", 1.5)), marker=str(data.get("marker", "")), marker_size=float(data.get("marker_size", 6.0)))
def to_data(self) -> dict[str, Any]:
return {"visible": self.visible, "label": self.label, "color": self.color, "line_style": self.line_style, "line_width": self.line_width, "marker": self.marker, "marker_size": self.marker_size}
@dataclass
class SimulationPlotSettings:
title: str = ""
x_label: str = ""
y_label: str = ""
x_scale: str = "linear"
y_scale: str = "linear"
x_auto: bool = True
y_auto: bool = True
x_min: float = 0.0
x_max: float = 1.0
y_min: float = 0.0
y_max: float = 1.0
grid_visible: bool = True
grid_axis: str = "both"
grid_style: str = "-"
grid_alpha: float = 0.5
legend_visible: bool = True
legend_location: str = "best"
traces: dict[str, SimulationTraceSettings] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationPlotSettings:
raw_traces = data.get("traces", {})
if not isinstance(raw_traces, Mapping):
raise TypeError("plot trace settings must be a mapping")
return cls(
title=str(data.get("title", "")), x_label=str(data.get("x_label", "")), y_label=str(data.get("y_label", "")),
x_scale=str(data.get("x_scale", "linear")), y_scale=str(data.get("y_scale", "linear")),
x_auto=bool(data.get("x_auto", True)), y_auto=bool(data.get("y_auto", True)),
x_min=float(data.get("x_min", 0.0)), x_max=float(data.get("x_max", 1.0)), y_min=float(data.get("y_min", 0.0)), y_max=float(data.get("y_max", 1.0)),
grid_visible=bool(data.get("grid_visible", True)), grid_axis=str(data.get("grid_axis", "both")), grid_style=str(data.get("grid_style", "-")), grid_alpha=float(data.get("grid_alpha", 0.5)),
legend_visible=bool(data.get("legend_visible", True)), legend_location=str(data.get("legend_location", "best")),
traces={str(signal): SimulationTraceSettings.from_data(trace) for signal, trace in raw_traces.items() if isinstance(trace, Mapping)},
)
def to_data(self) -> dict[str, Any]:
return {
"title": self.title, "x_label": self.x_label, "y_label": self.y_label, "x_scale": self.x_scale, "y_scale": self.y_scale,
"x_auto": self.x_auto, "y_auto": self.y_auto, "x_min": self.x_min, "x_max": self.x_max, "y_min": self.y_min, "y_max": self.y_max,
"grid_visible": self.grid_visible, "grid_axis": self.grid_axis, "grid_style": self.grid_style, "grid_alpha": self.grid_alpha,
"legend_visible": self.legend_visible, "legend_location": self.legend_location,
"traces": {signal: trace.to_data() for signal, trace in self.traces.items()},
}
@dataclass
class SimulationPlotTab:
name: str
signals: list[str] = field(default_factory=list)
x_axis: str | None = None
settings: SimulationPlotSettings = field(default_factory=SimulationPlotSettings)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> SimulationPlotTab:
raw_signals = data.get("signals", [])
if not isinstance(raw_signals, list):
raise TypeError("plot tab signals must be a list")
return cls(name=str(data.get("name", "Plot")), signals=[str(signal) for signal in raw_signals], x_axis=str(data["x_axis"]) if data.get("x_axis") is not None else None)
raw_settings = data.get("settings", {})
if not isinstance(raw_settings, Mapping):
raise TypeError("plot tab settings must be a mapping")
return cls(name=str(data.get("name", "Plot")), signals=[str(signal) for signal in raw_signals], x_axis=str(data["x_axis"]) if data.get("x_axis") is not None else None, settings=SimulationPlotSettings.from_data(raw_settings))
def to_data(self) -> dict[str, Any]:
return {"name": self.name, "signals": self.signals, "x_axis": self.x_axis}
return {"name": self.name, "signals": self.signals, "x_axis": self.x_axis, "settings": self.settings.to_data()}
@dataclass

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlotSettingsDialog</class>
<widget class="QDialog" name="PlotSettingsDialog">
<property name="geometry"><rect><x>0</x><y>0</y><width>520</width><height>430</height></rect></property>
<property name="windowTitle"><string>Plot Settings</string></property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex"><number>0</number></property>
<widget class="QWidget" name="generalTab">
<attribute name="title"><string>General</string></attribute>
<layout class="QVBoxLayout" name="generalLayout">
<item><widget class="QGroupBox" name="labelsGroup"><property name="title"><string>Labels</string></property><layout class="QFormLayout" name="labelsForm">
<item row="0" column="0"><widget class="QLabel" name="titleLabel"><property name="text"><string>Title:</string></property></widget></item>
<item row="0" column="1"><widget class="QLineEdit" name="titleEdit"><property name="placeholderText"><string>Optional plot title</string></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="xLabel"><property name="text"><string>X-axis label:</string></property></widget></item>
<item row="1" column="1"><widget class="QLineEdit" name="xLabelEdit"><property name="placeholderText"><string>Automatic</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="yLabel"><property name="text"><string>Y-axis label:</string></property></widget></item>
<item row="2" column="1"><widget class="QLineEdit" name="yLabelEdit"><property name="placeholderText"><string>Optional</string></property></widget></item>
</layout></widget></item>
<item><spacer name="generalSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>
</layout>
</widget>
<widget class="QWidget" name="axesTab">
<attribute name="title"><string>Axes</string></attribute>
<layout class="QVBoxLayout" name="axesLayout">
<item><widget class="QGroupBox" name="xAxisGroup"><property name="title"><string>X axis</string></property><layout class="QGridLayout" name="xAxisLayout">
<item row="0" column="0"><widget class="QLabel" name="xScaleLabel"><property name="text"><string>Scale:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="xScaleCombo"><item><property name="text"><string>Linear</string></property></item><item><property name="text"><string>Logarithmic</string></property></item></widget></item>
<item row="1" column="0" colspan="2"><widget class="QCheckBox" name="xAutoCheck"><property name="text"><string>Automatic limits</string></property><property name="checked"><bool>true</bool></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="xMinLabel"><property name="text"><string>Minimum:</string></property></widget></item>
<item row="2" column="1"><widget class="QDoubleSpinBox" name="xMinSpin"><property name="decimals"><number>8</number></property></widget></item>
<item row="2" column="2"><widget class="QLabel" name="xMaxLabel"><property name="text"><string>Maximum:</string></property></widget></item>
<item row="2" column="3"><widget class="QDoubleSpinBox" name="xMaxSpin"><property name="decimals"><number>8</number></property><property name="value"><double>1.000000000000000</double></property></widget></item>
</layout></widget></item>
<item><widget class="QGroupBox" name="yAxisGroup"><property name="title"><string>Y axis</string></property><layout class="QGridLayout" name="yAxisLayout">
<item row="0" column="0"><widget class="QLabel" name="yScaleLabel"><property name="text"><string>Scale:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="yScaleCombo"><item><property name="text"><string>Linear</string></property></item><item><property name="text"><string>Logarithmic</string></property></item></widget></item>
<item row="1" column="0" colspan="2"><widget class="QCheckBox" name="yAutoCheck"><property name="text"><string>Automatic limits</string></property><property name="checked"><bool>true</bool></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="yMinLabel"><property name="text"><string>Minimum:</string></property></widget></item>
<item row="2" column="1"><widget class="QDoubleSpinBox" name="yMinSpin"><property name="decimals"><number>8</number></property></widget></item>
<item row="2" column="2"><widget class="QLabel" name="yMaxLabel"><property name="text"><string>Maximum:</string></property></widget></item>
<item row="2" column="3"><widget class="QDoubleSpinBox" name="yMaxSpin"><property name="decimals"><number>8</number></property><property name="value"><double>1.000000000000000</double></property></widget></item>
</layout></widget></item>
<item><spacer name="axesSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>20</height></size></property></spacer></item>
</layout>
</widget>
<widget class="QWidget" name="tracesTab">
<attribute name="title"><string>Traces</string></attribute>
<layout class="QVBoxLayout" name="tracesLayout">
<item><layout class="QFormLayout" name="traceSelectionForm">
<item row="0" column="0"><widget class="QLabel" name="traceLabel"><property name="text"><string>Trace:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="traceCombo"/></item>
</layout></item>
<item><widget class="QGroupBox" name="traceGroup"><property name="title"><string>Appearance</string></property><layout class="QFormLayout" name="traceForm">
<item row="0" column="0" colspan="2"><widget class="QCheckBox" name="traceVisibleCheck"><property name="text"><string>Visible</string></property><property name="checked"><bool>true</bool></property></widget></item>
<item row="1" column="0"><widget class="QLabel" name="traceLegendLabel"><property name="text"><string>Legend label:</string></property></widget></item>
<item row="1" column="1"><widget class="QLineEdit" name="traceLegendEdit"><property name="placeholderText"><string>Signal name</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="traceColorLabel"><property name="text"><string>Line color:</string></property></widget></item>
<item row="2" column="1"><layout class="QHBoxLayout" name="traceColorLayout"><item><widget class="QPushButton" name="traceColorButton"><property name="text"><string>Automatic</string></property></widget></item><item><widget class="QPushButton" name="traceColorResetButton"><property name="text"><string>Reset</string></property></widget></item></layout></item>
<item row="3" column="0"><widget class="QLabel" name="traceLineStyleLabel"><property name="text"><string>Line style:</string></property></widget></item>
<item row="3" column="1"><widget class="QComboBox" name="traceLineStyleCombo"><item><property name="text"><string>Solid</string></property></item><item><property name="text"><string>Dashed</string></property></item><item><property name="text"><string>Dotted</string></property></item><item><property name="text"><string>Dash-dot</string></property></item><item><property name="text"><string>No line</string></property></item></widget></item>
<item row="4" column="0"><widget class="QLabel" name="traceLineWidthLabel"><property name="text"><string>Line width:</string></property></widget></item>
<item row="4" column="1"><widget class="QDoubleSpinBox" name="traceLineWidthSpin"><property name="minimum"><double>0.100000000000000</double></property><property name="maximum"><double>20.000000000000000</double></property><property name="singleStep"><double>0.250000000000000</double></property><property name="value"><double>1.500000000000000</double></property></widget></item>
<item row="5" column="0"><widget class="QLabel" name="traceMarkerLabel"><property name="text"><string>Marker:</string></property></widget></item>
<item row="5" column="1"><widget class="QComboBox" name="traceMarkerCombo"/></item>
<item row="6" column="0"><widget class="QLabel" name="traceMarkerSizeLabel"><property name="text"><string>Marker size:</string></property></widget></item>
<item row="6" column="1"><widget class="QDoubleSpinBox" name="traceMarkerSizeSpin"><property name="minimum"><double>0.100000000000000</double></property><property name="maximum"><double>50.000000000000000</double></property><property name="singleStep"><double>0.500000000000000</double></property><property name="value"><double>6.000000000000000</double></property></widget></item>
</layout></widget></item>
<item><spacer name="tracesSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>20</height></size></property></spacer></item>
</layout>
</widget>
<widget class="QWidget" name="appearanceTab">
<attribute name="title"><string>Grid &amp; Legend</string></attribute>
<layout class="QVBoxLayout" name="appearanceLayout">
<item><widget class="QGroupBox" name="gridGroup"><property name="title"><string>Grid</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><layout class="QFormLayout" name="gridForm">
<item row="0" column="0"><widget class="QLabel" name="gridAxisLabel"><property name="text"><string>Grid lines:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="gridAxisCombo"><item><property name="text"><string>Both axes</string></property></item><item><property name="text"><string>X axis only</string></property></item><item><property name="text"><string>Y axis only</string></property></item></widget></item>
<item row="1" column="0"><widget class="QLabel" name="gridStyleLabel"><property name="text"><string>Line style:</string></property></widget></item>
<item row="1" column="1"><widget class="QComboBox" name="gridStyleCombo"><item><property name="text"><string>Solid</string></property></item><item><property name="text"><string>Dashed</string></property></item><item><property name="text"><string>Dotted</string></property></item><item><property name="text"><string>Dash-dot</string></property></item></widget></item>
<item row="2" column="0"><widget class="QLabel" name="gridOpacityLabel"><property name="text"><string>Opacity:</string></property></widget></item>
<item row="2" column="1"><widget class="QSlider" name="gridOpacitySlider"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="maximum"><number>100</number></property><property name="value"><number>50</number></property></widget></item>
</layout></widget></item>
<item><widget class="QGroupBox" name="legendGroup"><property name="title"><string>Legend</string></property><property name="checkable"><bool>true</bool></property><property name="checked"><bool>true</bool></property><layout class="QFormLayout" name="legendForm">
<item row="0" column="0"><widget class="QLabel" name="legendLocationLabel"><property name="text"><string>Position:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="legendLocationCombo"/></item>
</layout></widget></item>
<item><spacer name="appearanceSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>30</height></size></property></spacer></item>
</layout>
</widget>
</widget>
</item>
<item><widget class="QDialogButtonBox" name="buttonBox"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="standardButtons"><set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set></property></widget></item>
</layout>
</widget>
<resources/>
<connections>
<connection><sender>buttonBox</sender><signal>accepted()</signal><receiver>PlotSettingsDialog</receiver><slot>accept()</slot><hints/></connection>
<connection><sender>buttonBox</sender><signal>rejected()</signal><receiver>PlotSettingsDialog</receiver><slot>reject()</slot><hints/></connection>
</connections>
</ui>

View File

@@ -0,0 +1,4 @@
"""Independent versions for the BEdit desktop applications."""
BEDIT_VERSION = "0.2.0"
BESIM_VERSION = "0.1.0"

View File

@@ -0,0 +1,139 @@
from __future__ import annotations
from copy import deepcopy
from PySide6.QtGui import QColor
from PySide6.QtWidgets import QColorDialog, QDialog, QMessageBox, QWidget
from bedit_gui.simulation_models import SimulationPlotSettings, SimulationTraceSettings
from bedit_gui.ui.generated.ui_plot_settings import Ui_PlotSettingsDialog
class PlotSettingsDialog(QDialog):
def __init__(self, settings: SimulationPlotSettings, signals: list[str], parent: QWidget | None = None) -> None:
super().__init__(parent)
self.ui = Ui_PlotSettingsDialog()
self.ui.setupUi(self)
self._trace_settings = deepcopy(settings.traces)
self._current_trace: str | None = None
self._trace_color = ""
self._configure_controls()
self._load(settings)
self.ui.traceCombo.addItems(signals)
self.ui.traceCombo.currentIndexChanged.connect(self._trace_changed)
self.ui.traceColorButton.clicked.connect(self._choose_trace_color)
self.ui.traceColorResetButton.clicked.connect(self._reset_trace_color)
self.ui.xAutoCheck.toggled.connect(self._update_limit_controls)
self.ui.yAutoCheck.toggled.connect(self._update_limit_controls)
self._update_limit_controls()
self._trace_changed(self.ui.traceCombo.currentIndex())
def settings(self) -> SimulationPlotSettings:
self._store_trace()
return SimulationPlotSettings(
title=self.ui.titleEdit.text().strip(), x_label=self.ui.xLabelEdit.text().strip(), y_label=self.ui.yLabelEdit.text().strip(),
x_scale=self.ui.xScaleCombo.currentData(), y_scale=self.ui.yScaleCombo.currentData(),
x_auto=self.ui.xAutoCheck.isChecked(), y_auto=self.ui.yAutoCheck.isChecked(),
x_min=self.ui.xMinSpin.value(), x_max=self.ui.xMaxSpin.value(), y_min=self.ui.yMinSpin.value(), y_max=self.ui.yMaxSpin.value(),
grid_visible=self.ui.gridGroup.isChecked(), grid_axis=self.ui.gridAxisCombo.currentData(), grid_style=self.ui.gridStyleCombo.currentData(), grid_alpha=self.ui.gridOpacitySlider.value() / 100.0,
legend_visible=self.ui.legendGroup.isChecked(), legend_location=self.ui.legendLocationCombo.currentData(),
traces=deepcopy(self._trace_settings),
)
def accept(self) -> None:
settings = self.settings()
if not settings.x_auto and settings.x_min >= settings.x_max:
QMessageBox.warning(self, "Invalid x-axis limits", "The x-axis maximum must be greater than its minimum.")
return
if not settings.y_auto and settings.y_min >= settings.y_max:
QMessageBox.warning(self, "Invalid y-axis limits", "The y-axis maximum must be greater than its minimum.")
return
if settings.x_scale == "log" and not settings.x_auto and settings.x_min <= 0:
QMessageBox.warning(self, "Invalid x-axis limits", "A logarithmic x axis requires a positive minimum.")
return
if settings.y_scale == "log" and not settings.y_auto and settings.y_min <= 0:
QMessageBox.warning(self, "Invalid y-axis limits", "A logarithmic y axis requires a positive minimum.")
return
super().accept()
def _configure_controls(self) -> None:
for combo in (self.ui.xScaleCombo, self.ui.yScaleCombo):
combo.setItemData(0, "linear")
combo.setItemData(1, "log")
for label, value in (("Both axes", "both"), ("X axis only", "x"), ("Y axis only", "y")):
self.ui.gridAxisCombo.setItemData(self.ui.gridAxisCombo.findText(label), value)
for index, value in enumerate(("-", "--", ":", "-.")):
self.ui.gridStyleCombo.setItemData(index, value)
self.ui.traceLineStyleCombo.setItemData(index, value)
self.ui.traceLineStyleCombo.setItemData(4, "None")
for label, value in (("None", ""), ("Point", "."), ("Circle", "o"), ("Square", "s"), ("Triangle up", "^"), ("Triangle down", "v"), ("Diamond", "D"), ("Plus", "+"), ("Cross", "x"), ("Star", "*")):
self.ui.traceMarkerCombo.addItem(label, value)
for label, value in (("Automatic", "best"), ("Upper right", "upper right"), ("Upper left", "upper left"), ("Lower right", "lower right"), ("Lower left", "lower left"), ("Center right", "center right"), ("Center left", "center left"), ("Upper center", "upper center"), ("Lower center", "lower center"), ("Center", "center")):
self.ui.legendLocationCombo.addItem(label, value)
for spin in (self.ui.xMinSpin, self.ui.xMaxSpin, self.ui.yMinSpin, self.ui.yMaxSpin):
spin.setRange(-1e100, 1e100)
def _load(self, settings: SimulationPlotSettings) -> None:
self.ui.titleEdit.setText(settings.title)
self.ui.xLabelEdit.setText(settings.x_label)
self.ui.yLabelEdit.setText(settings.y_label)
self.ui.xScaleCombo.setCurrentIndex(self.ui.xScaleCombo.findData(settings.x_scale))
self.ui.yScaleCombo.setCurrentIndex(self.ui.yScaleCombo.findData(settings.y_scale))
self.ui.xAutoCheck.setChecked(settings.x_auto)
self.ui.yAutoCheck.setChecked(settings.y_auto)
self.ui.xMinSpin.setValue(settings.x_min)
self.ui.xMaxSpin.setValue(settings.x_max)
self.ui.yMinSpin.setValue(settings.y_min)
self.ui.yMaxSpin.setValue(settings.y_max)
self.ui.gridGroup.setChecked(settings.grid_visible)
self.ui.gridAxisCombo.setCurrentIndex(self.ui.gridAxisCombo.findData(settings.grid_axis))
self.ui.gridStyleCombo.setCurrentIndex(self.ui.gridStyleCombo.findData(settings.grid_style))
self.ui.gridOpacitySlider.setValue(round(settings.grid_alpha * 100))
self.ui.legendGroup.setChecked(settings.legend_visible)
self.ui.legendLocationCombo.setCurrentIndex(self.ui.legendLocationCombo.findData(settings.legend_location))
def _update_limit_controls(self) -> None:
for widget in (self.ui.xMinLabel, self.ui.xMinSpin, self.ui.xMaxLabel, self.ui.xMaxSpin):
widget.setEnabled(not self.ui.xAutoCheck.isChecked())
for widget in (self.ui.yMinLabel, self.ui.yMinSpin, self.ui.yMaxLabel, self.ui.yMaxSpin):
widget.setEnabled(not self.ui.yAutoCheck.isChecked())
def _trace_changed(self, index: int) -> None:
self._store_trace()
self._current_trace = self.ui.traceCombo.itemText(index) if index >= 0 else None
self.ui.traceGroup.setEnabled(self._current_trace is not None)
trace = self._trace_settings.get(self._current_trace, SimulationTraceSettings())
self.ui.traceVisibleCheck.setChecked(trace.visible)
self.ui.traceLegendEdit.setText(trace.label)
self.ui.traceLineStyleCombo.setCurrentIndex(self.ui.traceLineStyleCombo.findData(trace.line_style))
self.ui.traceLineWidthSpin.setValue(trace.line_width)
self.ui.traceMarkerCombo.setCurrentIndex(self.ui.traceMarkerCombo.findData(trace.marker))
self.ui.traceMarkerSizeSpin.setValue(trace.marker_size)
self._set_trace_color(trace.color)
def _store_trace(self) -> None:
if self._current_trace is None:
return
trace = SimulationTraceSettings(
visible=self.ui.traceVisibleCheck.isChecked(), label=self.ui.traceLegendEdit.text().strip(), color=self._trace_color,
line_style=self.ui.traceLineStyleCombo.currentData(), line_width=self.ui.traceLineWidthSpin.value(),
marker=self.ui.traceMarkerCombo.currentData(), marker_size=self.ui.traceMarkerSizeSpin.value(),
)
if trace == SimulationTraceSettings():
self._trace_settings.pop(self._current_trace, None)
else:
self._trace_settings[self._current_trace] = trace
def _choose_trace_color(self) -> None:
initial = QColor(self._trace_color) if self._trace_color else QColor("black")
color = QColorDialog.getColor(initial, self, "Select trace color")
if color.isValid():
self._set_trace_color(color.name(QColor.NameFormat.HexRgb))
def _reset_trace_color(self) -> None:
self._set_trace_color("")
def _set_trace_color(self, color: str) -> None:
self._trace_color = color
self.ui.traceColorButton.setText(color or "Automatic")
self.ui.traceColorButton.setStyleSheet(f"background-color: {color};" if color else "")

View File

@@ -2,26 +2,41 @@ from __future__ import annotations
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT
from matplotlib.figure import Figure
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QVBoxLayout, QWidget
from bedit_gui.simulation_models import SimulationPlotSettings
from bedit_simulation import SimulationResult
class SimulationPlotWidget(QWidget):
settings_requested = Signal()
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.figure = Figure(layout="constrained")
self.canvas = FigureCanvasQTAgg(self.figure)
self.toolbar = NavigationToolbar2QT(self.canvas, self)
for action in self.toolbar.actions():
if action.text() in ("Customize", "Subplots"):
self.toolbar.removeAction(action)
self.toolbar.addSeparator()
self.settings_action = self.toolbar.addAction("Plot settings…")
self.settings_action.setToolTip("Edit plot title, axes, traces, grid, and legend")
self.settings_action.triggered.connect(self.settings_requested)
self.axes = self.figure.add_subplot()
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
def set_plot(self, results: list[SimulationResult], signals: list[str], x_axis: str | None = None) -> None:
def set_plot(self, results: list[SimulationResult], signals: list[str], x_axis: str | None = None, settings: SimulationPlotSettings | None = None) -> None:
settings = settings or SimulationPlotSettings()
self.axes.clear()
for signal in signals:
trace = settings.traces.get(signal)
if trace is not None and not trace.visible:
continue
x_values: list[float] = []
values: list[float] = []
sample_offset = 0
@@ -39,9 +54,20 @@ class SimulationPlotWidget(QWidget):
values.extend(result_values)
sample_offset += len(result_values)
if values:
self.axes.plot(x_values, values, label=signal)
self.axes.set_xlabel(x_axis or "Time")
self.axes.grid(True)
if self.axes.lines:
self.axes.legend()
options = {"label": trace.label or signal, "linestyle": trace.line_style, "linewidth": trace.line_width, "marker": trace.marker or None, "markersize": trace.marker_size} if trace is not None else {"label": signal}
if trace is not None and trace.color:
options["color"] = trace.color
self.axes.plot(x_values, values, **options)
self.axes.set_title(settings.title)
self.axes.set_xlabel(settings.x_label or x_axis or "Time")
self.axes.set_ylabel(settings.y_label)
self.axes.set_xscale(settings.x_scale)
self.axes.set_yscale(settings.y_scale)
if not settings.x_auto:
self.axes.set_xlim(settings.x_min, settings.x_max)
if not settings.y_auto:
self.axes.set_ylim(settings.y_min, settings.y_max)
self.axes.grid(settings.grid_visible, axis=settings.grid_axis, linestyle=settings.grid_style, alpha=settings.grid_alpha)
if self.axes.lines and settings.legend_visible:
self.axes.legend(loc=settings.legend_location)
self.canvas.draw_idle()