From e314f51ae4f6cc754cdc5a74e1ba91d0e368e416 Mon Sep 17 00:00:00 2001 From: jupfi Date: Wed, 13 Mar 2024 10:16:12 +0100 Subject: [PATCH] Implemented input validation. --- .../base_spectrometer_view.py | 2 +- src/nqrduck_spectrometer/settings.py | 116 +++++++++++++----- 2 files changed, 83 insertions(+), 35 deletions(-) diff --git a/src/nqrduck_spectrometer/base_spectrometer_view.py b/src/nqrduck_spectrometer/base_spectrometer_view.py index b527231..5739f5c 100644 --- a/src/nqrduck_spectrometer/base_spectrometer_view.py +++ b/src/nqrduck_spectrometer/base_spectrometer_view.py @@ -53,7 +53,7 @@ class BaseSpectrometerView(ModuleView): setting_label = QLabel(setting.name) setting_label.setMinimumWidth(200) - edit_widget = setting.get_widget() + edit_widget = setting.widget logger.debug("Setting widget: %s", edit_widget) # Add a icon that can be used as a tooltip diff --git a/src/nqrduck_spectrometer/settings.py b/src/nqrduck_spectrometer/settings.py index f36f3ad..968ce07 100644 --- a/src/nqrduck_spectrometer/settings.py +++ b/src/nqrduck_spectrometer/settings.py @@ -1,7 +1,8 @@ import logging import ipaddress -from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, QRegularExpression from PyQt6.QtWidgets import QLineEdit, QComboBox, QCheckBox +from PyQt6.QtGui import QValidator, QRegularExpressionValidator logger = logging.getLogger(__name__) @@ -10,10 +11,21 @@ class Setting(QObject): E.g. the number of averages or the number of points in a spectrum.""" settings_changed = pyqtSignal() - def __init__(self, name, description) -> None: + def __init__(self, name : str, description : str, default = None) -> None: + """ Create a new setting. + + Args: + name (str): The name of the setting. + description (str): A description of the setting. + """ super().__init__() self.name = name self.description = description + if default is not None: + self.value = default + + # This can be overriden by subclasses + self.widget = self.get_widget() @pyqtSlot(str) def on_value_changed(self, value): @@ -24,7 +36,7 @@ class Setting(QObject): def get_setting(self): return float(self.value) - def get_widget(self): + def get_widget(self): """Return a widget for the setting. The default widget is simply a QLineEdit. This method can be overwritten by subclasses to return a different widget. @@ -38,11 +50,34 @@ class Setting(QObject): widget.editingFinished.connect(lambda x=widget, s=self: s.on_value_changed(x.text())) return widget + def update_widget_style(self): + """ Update the style of the QLineEdit widget to indicate if the value is valid.""" + logger.debug("Updating widget style") + if self.validator.validate(self.widget.text(), 0)[0] == QValidator.State.Acceptable: + self.widget.setStyleSheet("QLineEdit { background-color: white; }") + elif self.validator.validate(self.widget.text(), 0)[0] == QValidator.State.Intermediate: + self.widget.setStyleSheet("QLineEdit { background-color: yellow; }") + else: + self.widget.setStyleSheet("QLineEdit { background-color: red; }") + class FloatSetting(Setting): """ A setting that is a Float. """ - def __init__(self, name : str, default : float, description : str) -> None: - super().__init__(name, description) - self.value = default + DEFAULT_LENGTH = 100 + def __init__(self, name : str, default : float, description : str, validator : QValidator = None) -> None: + super().__init__(name, description, default) + + # If a validator is given, set it for the QLineEdit widget + if validator: + self.validator = validator + else: + # Create a regex validator that only allows floats + regex = "[-+]?[0-9]*\.?[0-9]+" + self.validator = QRegularExpressionValidator(QRegularExpression(regex)) + + self.widget = self.get_widget() + # self.widget.setValidator(self.validator) + # Connect the update_widget_style method to the textChanged signal + self.widget.textChanged.connect(self.update_widget_style) @property def value(self): @@ -51,16 +86,33 @@ class FloatSetting(Setting): @value.setter def value(self, value): try: - self._value = float(value) + if self.validator.validate(value, 0)[0] == QValidator.State.Acceptable: + self._value = float(value) + self.settings_changed.emit() + # This should never be reached because the validator should prevent this except ValueError: raise ValueError("Value must be a float") - self.settings_changed.emit() + # This happens when the validator has not yet been set + except AttributeError: + self._value = float(value) + self.settings_changed.emit() class IntSetting(Setting): """ A setting that is an Integer.""" - def __init__(self, name : str, default : int, description : str) -> None: - super().__init__(name, description) - self.value = default + def __init__(self, name : str, default : int, description : str, validator : QValidator = None) -> None: + super().__init__(name, description, default) + + # If a validator is given, set it for the QLineEdit widget + if validator: + self.validator = validator + else: + # Create a regex validator that only allows integers + regex = "[-+]?[0-9]+" + self.validator = QRegularExpressionValidator(QRegularExpression(regex)) + + self.widget = self.get_widget() + # Connect the update_widget_style method to the textChanged signal + self.widget.textChanged.connect(self.update_widget_style) @property def value(self): @@ -78,8 +130,10 @@ class BooleanSetting(Setting): """ A setting that is a Boolean.""" def __init__(self, name : str, default : bool, description : str) -> None: - super().__init__(name, description) - self.value = default + super().__init__(name, description, default) + + # Overrides the default widget + self.widget = self.get_widget() @property def value(self): @@ -109,13 +163,15 @@ class BooleanSetting(Setting): class SelectionSetting(Setting): """ A setting that is a selection from a list of options.""" def __init__(self, name : str, options : list, default : str, description : str) -> None: - super().__init__(name, description) + super().__init__(name, description, default) # Check if default is in options if default not in options: raise ValueError("Default value must be one of the options") self.options = options - self.value = default + + # Overrides the default widget + self.widget = self.get_widget() @property def value(self): @@ -123,10 +179,16 @@ class SelectionSetting(Setting): @value.setter def value(self, value): - if value in self.options: + try: + if value in self.options: + self._value = value + else: + raise ValueError("Value must be one of the options") + # This fixes a bug when creating the widget when the options are not yet set + except AttributeError: self._value = value - else: - raise ValueError("Value must be one of the options") + self.options = [value] + self.settings_changed.emit() def get_widget(self): @@ -164,8 +226,7 @@ class IPSetting(Setting): class StringSetting(Setting): """ A setting that is a string.""" def __init__(self, name : str, default : str, description : str) -> None: - super().__init__(name, description) - self.value = default + super().__init__(name, description, default) @property def value(self): @@ -178,17 +239,4 @@ class StringSetting(Setting): except ValueError: raise ValueError("Value must be a string") - self.settings_changed.emit() - - def get_widget(self): - """Return a widget for the setting. - The default widget is simply a QLineEdit. - This method can be overwritten by subclasses to return a different widget. - - Returns: - QLineEdit: A QLineEdit widget that can be used to change the setting. - """ - widget = QLineEdit(str(self.value)) - widget.setMinimumWidth(100) - widget.editingFinished.connect(lambda x=widget, s=self: s.on_value_changed(x.text())) - return widget \ No newline at end of file + self.settings_changed.emit() \ No newline at end of file