diff --git a/parametertree/Parameter.py b/parametertree/Parameter.py index fce4ae3a..de1c9b63 100644 --- a/parametertree/Parameter.py +++ b/parametertree/Parameter.py @@ -3,25 +3,29 @@ import collections, os, weakref, re from .ParameterItem import ParameterItem PARAM_TYPES = {} - +PARAM_NAMES = {} def registerParameterType(name, cls, override=False): global PARAM_TYPES if name in PARAM_TYPES and not override: raise Exception("Parameter type '%s' already exists (use override=True to replace)" % name) PARAM_TYPES[name] = cls + PARAM_NAMES[cls] = name class Parameter(QtCore.QObject): """ - Tree of name=value pairs (modifiable or not) - - Value may be integer, float, string, bool, color, or list selection - - Optionally, a custom widget may be specified for a property - - Any number of extra columns may be added for other purposes - - Any values may be reset to a default value - - Parameters may be grouped / nested - - Parameter may be subclassed to provide customized behavior. + A Parameter is the basic unit of data in a parameter tree. Each parameter has + a name, a type, a value, and several other properties that modify the behavior of the + Parameter. Parameters may have parent / child / sibling relationships to construct + organized hierarchies. Parameters generally do not have any inherent GUI or visual + interpretation; instead they manage ParameterItem instances which take care of + display and user interaction. + + Note: It is fairly uncommon to use the Parameter class directly; mostly you + will use subclasses which provide specialized type and data handling. The static + pethod Parameter.create(...) is an easy way to generate instances of these subclasses. For more Parameter types, see ParameterTree.parameterTypes module. @@ -88,7 +92,11 @@ class Parameter(QtCore.QObject): Use registerParameterType() to add new class types. """ - cls = PARAM_TYPES[opts['type']] + typ = opts.get('type', None) + if typ is None: + cls = Parameter + else: + cls = PARAM_TYPES[opts['type']] return cls(**opts) def __init__(self, **opts): @@ -102,6 +110,7 @@ class Parameter(QtCore.QObject): 'renamable': False, 'removable': False, 'strictNaming': False, # forces name to be usable as a python variable + #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } self.opts.update(opts) @@ -120,9 +129,7 @@ class Parameter(QtCore.QObject): raise Exception("Parameter must have a string name specified in opts.") self.setName(opts['name']) - for chOpts in self.opts.get('children', []): - #print self, "Add child:", type(chOpts), id(chOpts) - self.addChild(chOpts) + self.addChildren(self.opts.get('children', [])) if 'value' in self.opts and 'default' not in self.opts: self.opts['default'] = self.opts['value'] @@ -159,6 +166,22 @@ class Parameter(QtCore.QObject): def type(self): return self.opts['type'] + def isType(self, typ): + """ + Return True if this parameter type matches the name *typ*. + This can occur either of two ways: + + - If self.type() == *typ* + - If this parameter's class is registered with the name *typ* + """ + if self.type() == typ: + return True + global PARAM_TYPES + cls = PARAM_TYPES.get(typ, None) + if cls is None: + raise Exception("Type name '%s' is not registered." % str(typ)) + return self.__class__ is cls + def childPath(self, child): """ Return the path of parameter names from self to child. @@ -175,7 +198,6 @@ class Parameter(QtCore.QObject): def setValue(self, value, blockSignal=None): ## return the actual value that was set ## (this may be different from the value that was requested) - #print self, "Set value:", value, self.opts['value'], self.opts['value'] == value try: if blockSignal is not None: self.sigValueChanged.disconnect(blockSignal) @@ -205,10 +227,13 @@ class Parameter(QtCore.QObject): The tree state may be restored from this structure using restoreState() """ state = self.opts.copy() - state['children'] = [ch.saveState() for ch in self] + state['children'] = collections.OrderedDict([(ch.name(), ch.saveState()) for ch in self]) + if state['type'] is None: + global PARAM_NAMES + state['type'] = PARAM_NAMES.get(type(self), None) return state - def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True): + def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True): """ Restore the state of this parameter and its children from a structure generated using saveState() If recursive is True, then attempt to restore the state of child parameters as well. @@ -216,55 +241,70 @@ class Parameter(QtCore.QObject): created if they do not already exist. If removeChildren is True, then any children which are not referenced in the state object will be removed. + If blockSignals is True, no signals will be emitted until the tree has been completely restored. + This prevents signal handlers from responding to a partially-rebuilt network. """ childState = state.get('children', []) - self.setOpts(**state) - if not recursive: - return - - ptr = 0 ## pointer to first child that has not been restored yet - foundChilds = set() - #print "==============", self.name() - for ch in childState: - name = ch['name'] - typ = ch['type'] - #print('child: %s, %s' % (self.name()+'.'+name, typ)) + ## list of children may be stored either as list or dict. + if isinstance(childState, dict): + childState = childState.values() - ## First, see if there is already a child with this name and type - gotChild = False - for i, ch2 in enumerate(self.childs[ptr:]): - #print ch2, ch2.name, ch2.type - if ch2.name() != name or ch2.type() != typ: - continue - gotChild = True - #print " found it" - if i != 0: ## move parameter to next position - self.removeChild(ch2) + + if blockSignals: + self.blockTreeChangeSignal() + + try: + self.setOpts(**state) + + if not recursive: + return + + ptr = 0 ## pointer to first child that has not been restored yet + foundChilds = set() + #print "==============", self.name() + + for ch in childState: + name = ch['name'] + typ = ch['type'] + #print('child: %s, %s' % (self.name()+'.'+name, typ)) + + ## First, see if there is already a child with this name and type + gotChild = False + for i, ch2 in enumerate(self.childs[ptr:]): + #print " ", ch2.name(), ch2.type() + if ch2.name() != name or not ch2.isType(typ): + continue + gotChild = True + #print " found it" + if i != 0: ## move parameter to next position + #self.removeChild(ch2) + self.insertChild(ptr, ch2) + #print " moved to position", ptr + ch2.restoreState(ch, recursive=recursive, addChildren=addChildren, removeChildren=removeChildren) + foundChilds.add(ch2) + + break + + if not gotChild: + if not addChildren: + #print " ignored child" + continue + #print " created new" + ch2 = Parameter.create(**ch) self.insertChild(ptr, ch2) - #print " moved to position", ptr - ch2.restoreState(ch, recursive=recursive, addChildren=addChildren, removeChildren=removeChildren) - foundChilds.add(ch2) - - break - - if not gotChild: - if not addChildren: - #print " ignored child" - continue - #print " created new" - ch2 = Parameter.create(**ch) - self.insertChild(ptr, ch2) - foundChilds.add(ch2) - - ptr += 1 - - if removeChildren: - for ch in self: - if ch not in foundChilds: - #print " remove:", ch - self.removeChild(ch) + foundChilds.add(ch2) + + ptr += 1 + if removeChildren: + for ch in self.childs[:]: + if ch not in foundChilds: + #print " remove:", ch + self.removeChild(ch) + finally: + if blockSignals: + self.unblockTreeChangeSignal() @@ -362,6 +402,22 @@ class Parameter(QtCore.QObject): def addChild(self, child): """Add another parameter to the end of this parameter's child list.""" return self.insertChild(len(self.childs), child) + + def addChildren(self, children): + ## If children was specified as dict, then assume keys are the names. + if isinstance(children, dict): + ch2 = [] + for name, opts in children.items(): + if isinstance(opts, dict) and 'name' not in opts: + opts = opts.copy() + opts['name'] = name + ch2.append(opts) + children = ch2 + + for chOpts in children: + #print self, "Add child:", type(chOpts), id(chOpts) + self.addChild(chOpts) + def insertChild(self, pos, child): """ @@ -373,7 +429,7 @@ class Parameter(QtCore.QObject): child = Parameter.create(**child) name = child.name() - if name in self.names: + if name in self.names and child is not self.names[name]: if child.opts.get('autoIncrementName', False): name = self.incrementName(name) child.setName(name) @@ -382,15 +438,16 @@ class Parameter(QtCore.QObject): if isinstance(pos, Parameter): pos = self.childs.index(pos) - if child.parent() is not None: - child.remove() + with self.treeChangeBlocker(): + if child.parent() is not None: + child.remove() + + self.names[name] = child + self.childs.insert(pos, child) - self.names[name] = child - self.childs.insert(pos, child) - - child.parentChanged(self) - self.sigChildAdded.emit(self, child, pos) - child.sigTreeStateChanged.connect(self.treeStateChanged) + child.parentChanged(self) + self.sigChildAdded.emit(self, child, pos) + child.sigTreeStateChanged.connect(self.treeStateChanged) return child def removeChild(self, child): @@ -398,7 +455,6 @@ class Parameter(QtCore.QObject): name = child.name() if name not in self.names or self.names[name] is not child: raise Exception("Parameter %s is not my child; can't remove." % str(child)) - del self.names[name] self.childs.pop(self.childs.index(child)) child.parentChanged(None) @@ -414,6 +470,9 @@ class Parameter(QtCore.QObject): """Return a list of this parameter's children.""" ## warning -- this overrides QObject.children return self.childs[:] + + def hasChildren(self): + return len(self.childs) > 0 def parentChanged(self, parent): """This method is called when the parameter's parent has changed. @@ -443,7 +502,7 @@ class Parameter(QtCore.QObject): num = int(num) while True: newName = base + ("%%0%dd"%numLen) % num - if newName not in self.childs: + if newName not in self.names: return newName num += 1 diff --git a/parametertree/ParameterTree.py b/parametertree/ParameterTree.py index cd56ea00..433209e8 100644 --- a/parametertree/ParameterTree.py +++ b/parametertree/ParameterTree.py @@ -8,17 +8,17 @@ import collections, os, weakref, re class ParameterTree(TreeWidget): """Widget used to display or control data from a ParameterSet""" - def __init__(self, parent=None): + def __init__(self, parent=None, showHeader=True): TreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setHorizontalScrollMode(self.ScrollPerPixel) self.setAnimated(False) self.setColumnCount(2) self.setHeaderLabels(["Parameter", "Value"]) - self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.paramSet = None self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) + self.setHeaderHidden(not showHeader) self.itemChanged.connect(self.itemChangedEvent) self.lastSel = None self.setRootIsDecorated(False) diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py index 3e650afc..46cce317 100644 --- a/parametertree/parameterTypes.py +++ b/parametertree/parameterTypes.py @@ -152,7 +152,6 @@ class WidgetParameterItem(ParameterItem): def valueChanged(self, param, val, force=False): ## called when the parameter's value has changed ParameterItem.valueChanged(self, param, val) - self.widget.sigChanged.disconnect(self.widgetValueChanged) try: if force or val != self.widget.value(): @@ -273,12 +272,21 @@ class SimpleParameter(Parameter): def __init__(self, *args, **kargs): Parameter.__init__(self, *args, **kargs) + + ## override a few methods for color parameters if self.opts['type'] == 'color': self.value = self.colorValue + self.saveState = self.saveColorState def colorValue(self): return pg.mkColor(Parameter.value(self)) + def saveColorState(self): + state = Parameter.saveState(self) + state['value'] = pg.colorTuple(self.value()) + return state + + registerParameterType('int', SimpleParameter, override=True) registerParameterType('float', SimpleParameter, override=True) registerParameterType('bool', SimpleParameter, override=True) @@ -303,9 +311,8 @@ class GroupParameterItem(ParameterItem): addText = param.opts['addText'] if 'addList' in param.opts: self.addWidget = QtGui.QComboBox() - self.addWidget.addItem(addText) - for t in param.opts['addList']: - self.addWidget.addItem(t) + self.addWidget.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.updateAddList() self.addWidget.currentIndexChanged.connect(self.addChanged) else: self.addWidget = QtGui.QPushButton(addText) @@ -315,7 +322,8 @@ class GroupParameterItem(ParameterItem): l.setContentsMargins(0,0,0,0) w.setLayout(l) l.addWidget(self.addWidget) - l.addItem(QtGui.QSpacerItem(200, 10, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)) + l.addStretch() + #l.addItem(QtGui.QSpacerItem(200, 10, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)) self.addWidgetBox = w self.addItem = QtGui.QTreeWidgetItem([]) self.addItem.setFlags(QtCore.Qt.ItemIsEnabled) @@ -370,17 +378,46 @@ class GroupParameterItem(ParameterItem): ParameterItem.insertChild(self, self.childCount()-1, child) else: ParameterItem.addChild(self, child) - + + def optsChanged(self, param, changed): + if 'addList' in changed: + self.updateAddList() + + def updateAddList(self): + self.addWidget.blockSignals(True) + try: + self.addWidget.clear() + self.addWidget.addItem(self.param.opts['addText']) + for t in self.param.opts['addList']: + self.addWidget.addItem(t) + finally: + self.addWidget.blockSignals(False) + class GroupParameter(Parameter): """ Group parameters are used mainly as a generic parent item that holds (and groups!) a set - of child parameters. It also provides a simple mechanism for displaying a button or combo - that can be used to add new parameters to the group. + of child parameters. + + It also provides a simple mechanism for displaying a button or combo + that can be used to add new parameters to the group. To enable this, the group + must be initialized with the 'addText' option (the text will be displayed on + a button which, when clicked, will cause addNew() to be called). If the 'addList' + option is specified as well, then a dropdown-list of addable items will be displayed + instead of a button. """ itemClass = GroupParameterItem def addNew(self, typ=None): + """ + This method is called when the user has requested to add a new item to the group. + """ raise Exception("Must override this function in subclass.") + + def setAddList(self, vals): + """Change the list of options available for the user to add to the group.""" + self.setOpts(addList=vals) + + registerParameterType('group', GroupParameter, override=True) @@ -394,8 +431,10 @@ class ListParameterItem(WidgetParameterItem): """ def __init__(self, param, depth): + self.targetValue = None WidgetParameterItem.__init__(self, param, depth) + def makeWidget(self): opts = self.param.opts t = opts['type'] @@ -418,7 +457,8 @@ class ListParameterItem(WidgetParameterItem): #else: #return key #print key, self.forward - return self.forward[key] + + return self.forward.get(key, None) def setValue(self, val): #vals = self.param.opts['limits'] @@ -431,6 +471,7 @@ class ListParameterItem(WidgetParameterItem): #raise Exception("Value '%s' not allowed." % val) #else: #key = unicode(val) + self.targetValue = val if val not in self.reverse: self.widget.setCurrentIndex(0) else: @@ -440,27 +481,23 @@ class ListParameterItem(WidgetParameterItem): def limitsChanged(self, param, limits): # set up forward / reverse mappings for name:value - self.forward = collections.OrderedDict() ## name: value - self.reverse = collections.OrderedDict() ## value: name - if isinstance(limits, dict): - for k, v in limits.items(): - self.forward[k] = v - self.reverse[v] = k - else: - for v in limits: - n = asUnicode(v) - self.forward[n] = v - self.reverse[v] = n + #self.forward = collections.OrderedDict([('', None)]) ## name: value + #self.reverse = collections.OrderedDict([(None, '')]) ## value: name + if len(limits) == 0: + limits = [''] ## Can never have an empty list--there is always at least a singhe blank item. + + self.forward, self.reverse = ListParameter.mapping(limits) try: self.widget.blockSignals(True) - val = asUnicode(self.widget.currentText()) + val = self.targetValue #asUnicode(self.widget.currentText()) + self.widget.clear() for k in self.forward: self.widget.addItem(k) if k == val: self.widget.setCurrentIndex(self.widget.count()-1) - + self.updateDisplayLabel() finally: self.widget.blockSignals(False) @@ -472,29 +509,107 @@ class ListParameter(Parameter): def __init__(self, **opts): self.forward = collections.OrderedDict() ## name: value self.reverse = collections.OrderedDict() ## value: name + + ## Parameter uses 'limits' option to define the set of allowed values if 'values' in opts: opts['limits'] = opts['values'] + if opts.get('limits', None) is None: + opts['limits'] = [] Parameter.__init__(self, **opts) def setLimits(self, limits): - self.forward = collections.OrderedDict() ## name: value - self.reverse = collections.OrderedDict() ## value: name - if isinstance(limits, dict): - for k, v in limits.items(): - self.forward[k] = v - self.reverse[v] = k - else: - for v in limits: - n = asUnicode(v) - self.forward[n] = v - self.reverse[v] = n + self.forward, self.reverse = self.mapping(limits) Parameter.setLimits(self, limits) #print self.name(), self.value(), limits if self.value() not in self.reverse and len(self.reverse) > 0: self.setValue(list(self.reverse.keys())[0]) + @staticmethod + def mapping(limits): + ## Return forward and reverse mapping dictionaries given a limit specification + forward = collections.OrderedDict() ## name: value + reverse = collections.OrderedDict() ## value: name + if isinstance(limits, dict): + for k, v in limits.items(): + forward[k] = v + reverse[v] = k + else: + for v in limits: + n = asUnicode(v) + forward[n] = v + reverse[v] = n + return forward, reverse registerParameterType('list', ListParameter, override=True) + +class ActionParameterItem(ParameterItem): + def __init__(self, param, depth): + ParameterItem.__init__(self, param, depth) + self.layoutWidget = QtGui.QWidget() + self.layout = QtGui.QHBoxLayout() + self.layoutWidget.setLayout(self.layout) + self.button = QtGui.QPushButton(param.name()) + #self.layout.addSpacing(100) + self.layout.addWidget(self.button) + self.layout.addStretch() + self.button.clicked.connect(self.buttonClicked) + param.sigNameChanged.connect(self.paramRenamed) + self.setText(0, '') + + def treeWidgetChanged(self): + ParameterItem.treeWidgetChanged(self) + tree = self.treeWidget() + if tree is None: + return + + tree.setFirstItemColumnSpanned(self, True) + tree.setItemWidget(self, 0, self.layoutWidget) + + def paramRenamed(self, param, name): + self.button.setText(name) + + def buttonClicked(self): + self.param.activate() + +class ActionParameter(Parameter): + """Used for displaying a button within the tree.""" + itemClass = ActionParameterItem + sigActivated = QtCore.Signal(object) + + def activate(self): + self.sigActivated.emit(self) + self.emitStateChanged('activated', None) + +registerParameterType('action', ActionParameter, override=True) + + + +class TextParameterItem(WidgetParameterItem): + def __init__(self, param, depth): + WidgetParameterItem.__init__(self, param, depth) + self.subItem = QtGui.QTreeWidgetItem() + self.addChild(self.subItem) + + def treeWidgetChanged(self): + self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) + self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + self.setExpanded(True) + + def makeWidget(self): + self.textBox = QtGui.QTextEdit() + self.textBox.setMaximumHeight(100) + self.textBox.value = lambda: str(self.textBox.toPlainText()) + self.textBox.setValue = self.textBox.setPlainText + self.textBox.sigChanged = self.textBox.textChanged + return self.textBox + +class TextParameter(Parameter): + """Editable string; displayed as large text box in the tree.""" + itemClass = TextParameterItem + + + +registerParameterType('text', TextParameter, override=True)