pyqtgraph/pyqtgraph/parametertree/parameterTypes/checklist.py

216 lines
8.4 KiB
Python

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)