From 9a10fee6d15659f8266a58004dde68d3c4d6c131 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Fri, 2 May 2014 15:38:37 +0100
Subject: [PATCH] Ok, now the PVL (that's 'PropertyValueList' for the
 uninitiated) is pretty tight. I think.

---
 fsl/props/properties_value.py | 190 +++++++++++++++++++---------------
 fsl/props/widgets_list.py     | 104 ++++++++++++-------
 2 files changed, 169 insertions(+), 125 deletions(-)

diff --git a/fsl/props/properties_value.py b/fsl/props/properties_value.py
index 2218fbc2f..83c2bb43d 100644
--- a/fsl/props/properties_value.py
+++ b/fsl/props/properties_value.py
@@ -1,6 +1,9 @@
 #!/usr/bin/env python
 #
-# properties_value.py -
+# properties_value.py - Definitions of the PropertyValue and
+#                       PropertyValueList classes.
+#
+# These definitions are a part of properties.py.
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
@@ -69,7 +72,8 @@ class PropertyValue(object):
 
         """
         
-        if name is None: name = 'PropertyValue_{}'.format(id(self))
+        if name     is     None: name  = 'PropertyValue_{}'.format(id(self))
+        if castFunc is not None: value = castFunc(value)
         
         self._context         = context
         self._validate        = validateFunc
@@ -134,7 +138,8 @@ class PropertyValue(object):
         # Check to see if the new value is valid
         valid = False
         try:
-            self._validate(self._context, newValue)
+            if self._validate is not None:
+                self._validate(self._context, newValue)
             valid = True
 
         except ValueError as e:
@@ -226,15 +231,12 @@ class PropertyValueList(PropertyValue):
     registered on individual items (accessible via the
     getPropertyValueList method), or on the entire list.
 
-    This code hurts my head, as its a little bit complicated. The value
+    This code hurts my head, as it's a bit complicated. The __value
     encapsulated by this PropertyValue object (a PropertyValueList is
     also a PropertyValue) is just the list of raw values.  Alongside this,
     a separate list is maintained, which contains PropertyValue objects.
     Whenever a list-modifying operation occurs on this PropertyValueList
     (which also acts a bit like a Python list), both lists are updated.
-
-    Modifications of the list of PropertyValue objecs occurs exclusively
-    in the __setitem__ method.
     """
 
     def __init__(self,
@@ -253,15 +255,33 @@ class PropertyValueList(PropertyValue):
         """
         if name is None: name = 'PropertyValueList_{}'.format(id(self))
 
+        # On list modifications, validate both the
+        # list, and all of the items separately
+        def validateFunc(self, newValues):
+            if listValidateFunc is not None:
+                listValidateFunc(context, newValues)
+            if itemValidateFunc is not None:
+                for value in newValues:
+                    itemValidateFunc(context, value)
+
+        # Cast each item separately
+        def castFunc(values):
+            if itemCastFunc is not None and values is not None:
+                return map(itemCastFunc, values)
+            return values
+
         PropertyValue.__init__(
             self,
             context,
             name=name,
-            validateFunc=listValidateFunc,
+            castFunc=castFunc,
+            validateFunc=validateFunc,
             allowInvalid=listAllowInvalid,
             preNotifyFunc=preNotifyFunc,
             postNotifyFunc=postNotifyFunc)
 
+        # These attributes are passed to the PropertyValue
+        # constructor whenever a new item is added to the list
         self._itemCastFunc     = itemCastFunc
         self._itemValidateFunc = itemValidateFunc
         self._itemAllowInvalid = itemAllowInvalid
@@ -269,46 +289,49 @@ class PropertyValueList(PropertyValue):
         # The list of PropertyValue objects.
         self.__propVals = []
 
+        # initialise the list
         if values is not None:
             self.set(values)
 
-
-    def _notify(self): pass
-
+        
+    def getPropertyValueList(self):
+        """
+        Return (a copy of) the underlying property value list, allowing
+        access to the PropertyValue objects which manage each list item.
+        """
+        return list(self.__propVals)
+ 
+        
     def get(self):
+        """
+        Overrides PropertyValue.get(). Returns this PropertyValueList
+        object.
+        """
         return self
-        
-    def set(self, newValue):
+
+
+    def set(self, newValues, recreate=True):
         """
+        Overrides PropertyValue.set(). Sets the values stored in this
+        PropertyValue list.  If the recreate parameter is True (default)
+        all of the PropertyValue objects managed by this PVL object are
+        discarded, and new ones recreated. This flag is intended for
+        internal use only.
         """
 
-        # Update the list of PropertyValue objects
-        # (this triggers a call to __setitem__)
-        self[:] = newValue
+        PropertyValue.set(self, newValues)
 
-        # Validate the list as a whole
-        PropertyValue.set(self, self[:])
- 
-        # Notify listeners on this PropertyValueList object
-        PropertyValue._notify(self)
+        if recreate: 
+            self.__propVals = map(self.__newItem, newValues)
 
-        
-    def getPropertyValueList(self):
 
+    def revalidate(self):
         """
-        Return (a copy of) the underlying property value list, allowing
-        access to the PropertyValue objects which manage each list item.
+        Overrides PropertyValue.revalidate(). Revalidates the values in
+        this list, ensuring that the corresponding PropertyValue objects
+        are not recreated.
         """
-        return list(self.__propVals)
-
-        
-    def __len__(self): return self.__propVals.__len__()
-    
-    def __repr__(self):
-        return list([i.get() for i in self.__propVals]).__repr__()
-        
-    def __str__(self):
-        return list([i.get() for i in self.__propVals]).__str__()
+        self.set(self.get(), False)
 
         
     def __newItem(self, item):
@@ -317,29 +340,35 @@ class PropertyValueList(PropertyValue):
         given item in a PropertyValue object.
         """
 
-        # TODO prenotify to validate entire list
-        # whenever an item is changed? This would
-        # be required for a list validation function
-        # which depends on the values of the list,
-        # in addition to its length. And without it,
-        # a list could get into the state where the
-        # list as a whole is valid, even if individual
-        # property values contained within are not.
+        # The only interesting thing here is the postNotifyFunc -
+        # whenever a PropertyValue in this list changes, the entire
+        # list is revalidated. This is primarily to ensure that
+        # list-listeners are notified of changes to individual list
+        # elements.
         propVal = PropertyValue(
             self._context,
             name='{}_Item'.format(self._name),
+            value=item,
             castFunc=self._itemCastFunc,
+            postNotifyFunc=lambda *a: self.revalidate(),
             validateFunc=self._itemValidateFunc,
             allowInvalid=self._itemAllowInvalid)
-
-        # Explicitly set the initial value  so, if
-        # itemAllowInvalid is False, a ValueError
-        # will be raised
-        propVal.set(item)
         
         return propVal
 
         
+    def __len__(self):
+        return self.__propVals.__len__()
+
+        
+    def __repr__(self):
+        return list([i.get() for i in self.__propVals]).__repr__()
+
+        
+    def __str__(self):
+        return list([i.get() for i in self.__propVals]).__str__()
+
+        
     def index(self, item):
         """
         Returns the first index of the value, or a ValueError if the
@@ -406,9 +435,13 @@ class PropertyValueList(PropertyValue):
         raised if the insertion would causes the list to grow beyond its
         maximum length.
         """
+
         listVals = self[:]
         listVals.append(item)
-        self.set(listVals)
+        self.set(listVals, False)
+        
+        propVal = self.__newItem(item)
+        self.__propVals.append(propVal)
 
 
     def extend(self, iterable):
@@ -417,7 +450,12 @@ class PropertyValueList(PropertyValue):
         list.  An IndexError is raised if an insertion would causes
         the list to grow beyond its maximum length.
         """
-        self.set(self[:].extend(iterable))
+        listVals = self[:]
+        listVals.extend(iterable)
+        self.set(listVals, False) 
+        
+        propVals = [self.__newItem(item) for item in iterable]
+        self.__propVals.extend(propVals)
 
         
     def pop(self, index=-1):
@@ -427,50 +465,30 @@ class PropertyValueList(PropertyValue):
         to shrink below its minimum length.
         """
         listVals = self[:]
-        val = listVals.pop(index)
-        self.set(listVals)
-        return val
+        listVals.pop(index)
+        self.set(listVals, False)
+
+        propVal = self.__propVals.pop(index)
+        return propVal.get()
 
 
-    def __setitem__(self, key, values):
+    def __setitem__(self, key, value):
         """
         Sets the value(s) of the list at the specified index/slice.
         """
 
         if isinstance(key, slice):
-            if (key.step is not None) and (key.step > 1):
-                raise ValueError(
-                    'PropertyValueList does not support extended slices')
-            indices = range(*key.indices(len(self)))
-
-        elif isinstance(key, int):
-            indices = [key]
-            values  = [values]
+            raise ValueError(
+                'PropertyValueList does not support extended slices')
 
         else:
             raise ValueError('Invalid key type')
 
-        # if the number of indices specified in the key
-        # is different from the number of values passed
-        # in, it means that we are either adding or
-        # removing items from the list
-        lenDiff = len(values) - len(indices)
-        oldLen  = len(self)
-        newLen  = oldLen + lenDiff
-
-        # Replace values of existing items
-        if newLen == oldLen:
-            for i, v in zip(indices, values):
-
-                # fail if value is bad
-                self.__propVals[i].set(v)
-
-        # Replace old PropertyValue objects with new ones. 
-        else:
-
-            # fail if values are bad
-            propVals = [self.__newItem(v) for v in values]
-            self.__propVals.__setitem__(key, propVals)
+        listVals = self[:]
+        listVals[key] = value
+        self.set(listVals, False)
+        
+        self.__propVals[key].set(value)
 
         
     def __delitem__(self, key):
@@ -480,5 +498,7 @@ class PropertyValueList(PropertyValue):
         shrink below its minimum length.
         """
         listVals = self[:]
-        listVals.__delitem(key)
-        self.set(listVals)
+        listVals.__delitem__(key)
+        self.set(listVals, False)
+
+        self.__propVals.__delitem__(key)
diff --git a/fsl/props/widgets_list.py b/fsl/props/widgets_list.py
index 4733aa5c9..4ab397bfb 100644
--- a/fsl/props/widgets_list.py
+++ b/fsl/props/widgets_list.py
@@ -8,14 +8,10 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 
-import sys
-import os
-
 import wx
 
 import widgets
 
-
 def _pasteDataDialog(parent, hasProps, propObj):
     """
     Displays a dialog containing an editable text field, allowing the
@@ -28,13 +24,18 @@ def _pasteDataDialog(parent, hasProps, propObj):
       - propObj:  The props.List property object
     """
 
-    listObj  = getattr(hasProps, propObj.label)
+    listObj  = getattr(hasProps, propObj._label)
     initText = '\n'.join([str(l).strip() for l in listObj])
  
-    frame = wx.Frame(parent)
+    frame = wx.Dialog(parent,
+                      style=wx.DEFAULT_DIALOG_STYLE |
+                            wx.RESIZE_BORDER)
     panel = wx.Panel(frame)
-    text  = wx.TextCtrl(panel, value=text,
-                        style=wx.MULTILINE | wx.VSCROLL | wx.HSCROLL)
+    text  = wx.TextCtrl(panel, value=initText,
+                        style=wx.TE_MULTILINE | 
+                              wx.TE_DONTWRAP  | 
+                              wx.VSCROLL      | 
+                              wx.HSCROLL)
 
     # ok/cancel buttons
     okButton     = wx.Button(panel, label="Ok")
@@ -44,11 +45,13 @@ def _pasteDataDialog(parent, hasProps, propObj):
 
     panel.SetSizer(sizer)
 
-    sizer.Add(text,         pos=(0,0), span=(1,2), flag=wx.EXPAND)
-    sizer.Add(okButton,     pos=(1,0), span=(1,1), flag=wx.EXPAND)
-    sizer.Add(cancelButton, pos=(1,1), span=(1,1), flag=wx.EXPAND)
+    sizer.Add(text,         pos=(0, 0), span=(1, 2), flag=wx.EXPAND)
+    sizer.Add(okButton,     pos=(1, 0), span=(1, 1), flag=wx.EXPAND)
+    sizer.Add(cancelButton, pos=(1, 1), span=(1, 1), flag=wx.EXPAND)
 
     sizer.AddGrowableRow(0)
+    sizer.AddGrowableCol(0)
+    sizer.AddGrowableCol(1)
 
     def pasteIntoList():
         """
@@ -61,12 +64,15 @@ def _pasteDataDialog(parent, hasProps, propObj):
         listData = listData.split('\n')
         listData = [s.strip() for s in listData]
 
-        setattr(hasProps, propObj.label, listData)
+        setattr(hasProps, propObj._label, listData)
         frame.Close()
 
     okButton    .Bind(wx.EVT_BUTTON, lambda e: pasteIntoList())
     cancelButton.Bind(wx.EVT_BUTTON, lambda e: frame.Close())
 
+    panel.Fit()
+    panel.Layout()
+
     frame.ShowModal()
 
 
@@ -76,9 +82,9 @@ def _editListDialog(parent, hasProps, propObj):
     which allows the user to adjust the number of items in the list.
     """
 
-    # listObj is a properties.ListWrapper object
+    # listObj is a properties_values.PropertyValueList object
     listObj  = getattr(hasProps, propObj._label)
-    listType = propObj.listType
+    listType = propObj._listType
 
     # Get a reference to a function in the widgets module,
     # which can make individual widgets for each list item
@@ -94,34 +100,48 @@ def _editListDialog(parent, hasProps, propObj):
     minval = propObj.getConstraint(hasProps, 'minlen')
     maxval = propObj.getConstraint(hasProps, 'maxlen')
 
-    if minval is None: minval = 1
+    if minval is None: minval = 0
     if maxval is None: maxval = 2 ** 31 - 1
 
-    frame       = wx.Dialog(parent)
-    panel       = wx.ScrolledWindow(frame)
-    okButton    = wx.Button(frame, label='Ok')
-    numRowsBox  = wx.SpinCtrl(frame,
-                              min=minval,
-                              max=maxval,
-                              initial=len(listObj))
+    frame        = wx.Dialog(parent,
+                             style=wx.DEFAULT_DIALOG_STYLE |
+                                   wx.RESIZE_BORDER)
+    numRowsPanel = wx.Panel(frame)
+    entryPanel   = wx.ScrolledWindow(frame)
+    okButton     = wx.Button(frame, label='Ok')
+
+    # Spin box (and label) to adjust the
+    # number of entries in the list
+    numRowsSizer = wx.BoxSizer(wx.HORIZONTAL)
+    numRowsPanel.SetSizer(numRowsSizer)
+    
+    numRowsLabel = wx.StaticText(numRowsPanel, label='Number of entries')
+    numRowsCtrl  = wx.SpinCtrl(numRowsPanel,
+                               min=minval,
+                               max=maxval,
+                               initial=len(listObj))
+
+    numRowsSizer.Add(numRowsLabel, flag=wx.EXPAND, proportion=1)
+    numRowsSizer.Add(numRowsCtrl,  flag=wx.EXPAND, proportion=1)
+    
     listWidgets = []
 
     # Make a widget for every element in the list
-    for i in range(len(listObj)):
-        propVal = propObj.getPropVal(hasProps, i)
-        widget  = makeFunc(panel, hasProps, listType, propVal)
+    propVals = listObj.getPropertyValueList()
+    for propVal in propVals:
+        widget  = makeFunc(entryPanel, hasProps, listType, propVal)
         listWidgets.append(widget)
 
     frameSizer = wx.BoxSizer(wx.VERTICAL)
     frame.SetSizer(frameSizer)
-    frameSizer.Add(numRowsBox, flag=wx.EXPAND)
-    frameSizer.Add(panel,      flag=wx.EXPAND, proportion=1)
-    frameSizer.Add(okButton,   flag=wx.EXPAND)
+    frameSizer.Add(numRowsPanel, flag=wx.EXPAND)
+    frameSizer.Add(entryPanel,   flag=wx.EXPAND, proportion=1)
+    frameSizer.Add(okButton,     flag=wx.EXPAND)
 
-    panelSizer = wx.BoxSizer(wx.VERTICAL)
-    panel.SetSizer(panelSizer)
-    for i in range(len(listWidgets)):
-        panelSizer.Add(listWidgets[i], flag=wx.EXPAND)
+    entryPanelSizer = wx.BoxSizer(wx.VERTICAL)
+    entryPanel.SetSizer(entryPanelSizer)
+    for lw in listWidgets:
+        entryPanelSizer.Add(lw, flag=wx.EXPAND)
 
 
     def changeNumRows(*args):
@@ -131,20 +151,20 @@ def _editListDialog(parent, hasProps, propObj):
         list, and corresponding widget from the window.
         """
 
-        oldLen = len(listObj)
-        newLen = numRowsBox.GetValue()
+        oldLen   = len(listObj)
+        newLen   = numRowsCtrl.GetValue()
 
         # add rows
         while oldLen < newLen:
 
             # add a new element to the list
             listObj.append(listType._default)
-            propVal = propObj.getPropVal(hasProps, -1)
+            propVal = listObj.getPropertyValueList()[-1]
 
             # add a widget
-            widg = makeFunc(frame, hasProps, listType, propVal)
+            widg = makeFunc(entryPanel, hasProps, listType, propVal)
             listWidgets.append(widg)
-            panelSizer.Add(widg, flag=wx.EXPAND)
+            entryPanelSizer.Add(widg, flag=wx.EXPAND)
  
             oldLen = oldLen + 1
 
@@ -156,13 +176,17 @@ def _editListDialog(parent, hasProps, propObj):
 
             # kill the widget
             widg = listWidgets.pop()
-            widg.Destroy()
 
-            # TODO Remove widget listener from property value ...
+            entryPanelSizer.Remove(widg)
+            
+            widg.Destroy()
 
             oldLen = oldLen - 1
+        entryPanel.Layout()
+        entryPanel.Refresh()
+        
 
-    numRowsBox.Bind(wx.EVT_SPINCTRL, changeNumRows)
+    numRowsCtrl.Bind(wx.EVT_SPINCTRL, changeNumRows)
     okButton.Bind(wx.EVT_BUTTON, lambda e: frame.Close())
 
     frame.ShowModal()
-- 
GitLab