Skip to content
Snippets Groups Projects
Commit 22626bb6 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Got rid of lastValue/revert functionality. Changed the structure of...

Got rid of lastValue/revert functionality. Changed the structure of validation/notification so that programmatic notification is a bit safer, and should not result in infinitely recursive callbacks. Now, whenever a property value changes, the HasProperties instance is notified; it then forces validation/notification for all other properties. This is done because the validity of one property may be dependent upon the value of another. Also, added ability in build.py for buttons to be added to interface.
parent 0a749182
No related branches found
No related tags found
No related merge requests found
......@@ -12,8 +12,8 @@
# accepts as parameters a tk object to be used as the parent (e.g. a
# root or Frame object), a tkprop.HasProperties object, an optional
# ViewItem object, which specifies how the interface is to be laid
# out, and two optional dictionaries for passing in labels and
# tooltips.
# out, two optional dictionaries for passing in labels and tooltips,
# and another optional dictionary for any buttons to be added.
#
# The view parameter allows the layout of the generated interface to
# be customised. Property widgets may be grouped together by embedding
......@@ -592,7 +592,12 @@ def _prepareEvents(propObj, propGui):
prop.addListener(propObj, lName, onChange)
def buildGUI(parent, propObj, view=None, labels=None, tooltips=None):
def buildGUI(parent,
propObj,
view=None,
labels=None,
tooltips=None,
buttons=None):
"""
Builds a Tkinter/ttk interface which allows the properties of the
given propObj object (a tkprop.HasProperties instance) to be edited.
......@@ -609,6 +614,12 @@ def buildGUI(parent, propObj, view=None, labels=None, tooltips=None):
- view: ViewItem object, specifying the interface layout
- labels: Dict specifying labels
- tooltips: Dict specifying tooltips
- buttons: Dict specifying buttons to add to the interface.
Keys are used as button labels, and values are
callback functions which take two arguments - the
Tkinter parent object, and the HasProperties
object (parent and propObj). Make sure to use a
collections.OrderedDict if order is important.
"""
if view is None: view = _defaultView(propObj)
......@@ -617,8 +628,30 @@ def buildGUI(parent, propObj, view=None, labels=None, tooltips=None):
if tooltips is None: tooltips = {}
propGui = PropGUI()
view = _prepareView(view, labels, tooltips)
topLevel = _create(parent, view, propObj, propGui)
view = _prepareView(view, labels, tooltips)
# If any buttons were specified, the properties
# interface is embedded in a higher level frame,
# along with the buttons
if len(buttons) > 0:
topLevel = ttk.Frame(parent)
propFrame = _create(topLevel, view, propObj, propGui)
topLevel.rowconfigure( 0, weight=1)
topLevel.columnconfigure(0, weight=1)
topLevel.columnconfigure(1, weight=1)
propFrame.grid(row=0, column=0, columnspan=2,
sticky=tk.N+tk.S+tk.E+tk.W)
for i,(label,callback) in enumerate(buttons.items()):
button = ttk.Button(topLevel, text=label, command=callback)
button.grid(row=1, column=i, sticky=tk.N+tk.S+tk.E+tk.W)
else:
topLevel = _create(parent, view, propObj, propGui)
_prepareEvents(propObj, propGui)
......
......@@ -163,7 +163,6 @@ class TkVarProxy(object):
self.tkVarType = tkVarType
self.tkProp = tkProp
self.owner = owner
self.lastValue = value
self.changeListeners = {}
self.name = name
self.tkVar = tkVarType(value=value, name=name)
......@@ -173,11 +172,13 @@ class TkVarProxy(object):
def addListener(self, name, callback):
"""
Adds a listener for this variable. When the variable value
changes, the listener callback function is called. The
callback function must accept these arguments:
changes, the listener callback function is called. Listener
notification may also be programmatically triggered via the
PropertyBase.forceValidation method. The callback function
must accept these arguments:
value - The new property value
valid - Whether the new value is valid or invalid
value - The property value
valid - Whether the value is valid or invalid
instance - The HasProperties instance
tkProp - The PropertyBase instance
name - The name of this TkVarProxy
......@@ -202,25 +203,13 @@ class TkVarProxy(object):
self.changeListeners[instance].pop(name, None)
def _traceCb(self, *args):
def _getVarValue(self):
"""
Called whenever the Tkinter control variable value is changed.
The PropertyBase.validate() method is called on the parent
property of this TkVarProxy object. If this validate method
does not raise an error, the new value is stored as the last
known good value. If the validate method does raise an error,
the last known good value is not changed. The variable can
be reverted to its last known good value via the revert
method.
After the PropertyBase.validate method is called, any registered
listeners are notified of the variable value change.
Returns the current value of the Tkinter control
variable being managed by this TkVarProxy object.
"""
valid = True
listeners = self.changeListeners.items()
# This is silly. Tkinter allows Boolean/Int/Double
# variables to be set to invalid values (e.g. it
# allows DoubleVars to be set to strings containing
......@@ -229,12 +218,12 @@ class TkVarProxy(object):
# the invalid value to a boolean/int/double. So here
# we attempt to get the current value in the normal
# way ...
try: newValue = self.tkVar.get()
try: value = self.tkVar.get()
# and if that fails, we manually look up the value
# via the current tk context, thus avoiding the
# failing type cast. Ugly.
except: newValue = self.tkVar._tk.globalgetvar(self.name)
except: value = self.tkVar._tk.globalgetvar(self.name)
# More silliness related to above silliness. All
# variables in Tk, be they IntVars, BooleanVars, or
......@@ -244,43 +233,55 @@ class TkVarProxy(object):
# which is quite different. So I'm following the
# convention that an empty string, for any of the
# variable types, is equivalent to None.
if newValue == '':
newValue = None
# print a log message if the value has changed
if newValue != self.lastValue:
log.debug(
'Variable {} changed: {} (valid: {}, {} listeners)'.format(
self.name, newValue, valid, len(listeners)))
# if the new value is valid, save
# it as the last known good value
try:
self.tkProp.validate(self.owner, newValue)
self.lastValue = newValue
except ValueError:
valid = False
# Notify all listeners about the change, ignoring
# any errors - it is up to the listeners to ensure
# that they handle invalid values
if value == '': value = None
return value
def validateAndNotify(self):
"""
Passes the current variable value to the validate()
method of the PropertyBase object which owns this
TkVarProxy, and then notifies any listeners which
have been registered with this TkVarProxy object.
"""
value = self._getVarValue()
valid = True
listeners = self.changeListeners.items()
try: self.tkProp.validate(self.owner, value)
except ValueError: valid = False
# Notify all listeners, ignoring any errors -
# it is up to the listeners to ensure that
# they handle invalid values
for (name,func) in listeners:
log.debug('Notifying listener on {}: {}'.format(self.name, name))
try: func(newValue, valid, self.owner, self.tkProp, self.name)
try: func(value, valid, self.owner, self.tkProp, self.name)
except Exception as e:
log.debug('Listener on {} ({}) raised exception: {}'.format(
self.name, name, e))
def revert(self):
def _traceCb(self, *args):
"""
Sets the Tk variable to its last known good value. This will
result in any registered listeners being notified of the change.
Called whenever the Tkinter control variable value is changed.
Notifies any registered listeners, and the HasProperties
property owner, of the change.
"""
self.tkVar.set(self.lastValue)
newValue = self._getVarValue()
log.debug('Variable {} changed: {}'.format(self.name, newValue))
# Validate the new value and notify any registered listeners
self.validateAndNotify()
# Notify the property owner that this property has changed
self.owner._propChanged(self.tkProp)
def __del__(self):
......@@ -337,7 +338,7 @@ class PropertyBase(object):
returns True or False.
- validateFunc: Custom validation function. Must accept
two parameters:a reference to the
two parameters: a reference to the
HasProperties instance, the owner of
this property; and the new property
value. Should raise a ValueError if the
......@@ -393,7 +394,7 @@ class PropertyBase(object):
varProxies = [varProxies]
for var in varProxies:
var._traceCb()
var.validateAndNotify()
def _varChanged(self, value, valid, instance, tkProp, name):
......@@ -487,7 +488,7 @@ class PropertyBase(object):
instval = instance.__dict__.get(self.label, None)
if instval is None: instval = self._makeTkVar(instance)
# See comments in TkVarProxy._traceCb
# See comments in TkVarProxy._getVarValue
# for a brief overview of this silliness.
try: val = instval.tkVar.get()
except: val = instval.tkVar._tk.globalgetvar(instval.tkVar._name)
......@@ -503,6 +504,8 @@ class PropertyBase(object):
the given value.
"""
# See comments in TkVarProxy._getVarValue
# for a brief overview of this silliness.
if value is None: value = ''
instval = self.getTkVar(instance)
......@@ -576,8 +579,8 @@ class HasProperties(object):
propNames, props = zip(*props)
return propNames, props
def validateAll(self):
"""
Validates all of the properties of this HasProperties object.
......@@ -603,6 +606,24 @@ class HasProperties(object):
return errors
def _propChanged(self, cProp):
"""
Called whenever any property value changes. Forces validation
for all other properties, and notification of their registered
listeners. This is done because the validity of some
properties may be dependent upon the values of others. So when
a particular property value changes, it may ahve changed the
validity of another property, meaning that the listeners of
the latter property need to be notified of this change in
validity.
"""
propNames, props = self.getAllProperties()
for prop in props:
if prop == cProp: continue
prop.forceValidation(self)
def __str__(self):
"""
Returns a multi-line string containing the names and values
......
......@@ -68,7 +68,11 @@ def _setupValidation(widget, propObj, tkProp, tkVar):
of the new value.
"""
_setBG(valid)
# We add a callback listener to the Tk variable, rather than
# to the property, as one property may be associated with
# multiple variables, and we don't want the widgets associated
# with those other variables to change background.
listenerName = 'ChangeBGOnValidate_{}'.format(tkVar.name)
tkVar.addListener(listenerName, _changeBGOnValidate)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment