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 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 c4ced31d18
commit 7b899eca2a
3 changed files with 97 additions and 15 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.unitful
class MutexParameter(pTypes.ListParameter): class MutexParameter(pTypes.ListParameter):

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"
}, },
@ -58,6 +59,7 @@
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"siPrefix": true, "siPrefix": true,
"noUnitEditing": true,
"param": [ "param": [
"pwm", "pwm",
"ch", "ch",
@ -76,7 +78,9 @@
-273, -273,
300 300
], ],
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"param": [ "param": [
"pid", "pid",
"ch", "ch",
@ -108,6 +112,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"param": [ "param": [
"pwm", "pwm",
"ch", "ch",
@ -126,6 +131,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"limits": [ "limits": [
0, 0,
2 2
@ -150,6 +156,7 @@
], ],
"siPrefix": true, "siPrefix": true,
"suffix": "V", "suffix": "V",
"noUnitEditing": true,
"param": [ "param": [
"pwm", "pwm",
"ch", "ch",
@ -179,7 +186,9 @@
-100, -100,
100 100
], ],
"format": "{value:.4f} °C", "format": "{value:.4f} {suffix}",
"suffix": "°C",
"noUnitEditing": true,
"param": [ "param": [
"s-h", "s-h",
"ch", "ch",
@ -196,6 +205,7 @@
"step": 1, "step": 1,
"siPrefix": true, "siPrefix": true,
"suffix": "Ω", "suffix": "Ω",
"noUnitEditing": true,
"param": [ "param": [
"s-h", "s-h",
"ch", "ch",
@ -211,6 +221,7 @@
"value": 3950, "value": 3950,
"step": 1, "step": 1,
"suffix": "K", "suffix": "K",
"noUnitEditing": true,
"decimals": 4, "decimals": 4,
"param": [ "param": [
"s-h", "s-h",
@ -269,6 +280,7 @@
"type": "float", "type": "float",
"step": 0.1, "step": 0.1,
"suffix": "Hz", "suffix": "Hz",
"noUnitEditing": true,
"param": [ "param": [
"pid", "pid",
"ch", "ch",
@ -283,6 +295,7 @@
"type": "float", "type": "float",
"step": 0.1, "step": 0.1,
"suffix": "s", "suffix": "s",
"noUnitEditing": true,
"param": [ "param": [
"pid", "pid",
"ch", "ch",
@ -311,6 +324,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"param": [ "param": [
"pid", "pid",
"ch", "ch",
@ -332,6 +346,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"param": [ "param": [
"pid", "pid",
"ch", "ch",
@ -355,7 +370,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": [ "pid_autotune": [
"target_temp", "target_temp",
"ch" "ch"
@ -376,6 +393,7 @@
"siPrefix": true, "siPrefix": true,
"pinSiPrefix": "m", "pinSiPrefix": "m",
"suffix": "A", "suffix": "A",
"noUnitEditing": true,
"pid_autotune": [ "pid_autotune": [
"test_current", "test_current",
"ch" "ch"
@ -389,7 +407,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": [ "pid_autotune": [
"temp_swing", "temp_swing",
"ch" "ch"
@ -402,7 +422,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": [ "pid_autotune": [
"lookback", "lookback",
"ch" "ch"

View File

@ -1,3 +1,6 @@
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.parameterTypes import ( 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. 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 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): 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)
@ -34,7 +93,7 @@ class PinSIPrefixSpinBox(SpinBox):
decimals = self.opts['decimals'] decimals = self.opts['decimals']
suffix = self.opts['suffix'] suffix = self.opts['suffix']
prefix = self.opts['prefix'] prefix = self.opts['prefix']
pin_si_prefix = self.opts["pinSiPrefix"] pin_si_prefix = self.opts.get("pinSiPrefix")
# format the string # format the string
val = self.value() val = self.value()
@ -50,6 +109,7 @@ class PinSIPrefixSpinBox(SpinBox):
else: else:
(s, p) = fn.siScale(val) (s, p) = fn.siScale(val)
parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val, 'prefix':prefix} parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val, 'prefix':prefix}
self._current_si_prefix = p
else: else:
# no SI prefix /suffix requested; scale is 1 # no SI prefix /suffix requested; scale is 1
@ -61,10 +121,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):
@ -74,7 +134,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
@ -84,7 +144,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 = UnitfulSpinBox()
w.setOpts(**defs) w.setOpts(**defs)
w.sigChanged = w.sigValueChanged w.sigChanged = w.sigValueChanged
w.sigChanging = w.sigValueChanging w.sigChanging = w.sigValueChanging
@ -92,8 +152,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
) )