Source code for pyqtgraph.parametertree.parameterTypes.checklist

from ... import functions as fn
from ...Qt import QtWidgets
from ...SignalProxy import SignalProxy
from ..ParameterItem import ParameterItem
from . import BoolParameterItem, SimpleParameter
from .basetypes import Emitter, GroupParameter, GroupParameterItem, WidgetParameterItem
from .list import ListParameter


[docs] 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'] = self.makeDefaultButton() self.metaBtnLayout.addWidget(self.metaBtns['default']) def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) tw = self.treeWidget() if tw is None: return tw.setItemWidget(self, 1, self.metaBtnWidget) def selectAllClicked(self): # timer stop: see explanation on param.setToDefault() self.param.valChangingProxy.timer.stop() self.param.setValue(self.param.reverse[0]) def clearAllClicked(self): # timer stop: see explanation on param.setToDefault() self.param.valChangingProxy.timer.stop() 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): super().optsChanged(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 name, btn in self.metaBtns.items(): if name != 'default': btn.setDisabled(exclusive or (not enabled)) self.btnGrp.setExclusive(exclusive) # "Limits" will force update anyway, no need to duplicate if it's present if 'limits' not in opts and ('enabled' in opts or 'readonly' in opts): self.updateDefaultBtn() def expandedChangedEvent(self, expanded): for btn in self.metaBtns.values(): btn.setVisible(expanded) def valueChanged(self, param, val): self.updateDefaultBtn() def updateDefaultBtn(self): self.metaBtns["default"].setEnabled( not self.param.valueIsDefault() and self.param.opts["enabled"] and self.param.writable() ) return makeDefaultButton = WidgetParameterItem.makeDefaultButton defaultClicked = WidgetParameterItem.defaultClicked
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): @property def itemClass(self): if self.opts.get('type') == 'bool': return BoolParameterItem else: return RadioParameterItem
[docs] 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. delay Controls the wait time between editing the checkboxes/radio button children and firing a "value changed" signal. This allows users to edit multiple boxes at once for a single value update. ============== ======================================================== """ itemClass = ChecklistParameterItem def __init__(self, **opts): # Child options are populated through values, not explicit "children" if 'children' in opts: raise ValueError( "Cannot pass 'children' to ChecklistParameter. Pass a 'value' key only." ) 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) self.valChangingProxy = SignalProxy( self.sigValueChanging, delay=opts.get('delay', 1.0), slot=self._finishChildChanges, threadSafe=False, ) def childrenValue(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 _onChildChanging(self, child, value): # When exclusive, ensure only this value is True if self.opts['exclusive'] and value: value = self.forward[child.name()] else: value = self.childrenValue() self.sigValueChanging.emit(self, value) def updateLimits(self, _param, limits): oldOpts = self.names val = self.opts.get('value', None) # 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._onChildChanging) # Purge child changes before unblocking self.treeStateChanges.clear() self.unblockTreeChangeSignal() self.setValue(val) def _finishChildChanges(self, paramAndValue): param, value = paramAndValue # Interpret value, fire sigValueChanged return self.setValue(value) def optsChanged(self, param, opts): if 'exclusive' in opts: self.updateLimits(None, self.opts.get('limits', [])) if 'delay' in opts: self.valChangingProxy.setDelay(opts['delay']) def setValue(self, value, blockSignal=None): self.targetValue = value if not isinstance(value, list): value = [value] names, values = self._intersectionWithLimits(value) valueToSet = values if self.opts['exclusive']: if len(self.forward): # Exclusive means at least one entry must exist, grab from limits # if they exist names.append(self.reverse[1][0]) if len(names) > 1: names = names[:1] if not len(names): valueToSet = None else: valueToSet = self.forward[names[0]] for chParam in self: checked = chParam.name() in names # Will emit at the end, so no problem discarding existing changes chParam.setValue(checked, self._onChildChanging) super().setValue(valueToSet, blockSignal) def _intersectionWithLimits(self, values: list): """ Returns the (names, values) from limits that intersect with ``values``. """ allowedNames = [] allowedValues = [] # Could be replaced by "value in self.reverse[0]" and "reverse[0].index", # but this allows for using pg.eq to cover more diverse value options for val in values: for limitName, limitValue in zip(*self.reverse): if fn.eq(limitValue, val): allowedNames.append(limitName) allowedValues.append(val) break return allowedNames, allowedValues def setToDefault(self): # Since changing values are covered by a proxy, this method must be overridden # to flush changes. Otherwise, setting to default while waiting for changes # to finalize will override the request to take default values self.valChangingProxy.timer.stop() super().setToDefault() def saveState(self, filter=None): # Unlike the normal GroupParameter, child states shouldn't be separately # preserved state = super().saveState(filter) state.pop("children", None) return state def restoreState( self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True ): # Child management shouldn't happen through state return super().restoreState( state, recursive, addChildren=False, removeChildren=False, blockSignals=blockSignals )