Plot settings and version
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
102
src/bedit_gui/ui/forms/plot_settings.ui
Normal file
102
src/bedit_gui/ui/forms/plot_settings.ui
Normal 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 & 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>
|
||||
4
src/bedit_gui/versions.py
Normal file
4
src/bedit_gui/versions.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Independent versions for the BEdit desktop applications."""
|
||||
|
||||
BEDIT_VERSION = "0.2.0"
|
||||
BESIM_VERSION = "0.1.0"
|
||||
139
src/bedit_gui/views/dialogs/plot_settings_dialog.py
Normal file
139
src/bedit_gui/views/dialogs/plot_settings_dialog.py
Normal 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 "")
|
||||
@@ -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()
|
||||
|
||||
@@ -7909,21 +7909,91 @@
|
||||
"j0.e",
|
||||
"j1.f"
|
||||
],
|
||||
"x_axis": null
|
||||
"x_axis": null,
|
||||
"settings": {
|
||||
"title": "Some title here",
|
||||
"x_label": "",
|
||||
"y_label": "",
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_auto": true,
|
||||
"y_auto": true,
|
||||
"x_min": 0.0,
|
||||
"x_max": 1.0,
|
||||
"y_min": 0.0,
|
||||
"y_max": 1.0,
|
||||
"grid_visible": true,
|
||||
"grid_axis": "both",
|
||||
"grid_style": "-",
|
||||
"grid_alpha": 0.5,
|
||||
"legend_visible": true,
|
||||
"legend_location": "best",
|
||||
"traces": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "R power",
|
||||
"signals": [
|
||||
"R.power"
|
||||
],
|
||||
"x_axis": null
|
||||
"x_axis": null,
|
||||
"settings": {
|
||||
"title": "",
|
||||
"x_label": "",
|
||||
"y_label": "",
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_auto": true,
|
||||
"y_auto": true,
|
||||
"x_min": 0.0,
|
||||
"x_max": 1.0,
|
||||
"y_min": 0.0,
|
||||
"y_max": 1.0,
|
||||
"grid_visible": true,
|
||||
"grid_axis": "both",
|
||||
"grid_style": "-",
|
||||
"grid_alpha": 0.5,
|
||||
"legend_visible": true,
|
||||
"legend_location": "best",
|
||||
"traces": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Eh idk?",
|
||||
"signals": [
|
||||
"j1.f"
|
||||
],
|
||||
"x_axis": "j0.e"
|
||||
"x_axis": "j0.e",
|
||||
"settings": {
|
||||
"title": "",
|
||||
"x_label": "",
|
||||
"y_label": "j1.f",
|
||||
"x_scale": "linear",
|
||||
"y_scale": "linear",
|
||||
"x_auto": true,
|
||||
"y_auto": true,
|
||||
"x_min": 0.0,
|
||||
"x_max": 1.0,
|
||||
"y_min": 0.0,
|
||||
"y_max": 1.0,
|
||||
"grid_visible": true,
|
||||
"grid_axis": "both",
|
||||
"grid_style": "-",
|
||||
"grid_alpha": 0.5,
|
||||
"legend_visible": true,
|
||||
"legend_location": "best",
|
||||
"traces": {
|
||||
"j1.f": {
|
||||
"visible": true,
|
||||
"label": "",
|
||||
"color": "#ff0000",
|
||||
"line_style": ":",
|
||||
"line_width": 1.5,
|
||||
"marker": "",
|
||||
"marker_size": 6.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user