Refactor settings

This commit is contained in:
CPD 2025-03-12 14:37:02 +01:00
parent 77054a676c
commit deccff8975
6 changed files with 220 additions and 70 deletions

View File

@ -2,7 +2,7 @@
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \
QVBoxLayout
from PyQt6.QtWidgets import QToolBox, QTabWidget
from PyQt6.QtWidgets import QTabWidget
from PyQt6.QtGui import QIcon, QPixmap, QAction, QKeySequence
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
@ -10,8 +10,7 @@ from ..resources import get_resource_path
from .widgets.menubar import MenuBar
from .widgets.toolbar import ToolBar
from .widgets.metadata_input import MetadataInput
from .widgets.measurement_settings import MeasurementSettings, ScriptSelection
from .widgets.app_settings import AppSettings
from cpdctrl_gui.ui.widgets.settings import MeasurementSettings, AppSettings
from .widgets.plot import Plot
from .widgets.device_select import ListChoice
from .widgets.about import MarkdownView
@ -47,7 +46,12 @@ class MainWindow(QMainWindow):
super().__init__()
# Window-Settings
self.setWindowTitle(AppConfig.APP_NAME)
self.setGeometry(100, 100, 800, 600)
x = AppConfig.MAIN_CFG.get_or("geometry_x", 100)
y = AppConfig.MAIN_CFG.get_or("geometry_y", 100)
width = AppConfig.MAIN_CFG.get_or("geometry_width", 1000)
height = AppConfig.MAIN_CFG.get_or("geometry_height", 700)
self.setGeometry(x, y, width, height)
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
@ -69,18 +73,17 @@ class MainWindow(QMainWindow):
central_widget.setLayout(layout)
# Left: Toolbox
self.w_leftbox = QToolBox(self)
self.w_leftbox.setMinimumWidth(300)
layout.addWidget(self.w_leftbox)
self.w_lefttab = QTabWidget(self)
self.w_lefttab.setMinimumWidth(300)
layout.addWidget(self.w_lefttab)
metadata_init_dict = AppConfig.MEAS_CFG.get_or("metadata", {})
# Measurement settings
self.w_measurement_settings = MeasurementSettings()
self.w_leftbox.addItem(self.w_measurement_settings, "Measurement settings")
self.w_lefttab.addTab(self.w_measurement_settings, "Measurement settings")
# Measurement metadata
self.w_metadata = MetadataInput(metadata_init_dict)
self.w_leftbox.addItem(self.w_metadata, "Measurement metadata")
self.w_lefttab.addTab(self.w_metadata, "Measurement metadata")
# Right: Tabs: Script, Plot
self.w_right_tab = QTabWidget()
layout.addWidget(self.w_right_tab)
@ -90,7 +93,7 @@ class MainWindow(QMainWindow):
# LED SCRIPT
self.w_led_script = LedScriptViewer(LedScript(0))
self.w_right_tab.addTab(self.w_led_script, "LED Script")
self.w_lefttab.addTab(self.w_led_script, "LED Script")
self.w_measurement_settings.w_led_script.script_changed.connect(self.led_script_load)
self.verbose = True
@ -237,7 +240,7 @@ class MainWindow(QMainWindow):
self.topbar.enable_button("meas_stop")
self.w_plot.clear_data()
measurement_name = "guitest"
name = self.w_measurement_settings.get_value("name")
script = self.w_measurement_settings.get_value("led_script")
led_script = LedScript(script=script)
flush_after = self.w_measurement_settings.get_value("flush_after")
@ -248,15 +251,18 @@ class MainWindow(QMainWindow):
auto_add_metadata = self.w_measurement_settings.get_value("auto_add_metadata")
metadata = self.w_metadata.get_dict()
metadata["name"] = measurement_name
metadata["name"] = name
metadata["led_script"] = str(script)
log.info(f"Starting measurement with:\n\tinterval = {interval}\n\tflush_after = {flush_after}\n\tuse_buffer = {use_buffer}\n\tmax_measurements = {max_measurements}\n\tstop_on_script_end = {stop_on_script_end}")
self.w_led_script.w_time_left.set_end_time(time.time() + led_script.script["dtsum"][-1])
# the time left estimation might be a little to short, since the actual measurement is started a few lines of
# code later
t_now = time.time()
self.w_led_script.w_time_left.set_start_end_time(t_now, t_now + led_script.script["dtsum"][-1])
self.led_script = LedScript(script=script, auto_update=True, verbose=True)
self.data_collector = DataCollector(metadata=metadata, data_path=AppConfig.MAIN_CFG.get("datadir"), data_name=measurement_name)
self.data_collector = DataCollector(metadata=metadata, data_path=AppConfig.MAIN_CFG.get("dir_cache"), data_name=measurement_name)
# data_collector.clear()
self.data_queue = mp.Queue()
self.command_queue = mp.Queue()
@ -353,6 +359,7 @@ class MainWindow(QMainWindow):
def app_open_about(self) -> None:
dialog = QDialog()
dialog.setWindowTitle("About cpdctrl-gui")
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept)
dialog.setLayout(QVBoxLayout())
@ -372,6 +379,7 @@ class MainWindow(QMainWindow):
def app_open_help(self) -> None:
dialog = QDialog()
dialog.setWindowTitle("Help")
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept)
dialog.setLayout(QVBoxLayout())
@ -385,9 +393,13 @@ class MainWindow(QMainWindow):
def app_open_settings(self) -> None:
dialog = QDialog()
w_settings = AppSettings()
dialog.setWindowTitle("Settings")
layout = QVBoxLayout()
layout.addWidget(w_settings)
w_app_settings = AppSettings()
layout.addWidget(w_app_settings)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept)
layout.addWidget(buttons)
dialog.setLayout(layout)
dialog.exec()
@ -397,4 +409,10 @@ class MainWindow(QMainWindow):
# save the metadata
metadata = self.w_metadata.get_dict()
AppConfig.MEAS_CFG.set("metadata", metadata)
event.accept()
# store the geometry as text values in config
geo = self.geometry()
AppConfig.MAIN_CFG.set("geometry_x", geo.x())
AppConfig.MAIN_CFG.set("geometry_y", geo.y())
AppConfig.MAIN_CFG.set("geometry_width", geo.width())
AppConfig.MAIN_CFG.set("geometry_height", geo.height())

View File

@ -1,9 +0,0 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QFormLayout
from ...utility.config import AppConfig
class AppSettings(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout())
self.l_form = QFormLayout()

View File

@ -0,0 +1,2 @@
from .app_settings import AppSettings
from .measurement_settings import MeasurementSettings

View File

@ -0,0 +1,22 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QFormLayout, QFileDialog, QCheckBox, QSpinBox
from cpdctrl_gui.utility.config import AppConfig
from .base import FileSelection, SettingsForm
class AppSettings(QWidget):
def __init__(self):
super().__init__()
self.setLayout(QVBoxLayout())
self.w_form = SettingsForm(AppConfig.MAIN_CFG)
self.layout().addWidget(self.w_form)
w_cache_dir = FileSelection(filemode=QFileDialog.FileMode.Directory)
self.w_form.add_form_row("dir_cache", "Cache Directory", "~/.cache/cpdctrl", w_cache_dir, "Directory to store temporary data")
self.w_form.add_form_row("led_device_auto_reconnect", "Autoconnect to last LED Controller", True, QCheckBox(), "Automatically connect to the last used LED Controller")
self.w_form.add_form_row("voltage_measurement_device_auto_reconnect", "Autoconnect to last Voltage Measurement Device", True, QCheckBox(), "Automatically connect to the last used Voltage Measurement Device")
w_plot_n = QSpinBox()
w_plot_n.setMinimum(1000)
w_plot_n.setMaximum(200000)
w_plot_n.setSingleStep(1000)
self.w_form.add_form_row("plot_max_data_points", "Max datapoints in the plot", 20000, w_plot_n, "Maximum number of datapoints in the live plot.\nThis value is limited to ensure performance is not degraded in long measurements")

View File

@ -0,0 +1,144 @@
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QWidget, QLabel, QFormLayout, QSpinBox, QDoubleSpinBox, QLineEdit, QHBoxLayout, QPushButton, QFileDialog, QCheckBox
from PyQt6.QtCore import pyqtSignal
from cpdctrl.utility.config_file import ConfigFile
class FileSelection(QWidget):
"""
Widget allowing the user to select a file or directory.
"""
value_changed = pyqtSignal(str)
def __init__(self, init_file="", filemode=QFileDialog.FileMode.AnyFile, parent=None):
super().__init__(parent)
self.setLayout(QHBoxLayout())
self.w_edit = QLineEdit()
self.w_edit.setText(init_file)
self.w_btn = QPushButton()
self.w_btn.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.FolderOpen))
self.layout().addWidget(self.w_edit)
self.layout().addWidget(self.w_btn)
# open file dialog when button is clicked
self.w_btn.clicked.connect(self._open_file_dialog)
# emit value_changed on self when w_edit emits value_changed
self.w_edit.textChanged.connect(self.value_changed)
self.file_mode = filemode
# remove all spacing and padding from the layout
self.layout().setContentsMargins(0, 0, 0, 0)
def setValue(self, value: str):
self.w_edit.setText(value)
def _open_file_dialog(self):
dialog = QFileDialog(self)
# only directories
dialog.setFileMode(self.file_mode)
if dialog.exec():
dirname = dialog.selectedFiles()[0]
self.w_edit.setText(dirname)
class SettingsForm(QWidget):
"""
Form that is connected to a config file instance
"""
def __init__(self, config_file: ConfigFile, parent=None):
super().__init__(parent)
self.setLayout(QFormLayout())
self.ws_form = {}
self.config_file = config_file
def __contains__(self, item):
return item in self.ws_form
def add_form_row(self, key: str, label: str, default_value, widget: QWidget, tooltip: str = None):
"""
Add a row to the form. Uses the value from the config file corresponding to <key> or the default value.
Parameters
----------
key
label: str
Label for the form widget
default_value
widget
Widget to add to the form
tooltip
Returns
-------
"""
if tooltip: widget.setToolTip(tooltip)
value = self.config_file.get_or(key, default_value)
# set the value depending on the type of the widget
if isinstance(widget, QSpinBox) or isinstance(widget, QDoubleSpinBox):
widget.setValue(value)
widget.valueChanged.connect(lambda v: self.value_updated(key, v))
elif isinstance(widget, QCheckBox):
widget.setChecked(value)
widget.stateChanged.connect(lambda v: self.value_updated(key, v))
elif isinstance(widget, FileSelection):
widget.setValue(value)
widget.value_changed.connect(lambda v: self.value_updated(key, v))
else:
raise ValueError(f"Unknown widget type: {type(widget)}")
self.layout().addRow(QLabel(label), widget)
self.ws_form[key] = widget
def value_updated(self, key, value):
"""
Update the value in the config file when it is updated in the widget
Parameters
----------
key
value
"""
self.config_file.set(key, value)
def set_value(self, key, value):
"""
Set the value of the widget with the given key.
""
Parameters
----------
key
value
Returns
-------
"""
if key in self.ws_form:
# set depending on widget type
if isinstance(self.ws_form[key], QSpinBox) or isinstance(self.ws_form[key], QDoubleSpinBox):
self.ws_form[key].setValue(value)
elif isinstance(self.ws_form[key], QCheckBox):
self.ws_form[key].setChecked(value)
elif isinstance(self.ws_form[key], FileSelection):
self.ws_form[key].setValue(value)
else:
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
else:
raise ValueError(f"Unknown key: {key}")
def get_value(self, key):
"""
Get the value of the widget with the given key.
Parameters
----------
key
Returns
-------
"""
if key in self.ws_form:
# get depending on widget type
if isinstance(self.ws_form[key], QSpinBox) or isinstance(self.ws_form[key], QDoubleSpinBox):
return self.ws_form[key].value()
elif isinstance(self.ws_form[key], QCheckBox):
return self.ws_form[key].isChecked()
else:
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
else:
raise ValueError(f"Unknown key: {key}")

View File

@ -4,7 +4,8 @@ from PyQt6.QtWidgets import QFormLayout, QDoubleSpinBox, QCheckBox, QLineEdit, Q
from os import path
from ...utility.config import AppConfig
from cpdctrl_gui.utility.config import AppConfig
from .base import SettingsForm
class DeviceSelection(QGroupBox):
@ -126,68 +127,40 @@ class MeasurementSettings(QWidget):
self.w_led_script = ScriptSelection()
self.l_vbox.addWidget(self.w_led_script)
# key-value stuff in a form
self.l_form = QFormLayout()
self.l_vbox.addLayout(self.l_form)
self.ws_form = {}
self.w_form = SettingsForm(AppConfig.MEAS_CFG)
self.l_vbox.addWidget(self.w_form)
self.w_form.add_form_row("name", "Name", "", QLineEdit(self), "Name of the measurement")
w_box_interval = QDoubleSpinBox(self)
w_box_interval.setDecimals(2)
w_box_interval.setMinimum(0.01)
w_box_interval.setSingleStep(0.1)
self._add_form_field("interval", "Interval [s]", 1.0, w_box_interval, "Amount of seconds to wait between voltage measurements and LED device updates")
self.w_form.add_form_row("interval", "Interval [s]", 1.0, w_box_interval, "Amount of seconds to wait between voltage measurements and LED device updates")
w_box_max_measurements = QSpinBox(self)
w_box_max_measurements.setMaximum(2147483647) # max int32
w_box_max_measurements.setMinimum(0) # 0 for infinite measurements
self._add_form_field("max_measurements", "Max Measurements", 0, w_box_max_measurements, "Number of measurements to take. Set to 0 for infinite measurements")
self._add_form_field("stop_on_script_end", "Stop on Script End", False, QCheckBox(self), "Stop measurement when LED script ends")
self._add_form_field("use_buffer", "Use Buffer", False, QCheckBox(self), "If available, use the voltage device buffer for more accurate measurement timings.\nLeads to a lower accuracy of LED update timings, up to 1*interval")
self.w_form.add_form_row("max_measurements", "Max Measurements", 0, w_box_max_measurements, "Number of measurements to take. Set to 0 for infinite measurements")
self.w_form.add_form_row("stop_on_script_end", "Stop on Script End", False, QCheckBox(self), "Stop measurement when LED script ends")
self.w_form.add_form_row("use_buffer", "Use Buffer", False, QCheckBox(self), "If available, use the voltage device buffer for more accurate measurement timings.\nLeads to a lower accuracy of LED update timings, up to 1*interval")
w_box_flush_after = QSpinBox(self)
w_box_flush_after.setMaximum(2147483647) # max int32
w_box_flush_after.setSingleStep(500)
self._add_form_field("flush_after", "Flush-Data Interval", 0, w_box_flush_after, "Number of measurements to take before writing the data to an intermediate file")
self.w_form.add_form_row("flush_after", "Flush-Data Interval", 0, w_box_flush_after, "Number of measurements to take before writing the data to an intermediate file")
self.w_form.add_form_row("auto_add_metadata", "Auto-add Metadata", True, QCheckBox(self), "Automatically add measurement metadata to the data file.\nThis includes: device names, measurement mode, measurement interval, start and stop times, led script")
self.l_vbox.addStretch(1)
self._add_form_field("auto_add_metadata", "Auto-add Metadata", True, QCheckBox(self), "Automatically add measurement metadata to the data file.\nThis includes: device names, measurement mode, measurement interval, start and stop times, led script")
def _add_form_field(self, key: str, label: str, default_value, widget: QWidget, tooltip: str = None):
if tooltip: widget.setToolTip(tooltip)
value = AppConfig.MEAS_CFG.get_or(key, default_value)
# set the value depending on the type of the widget
if isinstance(widget, QSpinBox) or isinstance(widget, QDoubleSpinBox):
widget.setValue(value)
widget.valueChanged.connect(lambda value: self.value_updated(key, value))
elif isinstance(widget, QCheckBox):
widget.setChecked(value)
widget.stateChanged.connect(lambda value: self.value_updated(key, value))
else:
raise ValueError(f"Unknown widget type: {type(widget)}")
self.l_form.addRow(QLabel(label), widget)
self.ws_form[key] = widget
def value_updated(self, key, value):
AppConfig.MEAS_CFG.set(key, value)
def set_value(self, key, value):
if key in self.ws_form:
# set depending on widget type
if isinstance(self.ws_form[key], QSpinBox) or isinstance(self.ws_form[key], QDoubleSpinBox):
self.ws_form[key].setValue(value)
elif isinstance(self.ws_form[key], QCheckBox):
self.ws_form[key].setChecked(value)
else:
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
if key in self.w_form:
self.w_form.set_value(key, value)
elif key.startswith("device_"):
self.w_device_selection.set_value(key, value)
else:
raise ValueError(f"Unknown key: {key}")
def get_value(self, key):
if key in self.ws_form:
# get depending on widget type
if isinstance(self.ws_form[key], QSpinBox) or isinstance(self.ws_form[key], QDoubleSpinBox):
return self.ws_form[key].value()
elif isinstance(self.ws_form[key], QCheckBox):
return self.ws_form[key].isChecked()
else:
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
if key in self.w_form:
return self.w_form.get_value(key)
elif key == "led_script":
return self.w_led_script.get_script()
else: