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.
This commit is contained in:
atse 2024-07-26 17:07:51 +08:00
parent 6df37e31aa
commit 4bda62ab41
3 changed files with 100 additions and 14 deletions

View File

@ -4,7 +4,7 @@ from pyqtgraph.parametertree import (
Parameter, Parameter,
registerParameterType, registerParameterType,
) )
import pytec.gui.view.pin_si_prefix import pytec.gui.view.lockable_unit
class MutexParameter(pTypes.ListParameter): class MutexParameter(pTypes.ListParameter):

View File

@ -1,21 +1,84 @@
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtGui import QValidator
from pyqtgraph import SpinBox from pyqtgraph import SpinBox
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
from pyqtgraph.parametertree import registerParameterItemType from pyqtgraph.parametertree import registerParameterItemType
from pyqtgraph.parametertree.parameterTypes import SimpleParameter, NumericParameterItem from pyqtgraph.parametertree.parameterTypes import SimpleParameter, NumericParameterItem
class PinSIPrefixSpinBox(SpinBox): class LockableUnitSpinBox(SpinBox):
""" """
Extension of PyQtGraph's SpinBox widget. Extension of PyQtGraph's SpinBox widget.
Adds: Adds:
* The "pinSiPrefix" option, where the siPrefix could be fixed to a * The "pinSiPrefix" option, where the siPrefix could be fixed to a
particular scale instead of as determined by its value. 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): def setOpts(self, **opts):
if "pinSiPrefix" in opts: if "pinSiPrefix" in opts:
self.opts["pinSiPrefix"] = opts.pop("pinSiPrefix") self.opts["pinSiPrefix"] = opts.pop("pinSiPrefix")
if "noUnitEditing" in opts:
self.opts["noUnitEditing"] = opts.pop("noUnitEditing")
super().setOpts(**opts) super().setOpts(**opts)
@ -57,10 +120,10 @@ class PinSIPrefixSpinBox(SpinBox):
return self.opts['format'].format(**parts) return self.opts['format'].format(**parts)
class PinSIPrefixNumericParameterItem(NumericParameterItem): class UnitfulNumericParameterItem(NumericParameterItem):
""" """
Subclasses PyQtGraph's `NumericParameterItem` and uses Subclasses PyQtGraph's `NumericParameterItem` and uses
PinSIPrefixSpinBox for editing. UnitfulSpinBox for editing.
""" """
def makeWidget(self): def makeWidget(self):
@ -70,7 +133,7 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem):
'value': 0, 'min': None, 'max': None, 'value': 0, 'min': None, 'max': None,
'step': 1.0, 'dec': False, 'step': 1.0, 'dec': False,
'siPrefix': False, 'suffix': '', 'decimals': 3, 'siPrefix': False, 'suffix': '', 'decimals': 3,
'pinSiPrefix': None, 'pinSiPrefix': None, 'noUnitEditing': False,
} }
if t == 'int': if t == 'int':
defs['int'] = True defs['int'] = True
@ -80,7 +143,7 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem):
defs[k] = opts[k] defs[k] = opts[k]
if 'limits' in opts: if 'limits' in opts:
defs['min'], defs['max'] = opts['limits'] defs['min'], defs['max'] = opts['limits']
w = PinSIPrefixSpinBox() w = LockableUnitSpinBox()
w.setOpts(**defs) w.setOpts(**defs)
w.sigChanged = w.sigValueChanged w.sigChanged = w.sigValueChanged
w.sigChanging = w.sigValueChanging w.sigChanging = w.sigValueChanging
@ -88,8 +151,8 @@ class PinSIPrefixNumericParameterItem(NumericParameterItem):
registerParameterItemType( registerParameterItemType(
"float", PinSIPrefixNumericParameterItem, SimpleParameter, override=True "float", UnitfulNumericParameterItem, SimpleParameter, override=True
) )
registerParameterItemType( registerParameterItemType(
"int", PinSIPrefixNumericParameterItem, SimpleParameter, override=True "int", UnitfulNumericParameterItem, SimpleParameter, override=True
) )

View File

@ -4,7 +4,8 @@
"name": "temperature", "name": "temperature",
"title": "Temperature", "title": "Temperature",
"type": "float", "type": "float",
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"readonly": true, "readonly": true,
"tip": "The measured temperature at the thermistor" "tip": "The measured temperature at the thermistor"
}, },
@ -54,6 +55,7 @@
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"siPrefix": true, "siPrefix": true,
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pwm", "topic": "pwm",
"field": "i_set" "field": "i_set"
@ -71,7 +73,9 @@
-273, -273,
300 300
], ],
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pid", "topic": "pid",
"field": "target" "field": "target"
@ -102,6 +106,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pwm", "topic": "pwm",
"field": "max_i_pos" "field": "max_i_pos"
@ -119,6 +124,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"limits": [ "limits": [
0, 0,
2 2
@ -142,6 +148,7 @@
], ],
"siPrefix": true, "siPrefix": true,
"suffix": "V", "suffix": "V",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pwm", "topic": "pwm",
"field": "max_v" "field": "max_v"
@ -170,7 +177,9 @@
-100, -100,
100 100
], ],
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "s-h", "topic": "s-h",
"field": "t0" "field": "t0"
@ -185,7 +194,9 @@
"value": 10000, "value": 10000,
"step": 1, "step": 1,
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "k",
"suffix": "Ω", "suffix": "Ω",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "s-h", "topic": "s-h",
"field": "r0" "field": "r0"
@ -200,6 +211,7 @@
"value": 3950, "value": 3950,
"step": 1, "step": 1,
"suffix": "K", "suffix": "K",
"noUnitEditing": true,
"decimals": 4, "decimals": 4,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "s-h", "topic": "s-h",
@ -255,6 +267,7 @@
"type": "float", "type": "float",
"step": 0.1, "step": 0.1,
"suffix": "Hz", "suffix": "Hz",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pid", "topic": "pid",
"field": "ki" "field": "ki"
@ -268,6 +281,7 @@
"type": "float", "type": "float",
"step": 0.1, "step": 0.1,
"suffix": "s", "suffix": "s",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pid", "topic": "pid",
"field": "kd" "field": "kd"
@ -295,6 +309,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pid", "topic": "pid",
"field": "output_min" "field": "output_min"
@ -315,6 +330,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"thermostat:set_param": { "thermostat:set_param": {
"topic": "pid", "topic": "pid",
"field": "output_max" "field": "output_max"
@ -337,7 +353,9 @@
"type": "float", "type": "float",
"value": 20, "value": 20,
"step": 0.1, "step": 0.1,
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"pid_autotune": "target_temp", "pid_autotune": "target_temp",
"tip": "The target temperature to autotune for" "tip": "The target temperature to autotune for"
}, },
@ -355,6 +373,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"pid_autotune": "test_current", "pid_autotune": "test_current",
"tip": "The testing current when autotuning" "tip": "The testing current when autotuning"
}, },
@ -365,7 +384,9 @@
"value": 1.5, "value": 1.5,
"step": 0.1, "step": 0.1,
"prefix": "±", "prefix": "±",
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"pid_autotune": "temp_swing", "pid_autotune": "temp_swing",
"tip": "The temperature swing around the target" "tip": "The temperature swing around the target"
}, },
@ -375,7 +396,9 @@
"type": "float", "type": "float",
"value": 3.0, "value": 3.0,
"step": 0.1, "step": 0.1,
"format": "{value:.4f} s", "format": "{value:.4f} {suffix}",
"noUnitEditing": true,
"suffix": "s",
"pid_autotune": "lookback", "pid_autotune": "lookback",
"tip": "Amount of time referenced for tuning" "tip": "Amount of time referenced for tuning"
}, },