diff --git a/fsl/props/properties_value.py b/fsl/props/properties_value.py index 2218fbc2fe183d225c1381eb4fc6141450f00f92..83c2bb43d8124289b1cac659efc90de95835ddd3 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 4733aa5c98be5ea4de6c7c6a268d0d776b47bb84..4ab397bfb0fdea219a46d2cd87ab7c67eaf138ea 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()