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.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \ from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \
QVBoxLayout QVBoxLayout
from PyQt6.QtWidgets import QToolBox, QTabWidget from PyQt6.QtWidgets import QTabWidget
from PyQt6.QtGui import QIcon, QPixmap, QAction, QKeySequence from PyQt6.QtGui import QIcon, QPixmap, QAction, QKeySequence
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
@ -10,8 +10,7 @@ from ..resources import get_resource_path
from .widgets.menubar import MenuBar from .widgets.menubar import MenuBar
from .widgets.toolbar import ToolBar from .widgets.toolbar import ToolBar
from .widgets.metadata_input import MetadataInput from .widgets.metadata_input import MetadataInput
from .widgets.measurement_settings import MeasurementSettings, ScriptSelection from cpdctrl_gui.ui.widgets.settings import MeasurementSettings, AppSettings
from .widgets.app_settings import AppSettings
from .widgets.plot import Plot from .widgets.plot import Plot
from .widgets.device_select import ListChoice from .widgets.device_select import ListChoice
from .widgets.about import MarkdownView from .widgets.about import MarkdownView
@ -47,7 +46,12 @@ class MainWindow(QMainWindow):
super().__init__() super().__init__()
# Window-Settings # Window-Settings
self.setWindowTitle(AppConfig.APP_NAME) 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) central_widget = QWidget(self)
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
@ -69,18 +73,17 @@ class MainWindow(QMainWindow):
central_widget.setLayout(layout) central_widget.setLayout(layout)
# Left: Toolbox # Left: Toolbox
self.w_leftbox = QToolBox(self) self.w_lefttab = QTabWidget(self)
self.w_leftbox.setMinimumWidth(300) self.w_lefttab.setMinimumWidth(300)
layout.addWidget(self.w_leftbox) layout.addWidget(self.w_lefttab)
metadata_init_dict = AppConfig.MEAS_CFG.get_or("metadata", {}) metadata_init_dict = AppConfig.MEAS_CFG.get_or("metadata", {})
# Measurement settings # Measurement settings
self.w_measurement_settings = MeasurementSettings() 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 # Measurement metadata
self.w_metadata = MetadataInput(metadata_init_dict) 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 # Right: Tabs: Script, Plot
self.w_right_tab = QTabWidget() self.w_right_tab = QTabWidget()
layout.addWidget(self.w_right_tab) layout.addWidget(self.w_right_tab)
@ -90,7 +93,7 @@ class MainWindow(QMainWindow):
# LED SCRIPT # LED SCRIPT
self.w_led_script = LedScriptViewer(LedScript(0)) 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.w_measurement_settings.w_led_script.script_changed.connect(self.led_script_load)
self.verbose = True self.verbose = True
@ -237,7 +240,7 @@ class MainWindow(QMainWindow):
self.topbar.enable_button("meas_stop") self.topbar.enable_button("meas_stop")
self.w_plot.clear_data() 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") script = self.w_measurement_settings.get_value("led_script")
led_script = LedScript(script=script) led_script = LedScript(script=script)
flush_after = self.w_measurement_settings.get_value("flush_after") 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") auto_add_metadata = self.w_measurement_settings.get_value("auto_add_metadata")
metadata = self.w_metadata.get_dict() metadata = self.w_metadata.get_dict()
metadata["name"] = measurement_name metadata["name"] = name
metadata["led_script"] = str(script) 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}") 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.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() # data_collector.clear()
self.data_queue = mp.Queue() self.data_queue = mp.Queue()
self.command_queue = mp.Queue() self.command_queue = mp.Queue()
@ -353,6 +359,7 @@ class MainWindow(QMainWindow):
def app_open_about(self) -> None: def app_open_about(self) -> None:
dialog = QDialog() dialog = QDialog()
dialog.setWindowTitle("About cpdctrl-gui")
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept) buttons.accepted.connect(dialog.accept)
dialog.setLayout(QVBoxLayout()) dialog.setLayout(QVBoxLayout())
@ -372,6 +379,7 @@ class MainWindow(QMainWindow):
def app_open_help(self) -> None: def app_open_help(self) -> None:
dialog = QDialog() dialog = QDialog()
dialog.setWindowTitle("Help")
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept) buttons.accepted.connect(dialog.accept)
dialog.setLayout(QVBoxLayout()) dialog.setLayout(QVBoxLayout())
@ -385,9 +393,13 @@ class MainWindow(QMainWindow):
def app_open_settings(self) -> None: def app_open_settings(self) -> None:
dialog = QDialog() dialog = QDialog()
w_settings = AppSettings() dialog.setWindowTitle("Settings")
layout = QVBoxLayout() 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.setLayout(layout)
dialog.exec() dialog.exec()
@ -397,4 +409,10 @@ class MainWindow(QMainWindow):
# save the metadata # save the metadata
metadata = self.w_metadata.get_dict() metadata = self.w_metadata.get_dict()
AppConfig.MEAS_CFG.set("metadata", metadata) 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 os import path
from ...utility.config import AppConfig from cpdctrl_gui.utility.config import AppConfig
from .base import SettingsForm
class DeviceSelection(QGroupBox): class DeviceSelection(QGroupBox):
@ -126,68 +127,40 @@ class MeasurementSettings(QWidget):
self.w_led_script = ScriptSelection() self.w_led_script = ScriptSelection()
self.l_vbox.addWidget(self.w_led_script) self.l_vbox.addWidget(self.w_led_script)
# key-value stuff in a form # key-value stuff in a form
self.l_form = QFormLayout() self.w_form = SettingsForm(AppConfig.MEAS_CFG)
self.l_vbox.addLayout(self.l_form) self.l_vbox.addWidget(self.w_form)
self.ws_form = {} self.w_form.add_form_row("name", "Name", "", QLineEdit(self), "Name of the measurement")
w_box_interval = QDoubleSpinBox(self) w_box_interval = QDoubleSpinBox(self)
w_box_interval.setDecimals(2) w_box_interval.setDecimals(2)
w_box_interval.setMinimum(0.01) w_box_interval.setMinimum(0.01)
w_box_interval.setSingleStep(0.1) 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 = QSpinBox(self)
w_box_max_measurements.setMaximum(2147483647) # max int32 w_box_max_measurements.setMaximum(2147483647) # max int32
w_box_max_measurements.setMinimum(0) # 0 for infinite measurements 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.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._add_form_field("stop_on_script_end", "Stop on Script End", False, QCheckBox(self), "Stop measurement when LED script ends") self.w_form.add_form_row("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("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 = QSpinBox(self)
w_box_flush_after.setMaximum(2147483647) # max int32 w_box_flush_after.setMaximum(2147483647) # max int32
w_box_flush_after.setSingleStep(500) 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.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): def set_value(self, key, value):
if key in self.ws_form: if key in self.w_form:
# set depending on widget type self.w_form.set_value(key, value)
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])}")
elif key.startswith("device_"): elif key.startswith("device_"):
self.w_device_selection.set_value(key, value) self.w_device_selection.set_value(key, value)
else: else:
raise ValueError(f"Unknown key: {key}") raise ValueError(f"Unknown key: {key}")
def get_value(self, key): def get_value(self, key):
if key in self.ws_form: if key in self.w_form:
# get depending on widget type return self.w_form.get_value(key)
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])}")
elif key == "led_script": elif key == "led_script":
return self.w_led_script.get_script() return self.w_led_script.get_script()
else: else: