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
|
.. autoclass:: CalendarParameter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: ChecklistParameter
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: ColorMapParameter
|
.. autoclass:: ColorMapParameter
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
@ -61,6 +64,9 @@ ParameterItems
|
||||||
.. autoclass:: CalendarParameterItem
|
.. autoclass:: CalendarParameterItem
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: ChecklistParameterItem
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: ColorMapParameterItem
|
.. autoclass:: ColorMapParameterItem
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ params = [
|
||||||
{'name': 'Integer', 'type': 'int', 'value': 10},
|
{'name': 'Integer', 'type': 'int', 'value': 10},
|
||||||
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1, 'finite': False},
|
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1, 'finite': False},
|
||||||
{'name': 'String', 'type': 'str', 'value': "hi", 'tip': 'Well hello'},
|
{'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': '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': '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"},
|
{'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 .basetypes import WidgetParameterItem, SimpleParameter, GroupParameter, GroupParameterItem
|
||||||
from .bool import BoolParameterItem
|
from .bool import BoolParameterItem
|
||||||
from .calendar import CalendarParameter, CalendarParameterItem
|
from .calendar import CalendarParameter, CalendarParameterItem
|
||||||
|
from .checklist import ChecklistParameter, ChecklistParameterItem
|
||||||
from .color import ColorParameter, ColorParameterItem
|
from .color import ColorParameter, ColorParameterItem
|
||||||
from .colormap import ColorMapParameter, ColorMapParameterItem
|
from .colormap import ColorMapParameter, ColorMapParameterItem
|
||||||
from .file import FileParameter, FileParameterItem
|
from .file import FileParameter, FileParameterItem
|
||||||
|
@ -16,22 +17,23 @@ from .str import StrParameterItem
|
||||||
from .text import TextParameter, TextParameterItem
|
from .text import TextParameter, TextParameterItem
|
||||||
from ..Parameter import registerParameterType, registerParameterItemType
|
from ..Parameter import registerParameterType, registerParameterItemType
|
||||||
|
|
||||||
registerParameterItemType('int', NumericParameterItem, SimpleParameter, override=True)
|
|
||||||
registerParameterItemType('float', NumericParameterItem, SimpleParameter, override=True)
|
|
||||||
registerParameterItemType('bool', BoolParameterItem, 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)
|
registerParameterItemType('str', StrParameterItem, SimpleParameter, override=True)
|
||||||
|
|
||||||
registerParameterType('group', GroupParameter, override=True)
|
registerParameterType('group', GroupParameter, override=True)
|
||||||
|
|
||||||
registerParameterType('color', ColorParameter, override=True)
|
registerParameterType('action', ActionParameter, override=True)
|
||||||
registerParameterType('colormap', ColorMapParameter, override=True)
|
registerParameterType('calendar', CalendarParameter, override=True)
|
||||||
registerParameterType('list', ListParameter, override=True)
|
registerParameterType('checklist', ChecklistParameter, override=True)
|
||||||
registerParameterType('action', ActionParameter, override=True)
|
registerParameterType('color', ColorParameter, override=True)
|
||||||
registerParameterType('text', TextParameter, override=True)
|
registerParameterType('colormap', ColorMapParameter, override=True)
|
||||||
registerParameterType('pen', PenParameter, override=True)
|
registerParameterType('file', FileParameter, override=True)
|
||||||
registerParameterType('progress', ProgressBarParameter, override=True)
|
registerParameterType('font', FontParameter, override=True)
|
||||||
registerParameterType('file', FileParameter, override=True)
|
registerParameterType('list', ListParameter, override=True)
|
||||||
registerParameterType('slider', SliderParameter, override=True)
|
registerParameterType('pen', PenParameter, override=True)
|
||||||
registerParameterType('calendar', CalendarParameter, override=True)
|
registerParameterType('progress', ProgressBarParameter, override=True)
|
||||||
registerParameterType('font', FontParameter, override=True)
|
|
||||||
# qtenum is a bit specific, hold off on registering for now
|
# 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):
|
def setAddList(self, vals):
|
||||||
"""Change the list of options available for the user to add to the group."""
|
"""Change the list of options available for the user to add to the group."""
|
||||||
self.setOpts(addList=vals)
|
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)
|
||||||
|
|
|
@ -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
|
import numpy as np
|
||||||
|
|
||||||
from .basetypes import WidgetParameterItem
|
from .basetypes import WidgetParameterItem, Emitter
|
||||||
from .. import Parameter
|
from .. import Parameter
|
||||||
from ...Qt import QtCore, QtWidgets
|
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):
|
class SliderParameterItem(WidgetParameterItem):
|
||||||
slider: QtWidgets.QSlider
|
slider: QtWidgets.QSlider
|
||||||
span: np.ndarray
|
span: np.ndarray
|
||||||
|
|
Loading…
Reference in New Issue