diff --git a/pytec/pytec/gui/view/ctrl_panel.py b/pytec/pytec/gui/view/ctrl_panel.py index c907591..4f55c9d 100644 --- a/pytec/pytec/gui/view/ctrl_panel.py +++ b/pytec/pytec/gui/view/ctrl_panel.py @@ -4,7 +4,7 @@ from pyqtgraph.parametertree import ( Parameter, registerParameterType, ) -import pytec.gui.view.pin_si_prefix +import pytec.gui.view.unitful class MutexParameter(pTypes.ListParameter): diff --git a/pytec/pytec/gui/view/param_tree.json b/pytec/pytec/gui/view/param_tree.json index 6e512a2..e90e7e5 100644 --- a/pytec/pytec/gui/view/param_tree.json +++ b/pytec/pytec/gui/view/param_tree.json @@ -4,7 +4,8 @@ "name": "temperature", "title": "Temperature", "type": "float", - "format": "{value:.4f} °C", + "format": "{value:.4f} {suffix}", + "suffix": "°C", "readonly": true, "tip": "The measured temperature at the thermistor" }, @@ -58,6 +59,7 @@ "pinSiPrefix": "m", "suffix": "A", "siPrefix": true, + "noUnitEditing": true, "param": [ "pwm", "ch", @@ -76,7 +78,9 @@ -273, 300 ], - "format": "{value:.4f} °C", + "format": "{value:.4f} {suffix}", + "suffix": "°C", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -108,6 +112,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "param": [ "pwm", "ch", @@ -126,6 +131,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "limits": [ 0, 2 @@ -150,6 +156,7 @@ ], "siPrefix": true, "suffix": "V", + "noUnitEditing": true, "param": [ "pwm", "ch", @@ -179,7 +186,9 @@ -100, 100 ], - "format": "{value:.4f} °C", + "format": "{value:.4f} {suffix}", + "suffix": "°C", + "noUnitEditing": true, "param": [ "s-h", "ch", @@ -196,6 +205,7 @@ "step": 1, "siPrefix": true, "suffix": "Ω", + "noUnitEditing": true, "param": [ "s-h", "ch", @@ -211,6 +221,7 @@ "value": 3950, "step": 1, "suffix": "K", + "noUnitEditing": true, "decimals": 4, "param": [ "s-h", @@ -269,6 +280,7 @@ "type": "float", "step": 0.1, "suffix": "Hz", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -283,6 +295,7 @@ "type": "float", "step": 0.1, "suffix": "s", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -311,6 +324,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -332,6 +346,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -355,7 +370,9 @@ "type": "float", "value": 20, "step": 0.1, - "format": "{value:.4f} °C", + "format": "{value:.4f} {suffix}", + "suffix": "°C", + "noUnitEditing": true, "pid_autotune": [ "target_temp", "ch" @@ -376,6 +393,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "pid_autotune": [ "test_current", "ch" @@ -389,7 +407,9 @@ "value": 1.5, "step": 0.1, "prefix": "±", - "format": "{value:.4f} °C", + "format": "{value:.4f} {suffix}", + "suffix": "°C", + "noUnitEditing": true, "pid_autotune": [ "temp_swing", "ch" @@ -402,7 +422,9 @@ "type": "float", "value": 3.0, "step": 0.1, - "format": "{value:.4f} s", + "format": "{value:.4f} {suffix}", + "noUnitEditing": true, + "suffix": "s", "pid_autotune": [ "lookback", "ch" diff --git a/pytec/pytec/gui/view/pin_si_prefix.py b/pytec/pytec/gui/view/unitful.py similarity index 53% rename from pytec/pytec/gui/view/pin_si_prefix.py rename to pytec/pytec/gui/view/unitful.py index 27acae6..be44047 100644 --- a/pytec/pytec/gui/view/pin_si_prefix.py +++ b/pytec/pytec/gui/view/unitful.py @@ -1,3 +1,6 @@ +from PyQt6.QtCore import QSignalBlocker +from PyQt6.QtGui import QValidator + from pyqtgraph import SpinBox import pyqtgraph.functions as fn from pyqtgraph.parametertree.parameterTypes import ( @@ -7,18 +10,74 @@ from pyqtgraph.parametertree.parameterTypes import ( ) -class PinSIPrefixSpinBox(SpinBox): +class UnitfulSpinBox(SpinBox): """ Extension of PyQtGraph's SpinBox widget. Adds: * The "pinSiPrefix" option, where the siPrefix could be fixed to a particular scale instead of as determined by its value. + * The "noUnitEditing" option, where the unit portion of the + SpinBox text, including the siPrefix, is fixed and uneditable. """ + def __init__(self, parent=None, value=0.0, **kwargs): + super().__init__(parent, value, **kwargs) + + self._current_si_prefix = "" + self.lineEdit().cursorPositionChanged.connect( + self.editor_cursor_position_changed + ) + + def validate(self, strn, pos): + ret, strn, pos = super().validate(strn, pos) + + if self.opts.get("noUnitEditing") is True: + suffix = self.opts["suffix"] + + # When the unit is edited / removed + if not ( + strn.endswith(suffix) + and strn.removesuffix(suffix).endswith(self._current_si_prefix) + ): + # Then the input is invalid instead of incomplete, reject this change + ret = QValidator.State.Invalid + + return ret, strn, pos + + def editor_cursor_position_changed(self, oldpos, newpos): + """ + Modified from the original Qt C++ source, + QAbstractSpinBox::editorCursorPositionChanged + + Their suffix is different than our suffix; there's no obvious + way to set that one here. + """ + if self.opts.get("noUnitEditing") is True: + edit = self.lineEdit() + if edit.hasSelectedText(): + return # Allow for selecting units, for copy-and-paste + + unit_len = len(self._current_si_prefix) + len(self.opts["suffix"]) + text_len = len(edit.text()) + + pos = -1 + # Cursor in unit + if text_len - unit_len < newpos < text_len: + if oldpos == text_len: + pos = text_len - unit_len + else: + pos = text_len + + if pos != -1: + with QSignalBlocker(edit): + edit.setCursorPosition(pos) + def setOpts(self, **opts): if "pinSiPrefix" in opts: self.opts["pinSiPrefix"] = opts.pop("pinSiPrefix") + if "noUnitEditing" in opts: + self.opts["noUnitEditing"] = opts.pop("noUnitEditing") super().setOpts(**opts) @@ -34,7 +93,7 @@ class PinSIPrefixSpinBox(SpinBox): decimals = self.opts['decimals'] suffix = self.opts['suffix'] prefix = self.opts['prefix'] - pin_si_prefix = self.opts["pinSiPrefix"] + pin_si_prefix = self.opts.get("pinSiPrefix") # format the string val = self.value() @@ -50,6 +109,7 @@ class PinSIPrefixSpinBox(SpinBox): else: (s, p) = fn.siScale(val) parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val, 'prefix':prefix} + self._current_si_prefix = p else: # no SI prefix /suffix requested; scale is 1 @@ -61,10 +121,10 @@ class PinSIPrefixSpinBox(SpinBox): return self.opts['format'].format(**parts) -class PinSIPrefixNumericParameterItem(NumericParameterItem): +class UnitfulNumericParameterItem(NumericParameterItem): """ Subclasses PyQtGraph's `NumericParameterItem` and uses - PinSIPrefixSpinBox for editing. + UnitfulSpinBox for editing. """ def makeWidget(self): @@ -74,7 +134,7 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem): 'value': 0, 'min': None, 'max': None, 'step': 1.0, 'dec': False, 'siPrefix': False, 'suffix': '', 'decimals': 3, - 'pinSiPrefix': None, + 'pinSiPrefix': None, 'noUnitEditing': False, } if t == 'int': defs['int'] = True @@ -84,7 +144,7 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem): defs[k] = opts[k] if 'limits' in opts: defs['min'], defs['max'] = opts['limits'] - w = PinSIPrefixSpinBox() + w = UnitfulSpinBox() w.setOpts(**defs) w.sigChanged = w.sigValueChanged w.sigChanging = w.sigValueChanging @@ -92,8 +152,8 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem): registerParameterItemType( - "float", PinSIPrefixNumericParameterItem, SimpleParameter, override=True + "float", UnitfulNumericParameterItem, SimpleParameter, override=True ) registerParameterItemType( - "int", PinSIPrefixNumericParameterItem, SimpleParameter, override=True + "int", UnitfulNumericParameterItem, SimpleParameter, override=True )