Ensure synchronization of parameter and widget's values

This commit is contained in:
Daniel Lidstrom 2020-12-17 21:42:43 -08:00
parent 6313bb8ae3
commit 0703e3db9b
7 changed files with 65 additions and 27 deletions

View File

@ -64,7 +64,7 @@ class ScalableGroup(pTypes.GroupParameter):
params = [
{'name': 'Basic parameter data types', 'type': 'group', 'children': [
{'name': 'Integer', 'type': 'int', 'value': 10},
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1},
{'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1, 'finite': False},
{'name': 'String', 'type': 'str', 'value': "hi"},
{'name': 'List', 'type': 'list', 'values': [1,2,3], 'value': 2},
{'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": "twosies", "three": [3,3,3]}, 'value': 2},

View File

@ -1,7 +1,7 @@
import numpy as np
from .Qt import QtGui, QtCore
from .python2_3 import basestring
from .functions import mkColor
from .functions import mkColor, eq
class ColorMap(object):
@ -257,3 +257,8 @@ class ColorMap(object):
pos = repr(self.pos).replace('\n', '')
color = repr(self.color).replace('\n', '')
return "ColorMap(%s, %s)" % (pos, color)
def __eq__(self, other):
if other is None:
return False
return eq(self.pos, other.pos) and eq(self.color, other.color)

View File

@ -420,18 +420,25 @@ def eq(a, b):
This function has some important differences from the == operator:
1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values.
2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur
1. Returns True if a IS b, even if a==b still evaluates to False.
2. While a is b will catch the case with np.nan values, special handling is done for distinct
float('nan') instances using np.isnan.
3. Tests for equivalence using ==, but silently ignores some common exceptions that can occur
(AtrtibuteError, ValueError).
3. When comparing arrays, returns False if the array shapes are not the same.
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
4. When comparing arrays, returns False if the array shapes are not the same.
5. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
the == operator would return a boolean array).
5. Collections (dict, list, etc.) must have the same type to be considered equal. One
consequence is that comparing a dict to an OrderedDict will always return False.
6. Collections (dict, list, etc.) must have the same type to be considered equal. One
consequence is that comparing a dict to an OrderedDict will always return False.
"""
if a is b:
return True
# The above catches np.nan, but not float('nan')
if isinstance(a, float) and isinstance(b, float):
if np.isnan(a) and np.isnan(b):
return True
# Avoid comparing large arrays against scalars; this is expensive and we know it should return False.
aIsArr = isinstance(a, (np.ndarray, MetaArray))
bIsArr = isinstance(b, (np.ndarray, MetaArray))

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from .. import functions as fn
from ..Qt import QtGui, QtCore
import os, weakref, re
from ..pgcollections import OrderedDict
@ -283,15 +284,15 @@ class Parameter(QtCore.QObject):
if blockSignal is not None:
self.sigValueChanged.disconnect(blockSignal)
value = self._interpretValue(value)
if self.opts['value'] == value:
if fn.eq(self.opts['value'], value):
return value
self.opts['value'] = value
self.sigValueChanged.emit(self, value)
self.sigValueChanged.emit(self, value) # value might change after signal is received by tree item
finally:
if blockSignal is not None:
self.sigValueChanged.connect(blockSignal)
return value
return self.opts['value']
def _interpretValue(self, v):
return v

View File

@ -216,13 +216,16 @@ 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():
if force or not fn.eq(val, self.widget.value()):
try:
self.widget.sigChanged.disconnect(self.widgetValueChanged)
self.param.sigValueChanged.disconnect(self.valueChanged)
self.widget.setValue(val)
self.updateDisplayLabel(val) ## always make sure label is updated, even if values match!
finally:
self.widget.sigChanged.connect(self.widgetValueChanged)
self.param.setValue(self.widget.value())
finally:
self.widget.sigChanged.connect(self.widgetValueChanged)
self.param.sigValueChanged.connect(self.valueChanged)
self.updateDisplayLabel() ## always make sure label is updated, even if values match!
self.updateDefaultBtn()
def updateDefaultBtn(self):

View File

@ -127,8 +127,31 @@ def check_param_types(param, types, map_func, init, objs, keys):
raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v))
def test_limits_enforcement():
p = pt.Parameter.create(name='params', type='group', children=[
dict(name='float', type='float', limits=[0, 1]),
dict(name='int', type='int', bounds=[0, 1]),
dict(name='list', type='list', values=['x', 'y']),
dict(name='dict', type='list', values={'x': 1, 'y': 2}),
])
t = pt.ParameterTree()
t.setParameters(p)
for k, vin, vout in [('float', -1, 0),
('float', 2, 1),
('int', -1, 0),
('int', 2, 1),
('list', 'w', 'x'),
('dict', 'w', 1)]:
p[k] = vin
assert p[k] == vout
def test_data_race():
p = pt.Parameter.create(name='int', type='int', value=0)
p.sigValueChanged.connect(lambda: p.setValue(1)) # connect signal before adding to tree to ensure priority
t = pt.ParameterTree()
t.setParameters(p)
pi = t.nextFocusableChild(t.invisibleRootItem())
pi.widget.setValue(2)
assert p.value() == pi.widget.value() == 1

View File

@ -360,10 +360,9 @@ class SpinBox(QtGui.QAbstractSpinBox):
if not isinstance(value, D):
value = D(asUnicode(value))
changed = value != self.val
prev = self.val
self.val = value
prev, self.val = self.val, value
changed = not fn.eq(value, prev) # use fn.eq to handle nan
if update and (changed or not bounded):
self.updateText(prev=prev)
@ -381,7 +380,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
def delayedChange(self):
try:
if self.val != self.lastValEmitted:
if not fn.eq(self.val, self.lastValEmitted): # use fn.eq to handle nan
self.emitChanged()
except RuntimeError:
pass ## This can happen if we try to handle a delayed signal after someone else has already deleted the underlying C++ object.