Adds checklist parameter (#1952)
* Alphabetize parameter registration * Implements a checklist parameter * Several checklist updates - Correct indentation - Allow dict-like setting + non-string values - Fix several bugs associated with 'exclusive' behavior * Add 'checklist' to param tree example * Add documentation * Removes unneeded code * Forgot to rebuilt RST doc * `exclusive` checklist uses radio buttons * better checklist change logic Co-authored-by: Ogi Moore <ognyan.moore@gmail.com>
This commit is contained in:
parent
745b04baa5
commit
babd037cf7
@ -16,6 +16,9 @@ Parameters
|
||||
.. autoclass:: CalendarParameter
|
||||
:members:
|
||||
|
||||
.. autoclass:: ChecklistParameter
|
||||
:members:
|
||||
|
||||
.. autoclass:: ColorMapParameter
|
||||
:members:
|
||||
|
||||
@ -61,6 +64,9 @@ ParameterItems
|
||||
.. autoclass:: CalendarParameterItem
|
||||
:members:
|
||||
|
||||
.. autoclass:: ChecklistParameterItem
|
||||
:members:
|
||||
|
||||
.. autoclass:: ColorMapParameterItem
|
||||
:members:
|
||||
|
||||
|
@ -66,6 +66,7 @@ params = [
|
||||
{'name': 'Integer', 'type': 'int', 'value': 10},
|
||||
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1, 'finite': False},
|
||||
{'name': 'String', 'type': 'str', 'value': "hi", 'tip': 'Well hello'},
|
||||
{'name': 'Checklist', 'type': 'checklist', 'limits': [1,2,3], 'value': 2},
|
||||
{'name': 'List', 'type': 'list', 'limits': [1,2,3], 'value': 2},
|
||||
{'name': 'Named List', 'type': 'list', 'limits': {"one": 1, "two": "twosies", "three": [3,3,3]}, 'value': 2},
|
||||
{'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"},
|
||||
|
@ -2,6 +2,7 @@ from .action import ActionParameter, ActionParameterItem
|
||||
from .basetypes import WidgetParameterItem, SimpleParameter, GroupParameter, GroupParameterItem
|
||||
from .bool import BoolParameterItem
|
||||
from .calendar import CalendarParameter, CalendarParameterItem
|
||||
from .checklist import ChecklistParameter, ChecklistParameterItem
|
||||
from .color import ColorParameter, ColorParameterItem
|
||||
from .colormap import ColorMapParameter, ColorMapParameterItem
|
||||
from .file import FileParameter, FileParameterItem
|
||||
@ -16,22 +17,23 @@ from .str import StrParameterItem
|
||||
from .text import TextParameter, TextParameterItem
|
||||
from ..Parameter import registerParameterType, registerParameterItemType
|
||||
|
||||
registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True)
|
||||
registerParameterItemType('float', NumericParameterItem, SimpleParameter, override=True)
|
||||
registerParameterItemType('bool', BoolParameterItem, SimpleParameter, override=True)
|
||||
registerParameterItemType('float', NumericParameterItem, SimpleParameter, override=True)
|
||||
registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True)
|
||||
registerParameterItemType('str', StrParameterItem, SimpleParameter, override=True)
|
||||
|
||||
registerParameterType('group', GroupParameter, override=True)
|
||||
|
||||
registerParameterType('color', ColorParameter, override=True)
|
||||
registerParameterType('colormap', ColorMapParameter, override=True)
|
||||
registerParameterType('list', ListParameter, override=True)
|
||||
registerParameterType('action', ActionParameter, override=True)
|
||||
registerParameterType('text', TextParameter, override=True)
|
||||
registerParameterType('pen', PenParameter, override=True)
|
||||
registerParameterType('progress', ProgressBarParameter, override=True)
|
||||
registerParameterType('file', FileParameter, override=True)
|
||||
registerParameterType('slider', SliderParameter, override=True)
|
||||
registerParameterType('calendar', CalendarParameter, override=True)
|
||||
registerParameterType('font', FontParameter, override=True)
|
||||
registerParameterType('action', ActionParameter, override=True)
|
||||
registerParameterType('calendar', CalendarParameter, override=True)
|
||||
registerParameterType('checklist', ChecklistParameter, override=True)
|
||||
registerParameterType('color', ColorParameter, override=True)
|
||||
registerParameterType('colormap', ColorMapParameter, override=True)
|
||||
registerParameterType('file', FileParameter, override=True)
|
||||
registerParameterType('font', FontParameter, override=True)
|
||||
registerParameterType('list', ListParameter, override=True)
|
||||
registerParameterType('pen', PenParameter, override=True)
|
||||
registerParameterType('progress', ProgressBarParameter, override=True)
|
||||
# qtenum is a bit specific, hold off on registering for now
|
||||
registerParameterType('slider', SliderParameter, override=True)
|
||||
registerParameterType('text', TextParameter, override=True)
|
||||
|
@ -419,3 +419,10 @@ class GroupParameter(Parameter):
|
||||
def setAddList(self, vals):
|
||||
"""Change the list of options available for the user to add to the group."""
|
||||
self.setOpts(addList=vals)
|
||||
|
||||
class Emitter(QtCore.QObject):
|
||||
"""
|
||||
WidgetParameterItem is not a QObject, so create a QObject wrapper that items can use for emitting
|
||||
"""
|
||||
sigChanging = QtCore.Signal(object, object)
|
||||
sigChanged = QtCore.Signal(object, object)
|
||||
|
215
pyqtgraph/parametertree/parameterTypes/checklist.py
Normal file
215
pyqtgraph/parametertree/parameterTypes/checklist.py
Normal file
@ -0,0 +1,215 @@
|
||||
from . import BoolParameterItem, SimpleParameter
|
||||
from .basetypes import GroupParameterItem, GroupParameter, WidgetParameterItem
|
||||
from .list import ListParameter
|
||||
from .slider import Emitter
|
||||
from .. import ParameterItem
|
||||
from ... import functions as fn
|
||||
from ...Qt import QtWidgets
|
||||
|
||||
class ChecklistParameterItem(GroupParameterItem):
|
||||
"""
|
||||
Wraps a :class:`GroupParameterItem` to manage ``bool`` parameter children. Also provides convenience buttons to
|
||||
select or clear all values at once. Note these conveniences are disabled when ``exclusive`` is *True*.
|
||||
"""
|
||||
def __init__(self, param, depth):
|
||||
self.btnGrp = QtWidgets.QButtonGroup()
|
||||
self.btnGrp.setExclusive(False)
|
||||
self._constructMetaBtns()
|
||||
|
||||
super().__init__(param, depth)
|
||||
|
||||
def _constructMetaBtns(self):
|
||||
self.metaBtnWidget = QtWidgets.QWidget()
|
||||
self.metaBtnLayout = lay = QtWidgets.QHBoxLayout(self.metaBtnWidget)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.setSpacing(2)
|
||||
self.metaBtns = {}
|
||||
lay.addStretch(0)
|
||||
for title in 'Clear', 'Select':
|
||||
self.metaBtns[title] = btn = QtWidgets.QPushButton(f'{title} All')
|
||||
self.metaBtnLayout.addWidget(btn)
|
||||
btn.clicked.connect(getattr(self, f'{title.lower()}AllClicked'))
|
||||
|
||||
self.metaBtns['default'] = WidgetParameterItem.makeDefaultButton(self)
|
||||
self.metaBtnLayout.addWidget(self.metaBtns['default'])
|
||||
|
||||
def defaultClicked(self):
|
||||
self.param.setToDefault()
|
||||
|
||||
def treeWidgetChanged(self):
|
||||
ParameterItem.treeWidgetChanged(self)
|
||||
tw = self.treeWidget()
|
||||
if tw is None:
|
||||
return
|
||||
tw.setItemWidget(self, 1, self.metaBtnWidget)
|
||||
|
||||
def selectAllClicked(self):
|
||||
self.param.setValue(self.param.reverse[0])
|
||||
|
||||
def clearAllClicked(self):
|
||||
self.param.setValue([])
|
||||
|
||||
def insertChild(self, pos, item):
|
||||
ret = super().insertChild(pos, item)
|
||||
self.btnGrp.addButton(item.widget)
|
||||
return ret
|
||||
|
||||
def addChild(self, item):
|
||||
ret = super().addChild(item)
|
||||
self.btnGrp.addButton(item.widget)
|
||||
return ret
|
||||
|
||||
def takeChild(self, i):
|
||||
child = super().takeChild(i)
|
||||
self.btnGrp.removeButton(child.widget)
|
||||
|
||||
def optsChanged(self, param, opts):
|
||||
if 'expanded' in opts:
|
||||
for btn in self.metaBtns.values():
|
||||
btn.setVisible(opts['expanded'])
|
||||
exclusive = opts.get('exclusive', param.opts['exclusive'])
|
||||
enabled = opts.get('enabled', param.opts['enabled'])
|
||||
for btn in self.metaBtns.values():
|
||||
btn.setDisabled(exclusive or (not enabled))
|
||||
self.btnGrp.setExclusive(exclusive)
|
||||
|
||||
def expandedChangedEvent(self, expanded):
|
||||
for btn in self.metaBtns.values():
|
||||
btn.setVisible(expanded)
|
||||
|
||||
class RadioParameterItem(BoolParameterItem):
|
||||
"""
|
||||
Allows radio buttons to function as booleans when `exclusive` is *True*
|
||||
"""
|
||||
|
||||
def __init__(self, param, depth):
|
||||
self.emitter = Emitter()
|
||||
super().__init__(param, depth)
|
||||
|
||||
def makeWidget(self):
|
||||
w = QtWidgets.QRadioButton()
|
||||
w.value = w.isChecked
|
||||
# Since these are only used during exclusive operations, only fire a signal when "True"
|
||||
# to avoid a double-fire
|
||||
w.setValue = w.setChecked
|
||||
w.sigChanged = self.emitter.sigChanged
|
||||
w.toggled.connect(self.maybeSigChanged)
|
||||
self.hideWidget = False
|
||||
return w
|
||||
|
||||
def maybeSigChanged(self, val):
|
||||
"""
|
||||
Make sure to only activate on a "true" value, since an exclusive button group fires once to deactivate
|
||||
the old option and once to activate the new selection
|
||||
"""
|
||||
if not val:
|
||||
return
|
||||
self.emitter.sigChanged.emit(self, val)
|
||||
|
||||
|
||||
# Proxy around radio/bool type so the correct item class gets instantiated
|
||||
class BoolOrRadioParameter(SimpleParameter):
|
||||
|
||||
def __init__(self, **kargs):
|
||||
if kargs.get('type') == 'bool':
|
||||
self.itemClass = BoolParameterItem
|
||||
else:
|
||||
self.itemClass = RadioParameterItem
|
||||
super().__init__(**kargs)
|
||||
|
||||
class ChecklistParameter(GroupParameter):
|
||||
"""
|
||||
Can be set just like a :class:`ListParameter`, but allows for multiple values to be selected simultaneously.
|
||||
|
||||
============== ========================================================
|
||||
**Options**
|
||||
exclusive When *False*, any number of options can be selected. The resulting ``value()`` is a list of
|
||||
all checked values. When *True*, it behaves like a ``list`` type -- only one value can be selected.
|
||||
If no values are selected and ``exclusive`` is set to *True*, the first available limit is selected.
|
||||
The return value of an ``exclusive`` checklist is a single value rather than a list with one element.
|
||||
============== ========================================================
|
||||
"""
|
||||
itemClass = ChecklistParameterItem
|
||||
|
||||
def __init__(self, **opts):
|
||||
self.targetValue = None
|
||||
limits = opts.setdefault('limits', [])
|
||||
self.forward, self.reverse = ListParameter.mapping(limits)
|
||||
value = opts.setdefault('value', limits)
|
||||
opts.setdefault('exclusive', False)
|
||||
super().__init__(**opts)
|
||||
# Force 'exclusive' to trigger by making sure value is not the same
|
||||
self.sigLimitsChanged.connect(self.updateLimits)
|
||||
self.sigOptionsChanged.connect(self.optsChanged)
|
||||
if len(limits):
|
||||
# Since update signal wasn't hooked up until after parameter construction, need to fire manually
|
||||
self.updateLimits(self, limits)
|
||||
# Also, value calculation will be incorrect until children are added, so make sure to recompute
|
||||
self.setValue(value)
|
||||
|
||||
def updateLimits(self, _param, limits):
|
||||
oldOpts = self.names
|
||||
val = self.opts['value']
|
||||
# Make sure adding and removing children don't cause tree state changes
|
||||
self.blockTreeChangeSignal()
|
||||
self.clearChildren()
|
||||
self.forward, self.reverse = ListParameter.mapping(limits)
|
||||
if self.opts.get('exclusive'):
|
||||
typ = 'radio'
|
||||
else:
|
||||
typ = 'bool'
|
||||
for chName in self.forward:
|
||||
# Recycle old values if they match the new limits
|
||||
newVal = bool(oldOpts.get(chName, False))
|
||||
child = BoolOrRadioParameter(type=typ, name=chName, value=newVal, default=None)
|
||||
self.addChild(child)
|
||||
# Prevent child from broadcasting tree state changes, since this is handled by self
|
||||
child.blockTreeChangeSignal()
|
||||
child.sigValueChanged.connect(self._onSubParamChange)
|
||||
# Purge child changes before unblocking
|
||||
self.treeStateChanges.clear()
|
||||
self.unblockTreeChangeSignal()
|
||||
self.setValue(val)
|
||||
|
||||
def _onSubParamChange(self, param, value):
|
||||
if self.opts['exclusive']:
|
||||
val = self.reverse[0][self.reverse[1].index(param.name())]
|
||||
return self.setValue(val)
|
||||
# Interpret value, fire sigValueChanged
|
||||
return self.setValue(self.value())
|
||||
|
||||
def optsChanged(self, param, opts):
|
||||
if 'exclusive' in opts:
|
||||
# Force set value to ensure updates
|
||||
# self.opts['value'] = self._VALUE_UNSET
|
||||
self.updateLimits(None, self.opts.get('limits', []))
|
||||
|
||||
def value(self):
|
||||
vals = [self.forward[p.name()] for p in self.children() if p.value()]
|
||||
exclusive = self.opts['exclusive']
|
||||
if not vals and exclusive:
|
||||
return None
|
||||
elif exclusive:
|
||||
return vals[0]
|
||||
else:
|
||||
return vals
|
||||
|
||||
def setValue(self, value, blockSignal=None):
|
||||
self.targetValue = value
|
||||
exclusive = self.opts['exclusive']
|
||||
# Will emit at the end, so no problem discarding existing changes
|
||||
cmpVals = value if isinstance(value, list) else [value]
|
||||
for ii in range(len(cmpVals)-1, -1, -1):
|
||||
exists = any(fn.eq(cmpVals[ii], lim) for lim in self.reverse[0])
|
||||
if not exists:
|
||||
del cmpVals[ii]
|
||||
names = [self.reverse[1][self.reverse[0].index(val)] for val in cmpVals]
|
||||
if exclusive and len(names) > 1:
|
||||
names = [names[0]]
|
||||
elif exclusive and not len(names) and len(self.forward):
|
||||
# An option is required during exclusivity
|
||||
names = [self.reverse[1][0]]
|
||||
for chParam in self:
|
||||
checked = chParam.name() in names
|
||||
chParam.setValue(checked, self._onSubParamChange)
|
||||
super().setValue(self.value(), blockSignal)
|
@ -1,19 +1,9 @@
|
||||
import numpy as np
|
||||
|
||||
from .basetypes import WidgetParameterItem
|
||||
from .basetypes import WidgetParameterItem, Emitter
|
||||
from .. import Parameter
|
||||
from ...Qt import QtCore, QtWidgets
|
||||
|
||||
|
||||
class Emitter(QtCore.QObject):
|
||||
"""
|
||||
WidgetParameterItem is not a QObject, and the slider's value needs to be converted before
|
||||
emitting. So, create an emitter class here that can be used instead
|
||||
"""
|
||||
sigChanging = QtCore.Signal(object, object)
|
||||
sigChanged = QtCore.Signal(object, object)
|
||||
|
||||
|
||||
class SliderParameterItem(WidgetParameterItem):
|
||||
slider: QtWidgets.QSlider
|
||||
span: np.ndarray
|
||||
|
Loading…
Reference in New Issue
Block a user