Fix: Parameter tree ignores user-set 'expanded' state (#1175)

* Fix: Parameter tree ignores user-set 'expanded' state

When setting the 'expanded' state of parameters, this change is not applied
in the graphically visible tree. This commit changes that behaviour by
adding a clause in `ParameterItem.optsChanged` to react to that.

Fixes #1130

* ParameterTree: Add option to synchronize "expanded" state

As seen in #1130, there is interest in synchronizing the "expanded" state
of `Parameter`s in `ParameterTree`s. As a default, this would lead to
users being forced to always have multiple `ParameterTree`s to be
expanded in the exact same way. Since that might not be desirable, this
commit adds an option to customize whether synchronization
of the "expanded" state should happen.

* Fix: Sync Parameter options "renamable" and "removable" with ParameterTrees

Currently, `Parameter` options `renamable` and `removable` are only considered
when building a new `ParameterTree`. This commit makes changes in those
options reflected in the corresponding `ParameterItem`s.

* ParameterTree: Reflect changes in Parameter option 'tip'

* Parameter: When setting "syncExpanded", update "expanded" state directly

Co-authored-by: 2xB <2xB@users.noreply.github.com>
This commit is contained in:
2xB 2020-05-30 22:01:39 +02:00 committed by GitHub
parent 03b8385e62
commit 7672b5b725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 32 deletions

View File

@ -137,9 +137,12 @@ class Parameter(QtCore.QObject):
(default=False) (default=False)
removable If True, the user may remove this Parameter. removable If True, the user may remove this Parameter.
(default=False) (default=False)
expanded If True, the Parameter will appear expanded when expanded If True, the Parameter will initially be expanded in
displayed in a ParameterTree (its children will be ParameterTrees: Its children will be visible.
visible). (default=True) (default=True)
syncExpanded If True, the `expanded` state of this Parameter is
synchronized with all ParameterTrees it is displayed in.
(default=False)
title (str or None) If specified, then the parameter will be title (str or None) If specified, then the parameter will be
displayed to the user using this string as its name. displayed to the user using this string as its name.
However, the parameter will still be referred to However, the parameter will still be referred to
@ -161,6 +164,7 @@ class Parameter(QtCore.QObject):
'removable': False, 'removable': False,
'strictNaming': False, # forces name to be usable as a python variable 'strictNaming': False, # forces name to be usable as a python variable
'expanded': True, 'expanded': True,
'syncExpanded': False,
'title': None, 'title': None,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
} }
@ -461,7 +465,7 @@ class Parameter(QtCore.QObject):
Set any arbitrary options on this parameter. Set any arbitrary options on this parameter.
The exact behavior of this function will depend on the parameter type, but The exact behavior of this function will depend on the parameter type, but
most parameters will accept a common set of options: value, name, limits, most parameters will accept a common set of options: value, name, limits,
default, readonly, removable, renamable, visible, enabled, and expanded. default, readonly, removable, renamable, visible, enabled, expanded and syncExpanded.
See :func:`Parameter.__init__ <pyqtgraph.parametertree.Parameter.__init__>` See :func:`Parameter.__init__ <pyqtgraph.parametertree.Parameter.__init__>`
for more information on default options. for more information on default options.

View File

@ -34,30 +34,20 @@ class ParameterItem(QtGui.QTreeWidgetItem):
param.sigOptionsChanged.connect(self.optsChanged) param.sigOptionsChanged.connect(self.optsChanged)
param.sigParentChanged.connect(self.parentChanged) param.sigParentChanged.connect(self.parentChanged)
opts = param.opts self.updateFlags()
## flag used internally during name editing
self.ignoreNameColumnChange = False
def updateFlags(self):
## called when Parameter opts changed
opts = self.param.opts
## Generate context menu for renaming/removing parameter
self.contextMenu = QtGui.QMenu()
self.contextMenu.addSeparator()
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
if opts.get('renamable', False): if opts.get('renamable', False):
if param.opts.get('title', None) is not None: if opts.get('title', None) is not None:
raise Exception("Cannot make parameter with both title != None and renamable == True.") raise Exception("Cannot make parameter with both title != None and renamable == True.")
flags |= QtCore.Qt.ItemIsEditable flags |= QtCore.Qt.ItemIsEditable
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if opts.get('removable', False):
self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove)
# context menu
context = opts.get('context', None)
if isinstance(context, list):
for name in context:
self.contextMenu.addAction(name).triggered.connect(
self.contextMenuTriggered(name))
elif isinstance(context, dict):
for name, title in context.items():
self.contextMenu.addAction(title).triggered.connect(
self.contextMenuTriggered(name))
## handle movable / dropEnabled options ## handle movable / dropEnabled options
if opts.get('movable', False): if opts.get('movable', False):
@ -65,9 +55,6 @@ class ParameterItem(QtGui.QTreeWidgetItem):
if opts.get('dropEnabled', False): if opts.get('dropEnabled', False):
flags |= QtCore.Qt.ItemIsDropEnabled flags |= QtCore.Qt.ItemIsDropEnabled
self.setFlags(flags) self.setFlags(flags)
## flag used internally during name editing
self.ignoreNameColumnChange = False
def valueChanged(self, param, val): def valueChanged(self, param, val):
@ -120,7 +107,26 @@ class ParameterItem(QtGui.QTreeWidgetItem):
if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\
and "context" not in self.param.opts: and "context" not in self.param.opts:
return return
## Generate context menu for renaming/removing parameter
self.contextMenu = QtGui.QMenu() # Put in global name space to prevent garbage collection
self.contextMenu.addSeparator()
if self.param.opts.get('renamable', False):
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if self.param.opts.get('removable', False):
self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove)
# context menu
context = opts.get('context', None)
if isinstance(context, list):
for name in context:
self.contextMenu.addAction(name).triggered.connect(
self.contextMenuTriggered(name))
elif isinstance(context, dict):
for name, title in context.items():
self.contextMenu.addAction(title).triggered.connect(
self.contextMenuTriggered(name))
self.contextMenu.popup(ev.globalPos()) self.contextMenu.popup(ev.globalPos())
def columnChangedEvent(self, col): def columnChangedEvent(self, col):
@ -141,6 +147,10 @@ class ParameterItem(QtGui.QTreeWidgetItem):
self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back. self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back.
finally: finally:
self.ignoreNameColumnChange = False self.ignoreNameColumnChange = False
def expandedChangedEvent(self, expanded):
if self.param.opts['syncExpanded']:
self.param.setOpts(expanded=expanded)
def nameChanged(self, param, name): def nameChanged(self, param, name):
## called when the parameter's name has changed. ## called when the parameter's name has changed.
@ -158,10 +168,22 @@ class ParameterItem(QtGui.QTreeWidgetItem):
def optsChanged(self, param, opts): def optsChanged(self, param, opts):
"""Called when any options are changed that are not """Called when any options are changed that are not
name, value, default, or limits""" name, value, default, or limits"""
#print opts
if 'visible' in opts: if 'visible' in opts:
self.setHidden(not opts['visible']) self.setHidden(not opts['visible'])
if 'expanded' in opts:
if self.param.opts['syncExpanded']:
if self.isExpanded() != opts['expanded']:
self.setExpanded(opts['expanded'])
if 'syncExpanded' in opts:
if opts['syncExpanded']:
if self.isExpanded() != self.param.opts['expanded']:
self.setExpanded(self.param.opts['expanded'])
self.updateFlags()
def contextMenuTriggered(self, name): def contextMenuTriggered(self, name):
def trigger(): def trigger():
self.param.contextMenu(name) self.param.contextMenu(name)

View File

@ -28,6 +28,8 @@ class ParameterTree(TreeWidget):
self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
self.setHeaderHidden(not showHeader) self.setHeaderHidden(not showHeader)
self.itemChanged.connect(self.itemChangedEvent) self.itemChanged.connect(self.itemChangedEvent)
self.itemExpanded.connect(self.itemExpandedEvent)
self.itemCollapsed.connect(self.itemCollapsedEvent)
self.lastSel = None self.lastSel = None
self.setRootIsDecorated(False) self.setRootIsDecorated(False)
@ -134,6 +136,14 @@ class ParameterTree(TreeWidget):
def itemChangedEvent(self, item, col): def itemChangedEvent(self, item, col):
if hasattr(item, 'columnChangedEvent'): if hasattr(item, 'columnChangedEvent'):
item.columnChangedEvent(col) item.columnChangedEvent(col)
def itemExpandedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(True)
def itemCollapsedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(False)
def selectionChanged(self, *args): def selectionChanged(self, *args):
sel = self.selectedItems() sel = self.selectedItems()

View File

@ -44,10 +44,6 @@ class WidgetParameterItem(ParameterItem):
self.widget = w self.widget = w
self.eventProxy = EventProxy(w, self.widgetEventFilter) self.eventProxy = EventProxy(w, self.widgetEventFilter)
opts = self.param.opts
if 'tip' in opts:
w.setToolTip(opts['tip'])
self.defaultBtn = QtGui.QPushButton() self.defaultBtn = QtGui.QPushButton()
self.defaultBtn.setFixedWidth(20) self.defaultBtn.setFixedWidth(20)
self.defaultBtn.setFixedHeight(20) self.defaultBtn.setFixedHeight(20)
@ -73,6 +69,7 @@ class WidgetParameterItem(ParameterItem):
w.sigChanging.connect(self.widgetValueChanging) w.sigChanging.connect(self.widgetValueChanging)
## update value shown in widget. ## update value shown in widget.
opts = self.param.opts
if opts.get('value', None) is not None: if opts.get('value', None) is not None:
self.valueChanged(self, opts['value'], force=True) self.valueChanged(self, opts['value'], force=True)
else: else:
@ -80,6 +77,8 @@ class WidgetParameterItem(ParameterItem):
self.widgetValueChanged() self.widgetValueChanged()
self.updateDefaultBtn() self.updateDefaultBtn()
self.optsChanged(self.param, self.param.opts)
def makeWidget(self): def makeWidget(self):
""" """
@ -280,6 +279,9 @@ class WidgetParameterItem(ParameterItem):
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
self.widget.setEnabled(not opts['readonly']) self.widget.setEnabled(not opts['readonly'])
if 'tip' in opts:
self.widget.setToolTip(opts['tip'])
## If widget is a SpinBox, pass options straight through ## If widget is a SpinBox, pass options straight through
if isinstance(self.widget, SpinBox): if isinstance(self.widget, SpinBox):
# send only options supported by spinbox # send only options supported by spinbox