From fff42bfa4c3a7ebef431f74f24f14770e7ef5e92 Mon Sep 17 00:00:00 2001 From: atse Date: Fri, 26 Jul 2024 17:07:51 +0800 Subject: [PATCH] ctrl_panel: Pin down units for editable fields User input always has the same order of magnitude, so allowing multiple siPrefixes would be unwanted complexity. Don't allow them to be changed. The Parameter option "noUnitEditing" is added to do so by the following measures: 1. Don't validate for changed siPrefix (if pinned) and suffix, which avoids their removal. 2. Avoid getting the cursor embedded within the unit. --- pytec/pytec/gui/view/ctrl_panel.py | 2 +- .../{pin_si_prefix.py => lockable_unit.py} | 77 +++++++++++++++++-- pytec/pytec/gui/view/param_tree.json | 35 +++++++-- 3 files changed, 100 insertions(+), 14 deletions(-) rename pytec/pytec/gui/view/{pin_si_prefix.py => lockable_unit.py} (52%) diff --git a/pytec/pytec/gui/view/ctrl_panel.py b/pytec/pytec/gui/view/ctrl_panel.py index c907591..c98af87 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.lockable_unit class MutexParameter(pTypes.ListParameter): diff --git a/pytec/pytec/gui/view/pin_si_prefix.py b/pytec/pytec/gui/view/lockable_unit.py similarity index 52% rename from pytec/pytec/gui/view/pin_si_prefix.py rename to pytec/pytec/gui/view/lockable_unit.py index 1b0ad3d..78e519f 100644 --- a/pytec/pytec/gui/view/pin_si_prefix.py +++ b/pytec/pytec/gui/view/lockable_unit.py @@ -1,21 +1,84 @@ +from PyQt6.QtCore import QSignalBlocker +from PyQt6.QtGui import QValidator + from pyqtgraph import SpinBox import pyqtgraph.functions as fn from pyqtgraph.parametertree import registerParameterItemType from pyqtgraph.parametertree.parameterTypes import SimpleParameter, NumericParameterItem -class PinSIPrefixSpinBox(SpinBox): +class LockableUnitSpinBox(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 suffix and pinned siPrefix + of the SpinBox text is fixed and uneditable. """ + def __init__(self, parent=None, value=0.0, **kwargs): + super().__init__(parent, value, **kwargs) + + 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"] + pinned_si_prefix = self.opts.get("pinSiPrefix") + + suffix_edited = not strn.endswith(suffix) + pinned_si_prefix_edited = ( + pinned_si_prefix is not None + and not strn.removesuffix(suffix).endswith(pinned_si_prefix) + ) + + if suffix_edited or pinned_si_prefix_edited: + ret = QValidator.State.Invalid + + return ret, strn, pos + + def _editor_cursor_position_changed(self, oldpos, newpos): + # Called on cursor position change + # Skips over the suffix and pinned SI prefix on cursor navigation if option + # noUnitEditing is enabled. + + # Modified from the original Qt C++ source, + # QAbstractSpinBox::editorCursorPositionChanged. + # Their suffix is different than our suffix; there's no obvious way to set + # theirs here in the derived class since it is private. + + if self.opts.get("noUnitEditing") is True: + edit = self.lineEdit() + if edit.hasSelectedText(): + return # Allow for selecting units, for copy-and-paste + + pinned_si_prefix = self.opts.get("pinSiPrefix") or "" + unit_len = len(pinned_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) @@ -57,10 +120,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): @@ -70,7 +133,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 @@ -80,7 +143,7 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem): defs[k] = opts[k] if 'limits' in opts: defs['min'], defs['max'] = opts['limits'] - w = PinSIPrefixSpinBox() + w = LockableUnitSpinBox() w.setOpts(**defs) w.sigChanged = w.sigValueChanged w.sigChanging = w.sigValueChanging @@ -88,8 +151,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 ) diff --git a/pytec/pytec/gui/view/param_tree.json b/pytec/pytec/gui/view/param_tree.json index f2ce047..9f4df00 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", @@ -195,7 +204,9 @@ "value": 10000, "step": 1, "siPrefix": true, + "pinSiPrefix": "k", "suffix": "Ω", + "noUnitEditing": true, "param": [ "s-h", "ch", @@ -211,6 +222,7 @@ "value": 3950, "step": 1, "suffix": "K", + "noUnitEditing": true, "decimals": 4, "param": [ "s-h", @@ -269,6 +281,7 @@ "type": "float", "step": 0.1, "suffix": "Hz", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -283,6 +296,7 @@ "type": "float", "step": 0.1, "suffix": "s", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -311,6 +325,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -332,6 +347,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "param": [ "pid", "ch", @@ -355,7 +371,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 +394,7 @@ "siPrefix": true, "pinSiPrefix": "m", "suffix": "A", + "noUnitEditing": true, "pid_autotune": [ "test_current", "ch" @@ -389,7 +408,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 +423,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"