From d11db0732c4c4f36370b642120c92934a3da09f1 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Fri, 14 Mar 2014 16:55:10 +0000
Subject: [PATCH] Updated visible/enabledWhen callback structure. Now there is
 no need to specify the conditional properties - just a function which takes a
 reference to the HasProperties owner. Need to do some commenting and cleaning
 up before merging back to master.

---
 featDemo.py          |   1 +
 testBuild.py         |   2 +-
 tkprop/build.py      | 203 ++++++++++++++++++++++++++-----------------
 tkprop/properties.py |   6 --
 4 files changed, 124 insertions(+), 88 deletions(-)

diff --git a/featDemo.py b/featDemo.py
index 03ed3d751..1809eed4a 100644
--- a/featDemo.py
+++ b/featDemo.py
@@ -240,6 +240,7 @@ featView = tkp.NotebookGroup((
             'b0Unwarping',
             tkp.VGroup(
                 label='B0 Unwarping options',
+                visibleWhen=lambda i: i.b0Unwarping,
                 children=(
                     'b0_fieldmap',
                     'b0_fieldmapMag',
diff --git a/testBuild.py b/testBuild.py
index 9079f1f91..fa3e676d0 100644
--- a/testBuild.py
+++ b/testBuild.py
@@ -135,7 +135,7 @@ betView = tkp.NotebookGroup((
             tkp.Widget('fractionalIntensity', label=optNames['fractionalIntensity']),
             tkp.Widget('runChoice',           label=optNames['runChoice']),
             tkp.Widget('t2Image',             label=optNames['t2Image'],
-                       visibleWhen=('runChoice', lambda v: v.startswith('As above')))
+                       visibleWhen=lambda i: i.runChoice.startswith('As above'))
         )),
     tkp.VGroup(
         label='Advanced options',
diff --git a/tkprop/build.py b/tkprop/build.py
index 4d2da33c1..2a01cc8f9 100644
--- a/tkprop/build.py
+++ b/tkprop/build.py
@@ -68,18 +68,6 @@ class ViewItem(object):
                          between enabled and disabled.
         """
         
-        if visibleWhen is not None:
-            props, func = visibleWhen
-            if isinstance(props, str):
-                props = [props]
-            visibleWhen = (props, func)
-
-        if enabledWhen is not None:
-            props, func = enabledWhen
-            if isinstance(props, str):
-                props = [props]
-            enabledWhen = (props, func)
-
         self.key         = key
         self.label       = label
         self.tooltip     = tooltip
@@ -98,6 +86,28 @@ class Button(ViewItem):
         ViewItem.__init__(self, key, **kwargs)
 
 
+class Label(ViewItem):
+    """
+    Represents a static text label.
+    """
+    def __init__(self, viewItem=None, **kwargs):
+        """
+        A Label object may either be created in the same way as any other
+        ViewItem object, or it may be created from another ViewItem object,
+        the object to be lablled.
+        """
+
+        if viewItem is not None:
+            kwargs['key']         = '{}_label'.format(viewItem.key)
+            kwargs['label']       = viewItem.label
+            kwargs['tooltip']     = viewItem.tooltip
+            kwargs['visibleWhen'] = viewItem.enabledWhen
+            kwargs['enabledWhen'] = viewItem.enabledWhen
+            
+        ViewItem.__init__(self, **kwargs)
+    pass
+
+
 class Widget(ViewItem):
     """
     Represents a low level widget which is used to modify a property value.
@@ -128,7 +138,10 @@ class Group(ViewItem):
                         Group.
         
           - showLabels: Whether labels should be displayed for each of
-                        the children.
+                        the children. If this is true, an attribute will
+                        be added to this Group object in the _prepareView
+                        function, called 'childLabels', and containing a
+                        Label object for each child.
         
           - kwargs:     Passed to the ViewItem constructor.
         """
@@ -161,16 +174,25 @@ class VGroup(Group):
     pass
 
 
-def _configureEnabledWhen(viewItem, tkObj, propObj, tkLabel=None):
+class PropGUI(object):
+    """
+    A container class used for convenience. Stores references to
+    all Tkinter/ttk objects that are created, and to all conditional
+    callbacks (which control visibility/state).
+    """
+    
+    def __init__(self):
+        self.onChangeCallbacks = []
+        self.tkObjects         = {}
+ 
+
+def _configureEnabledWhen(viewItem, tkObj, propObj):
     """
     Sets up event handling for enabling/disabling the Tkinter object
     represented by the given viewItem.
     """
 
-    if viewItem.enabledWhen is None: return
-
-    condProps, condFunc = viewItem.enabledWhen
-    tkVars = [getattr(propObj, '{}_tkVar'.format(p)) for p in condProps]
+    if viewItem.enabledWhen is None: return None
 
     def _changeState(obj, state):
         """
@@ -185,52 +207,34 @@ def _configureEnabledWhen(viewItem, tkObj, propObj, tkLabel=None):
             for child in obj.winfo_children():
                 _changeState(child, state)
     
-    def _toggleEnabled(*a):
+    def _toggleEnabled():
         """
         Calls the conditional function and enables/disables
         the tk object (and its label if there is one).
         """
 
-        varVals = [tkVar.get() for tkVar in tkVars]
-
-        if condFunc(*varVals): state = 'enabled'
-        else:                  state = 'disabled'
+        if viewItem.enabledWhen(propObj): state = 'enabled'
+        else:                             state = 'disabled'
         
         _changeState(tkObj, state)
-        if tkLabel is not None: _changeState(tkLabel, state)
 
-    # set up initial state
-    _toggleEnabled()
-    
-    for tkVar in tkVars: tkVar.trace('w', _toggleEnabled)
+    return _toggleEnabled
 
 
-def _configureVisibleWhen(viewItem, tkObj, propObj, tkLabel=None):
+def _configureVisibleWhen(viewItem, tkObj, propObj):
     """
     Sets up event handling for showing/hiding the Tkinter object
     represented by the given viewItem.
     """ 
 
-    if viewItem.visibleWhen is None: return
+    if viewItem.visibleWhen is None: return None
 
-    condProps, condFunc = viewItem.visibleWhen
-    tkVars = [getattr(propObj, '{}_tkVar'.format(p)) for p in condProps]
+    def _toggleVis():
 
-    def _toggleVis(*a):
+        if not viewItem.visibleWhen(propObj): tkObj.grid_remove()
+        else:                                 tkObj.grid()
 
-        varVals = [tkVar.get() for tkVar in tkVars]
-
-        if not condFunc(*varVals):
-            tkObj.grid_remove()
-            if tkLabel is not None: tkLabel.grid_remove()
-        else:
-            tkObj.grid()
-            if tkLabel is not None: tkLabel.grid()
-
-    # set up initial visibility
-    _toggleVis()
-    
-    for tkVar in tkVars: tkVar.trace('w', _toggleVis)
+    return _toggleVis
 
 
 def _createLabel(parent, viewItem, propObj):
@@ -239,10 +243,7 @@ def _createLabel(parent, viewItem, propObj):
     viewItem.
     """
 
-    if isinstance(viewItem, str): labelText = viewItem
-    else:                         labelText = viewItem.label
-
-    label = ttk.Label(parent, text=labelText)
+    label = ttk.Label(parent, text=viewItem.label)
     return label
 
 
@@ -266,7 +267,7 @@ def _createWidget(parent, widget, propObj):
     return tkWidget
 
     
-def _createNotebookGroup(parent, group, propObj):
+def _createNotebookGroup(parent, group, propObj, propGui):
     """
     Creates a ttk.Notebook object for the given NotebookGroup object.
     The children of the group object are also created via recursive
@@ -277,7 +278,7 @@ def _createNotebookGroup(parent, group, propObj):
 
     for child in group.children:
 
-        page = _create(notebook, child, propObj)
+        page = _create(notebook, child, propObj, propGui)
         page.pack(fill=tk.X, expand=1)
         
         notebook.add(page, text=child.label)
@@ -327,7 +328,7 @@ def _layoutGroup(group, parent, children, labels):
                 children[cidx].grid(row=cidx, column=0, sticky=tk.E+tk.W)
     
 
-def _createGroup(parent, group, propObj):
+def _createGroup(parent, group, propObj, propGui):
     """
     Creates a ttk.Frame object for the given group. Children of the
     group are recursively created via calls to _create, and laid out on
@@ -343,58 +344,56 @@ def _createGroup(parent, group, propObj):
 
     for i,child in enumerate(group.children):
 
-        if isinstance(child, str):
-            child = Widget(child)
-            group.children[i] = child
-
         labelObj = None
         if group.showLabels:
-            labelObj = _createLabel(frame, child, propObj)
+            labelObj = _create(frame, group.childLabels[i], propObj, propGui)
             labelObjs.append(labelObj)
 
-        childObj = _create(frame, child, propObj)
+        childObj = _create(frame, child, propObj, propGui)
 
         childObjs.append(childObj)
 
     _layoutGroup(group, frame, childObjs, labelObjs)
 
-    # set up widget events after they have been
-    # laid out, so their initial state (e.g.
-    # visible, enabled, etc) is correct
-    for cidx in range(len(childObjs)):
-
-        child    = group.children[cidx]
-        childObj = childObjs[cidx]
-
-        if labelObjs is not None: labelObj = labelObjs[cidx]
-        else:                     labelObj = None
-
-        _configureVisibleWhen(child, childObj, propObj, labelObj)
-        _configureEnabledWhen(child, childObj, propObj, labelObj) 
-
     return frame
 
 
-def _create(parent, viewItem, propObj):
+def _create(parent, viewItem, propObj, propGui):
     """
     Creates the given ViewItem object and, if it is a group, all of its
     children.
     """
 
+    # replace with an introspective lookup
+
     if isinstance(viewItem, Widget):
-        return _createWidget(parent, viewItem, propObj)
+        tkObject = _createWidget(parent, viewItem, propObj)
+
+    elif isinstance(viewItem, Label):
+        tkObject = _createLabel(parent, viewItem, propObj)
 
     elif isinstance(viewItem, Button):
-        return _createButton(parent, viewItem)
+        tkObject = _createButton(parent, viewItem)
         
     elif isinstance(viewItem, NotebookGroup):
-        return _createNotebookGroup(parent, viewItem, propObj)
+        tkObject = _createNotebookGroup(parent, viewItem, propObj, propGui)
         
     elif isinstance(viewItem, Group):
-        return _createGroup(parent, viewItem, propObj)
+        tkObject = _createGroup(parent, viewItem, propObj, propGui)
+
+    else:
+        raise ValueError('Unrecognised ViewItem: {}'.format(
+            viewItem.__class__.__name__))
+
+    visibleCb = _configureVisibleWhen(viewItem, tkObject, propObj)
+    enableCb  = _configureEnabledWhen(viewItem, tkObject, propObj)
+
+    if visibleCb is not None: propGui.onChangeCallbacks.append(visibleCb)
+    if enableCb  is not None: propGui.onChangeCallbacks.append(enableCb)
 
-    raise ValueError('Unrecognised ViewItem: {}'.format(
-        viewItem.__class__.__name__)) 
+    propGui.tkObjects[viewItem.key] = tkObject
+
+    return tkObject
 
 
 def _defaultView(propObj):
@@ -442,8 +441,42 @@ def _prepareView(viewItem, labels, tooltips):
         for i,child in enumerate(viewItem.children):
             viewItem.children[i] = _prepareView(child, labels, tooltips)
 
+        if viewItem.showLabels:
+            viewItem.childLabels = []
+
+            for child in viewItem.children:
+                viewItem.childLabels.append(Label(child))
+
     return viewItem
 
+def _prepareEvents(propObj, propGui):
+    """
+    """
+
+    if len(propGui.onChangeCallbacks) == 0:
+        return
+
+    def onChange(*a):
+        for cb in propGui.onChangeCallbacks:
+            cb()
+
+    propDict = propObj.__class__.__dict__
+    
+    props = filter(
+        lambda (name,prop): isinstance(prop, tkp.PropertyBase),
+        propDict.items())
+    
+    propNames,props = zip(*props)
+
+    tkVars = [getattr(propObj, '{}_tkVar'.format(name)) for name in propNames]
+
+    # initialise widget states
+    onChange()
+
+    for t in tkVars:
+        if not isinstance(t, tk.Variable): continue
+        t.trace('w', onChange)
+
 
 def buildGUI(parent, propObj, view=None, labels=None, tooltips=None):
     """
@@ -467,11 +500,19 @@ def buildGUI(parent, propObj, view=None, labels=None, tooltips=None):
      - tooltips: Dict specifying tooltips
     """
 
+
+
     if view is None: view = _defaultView(propObj)
 
     if labels   is None: labels   = {}
     if tooltips is None: tooltips = {}
 
     view = _prepareView(view, labels, tooltips)
+
+    propGui = PropGUI()
         
-    return _create(parent, view, propObj)
+    topLevel = _create(parent, view, propObj, propGui)
+
+    _prepareEvents(propObj, propGui)
+
+    return topLevel
diff --git a/tkprop/properties.py b/tkprop/properties.py
index 812f5497e..6a4ba4d32 100644
--- a/tkprop/properties.py
+++ b/tkprop/properties.py
@@ -93,12 +93,6 @@ import Tkinter as tk
 #      the new value is valid. If the new value is not valid,
 #      a ValueError is raised.
 #
-# NOTE TO SELF: Another way that I could provide this callback
-# functionality is to simply use the trace feature of Tkinter
-# variables. I'd need to come up with some sort of mechanism
-# for reverting to a previous value for invalid writes, though.
-# Better? Worse? I don't know.
-#
 class _StringVar(tk.StringVar):
     def __init__(self, tkProp, instance, **kwargs):
         self.tkProp   = tkProp
-- 
GitLab