]> git.saurik.com Git - wxWidgets.git/blobdiff - wxPython/wx/lib/masked/maskededit.py
Compile fixes for Panther
[wxWidgets.git] / wxPython / wx / lib / masked / maskededit.py
index c57038ed60b2f1dc9e7a6f19974e8ffeae9859fb..77776d7737079d9b66409fd2a730f11f17a4a8a1 100644 (file)
@@ -1,10 +1,10 @@
 #----------------------------------------------------------------------------
 # Name:         maskededit.py
 # Authors:      Jeff Childers, Will Sadkin
 #----------------------------------------------------------------------------
 # Name:         maskededit.py
 # Authors:      Jeff Childers, Will Sadkin
-# Email:        jchilders_98@yahoo.com, wsadkin@nameconnector.com
+# Email:        jchilders_98@yahoo.com, wsadkin@parlancecorp.com
 # Created:      02/11/2003
 # Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
 # Created:      02/11/2003
 # Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
-# Portions:     (c) 2002 by Will Sadkin, 2002-2003
+# Portions:     (c) 2002 by Will Sadkin, 2002-2006
 # RCS-ID:       $Id$
 # License:      wxWindows license
 #----------------------------------------------------------------------------
 # RCS-ID:       $Id$
 # License:      wxWindows license
 #----------------------------------------------------------------------------
@@ -50,7 +50,9 @@
 # o wxTimeCtrl -> TimeCtrl
 #
 
 # o wxTimeCtrl -> TimeCtrl
 #
 
-"""\
+__doc__ = """\
+contains MaskedEditMixin class that drives all the other masked controls.
+
 ====================
 Masked Edit Overview
 ====================
 ====================
 Masked Edit Overview
 ====================
@@ -88,11 +90,11 @@ masked.Ctrl:
     will be masked.TextCtrl.
 
     Each of the above classes has its own set of arguments, but masked.Ctrl
     will be masked.TextCtrl.
 
     Each of the above classes has its own set of arguments, but masked.Ctrl
-    provides a single "unified" interface for masked controls.  Those for
-    masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented
-    below; the others have their own demo pages and interface descriptions.
-    (See end of following discussion for how to configure the wx.MaskedCtrl()
-    to select the above control types.)
+    provides a single "unified" interface for masked controls.
+
+What follows is a description of how to configure the generic masked.TextCtrl
+and masked.ComboBox;  masked.NumCtrl and masked.TimeCtrl have their own demo 
+pages and interface descriptions.
 
 =========================
 
 
 =========================
 
@@ -110,8 +112,12 @@ mask
         a       Allow lowercase letters only
         C       Allow any letter, upper or lower
         X       Allow string.letters, string.punctuation, string.digits
         a       Allow lowercase letters only
         C       Allow any letter, upper or lower
         X       Allow string.letters, string.punctuation, string.digits
-        &       Allow string.punctuation only
-        \*       Allow any ansi character
+        &       Allow string.punctuation only (doesn't include all unicode symbols)
+        \*       Allow any visible character
+        |       explicit field boundary (takes no space in the control; allows mix
+                of adjacent mask characters to be treated as separate fields,
+                eg: '&|###' means "field 0 = '&', field 1 = '###'", but there's
+                no fixed characters in between.
     =========  ==========================================================
 
 
     =========  ==========================================================
 
 
@@ -127,8 +133,13 @@ mask
       import locale
       locale.setlocale(locale.LC_ALL, '')
 
       import locale
       locale.setlocale(locale.LC_ALL, '')
 
-    The controls now also support (by popular demand) all "ansi" chars,
-    that is, all ascii codes between 32 and 255, by use of the * mask character.
+    The controls now also support (by popular demand) all "visible" characters,
+    by use of the * mask character, including unicode characters above
+    the standard ANSI keycode range.
+    Note:  As string.punctuation doesn't typically include all unicode
+    symbols, you will have to use includechars to get some of these into 
+    otherwise restricted positions in your control, such as those specified
+    with &.
 
   Using these mask characters, a variety of template masks can be built. See
   the demo for some other common examples include date+time, social security
 
   Using these mask characters, a variety of template masks can be built. See
   the demo for some other common examples include date+time, social security
@@ -323,6 +334,9 @@ to individual fields:
                                of the nature of the validation and control of input.  However,
                                you can supply one to provide data transfer routines for the
                                controls.
                                of the nature of the validation and control of input.  However,
                                you can supply one to provide data transfer routines for the
                                controls.
+        raiseOnInvalidPaste    False by default; normally a bad paste simply is ignored with a bell;
+                               if True, this will cause a ValueError exception to be thrown,
+                               with the .value attribute of the exception containing the bad value.
         =====================  ==================================================================
 
 
         =====================  ==================================================================
 
 
@@ -835,17 +849,53 @@ control = (
     WXK_CTRL_X, WXK_CTRL_Z
     )
 
     WXK_CTRL_X, WXK_CTRL_Z
     )
 
+# Because unicode can go over the ansi character range, we need to explicitly test
+# for all non-visible keystrokes, rather than just assuming a particular range for
+# visible characters:
+wx_control_keycodes = range(32) + list(nav) + list(control) + [
+    wx.WXK_START, wx.WXK_LBUTTON, wx.WXK_RBUTTON, wx.WXK_CANCEL, wx.WXK_MBUTTON,
+    wx.WXK_CLEAR, wx.WXK_SHIFT, wx.WXK_CONTROL, wx.WXK_MENU, wx.WXK_PAUSE, 
+    wx.WXK_CAPITAL, wx.WXK_SELECT, wx.WXK_PRINT, wx.WXK_EXECUTE, wx.WXK_SNAPSHOT,
+    wx.WXK_HELP, wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3,
+    wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8,
+    wx.WXK_NUMPAD9, wx.WXK_MULTIPLY, wx.WXK_ADD, wx.WXK_SEPARATOR, wx.WXK_SUBTRACT,
+    wx.WXK_DECIMAL, wx.WXK_DIVIDE, wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4, 
+    wx.WXK_F5, wx.WXK_F6, wx.WXK_F7, wx.WXK_F8, wx.WXK_F9, wx.WXK_F10, wx.WXK_F11,
+    wx.WXK_F12, wx.WXK_F13, wx.WXK_F14, wx.WXK_F15, wx.WXK_F16, wx.WXK_F17,
+    wx.WXK_F18, wx.WXK_F19, wx.WXK_F20, wx.WXK_F21, wx.WXK_F22, wx.WXK_F23,
+    wx.WXK_F24, wx.WXK_NUMLOCK, wx.WXK_SCROLL, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN,
+    wx.WXK_NUMPAD_SPACE, wx.WXK_NUMPAD_TAB, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_F1,
+    wx.WXK_NUMPAD_F2, wx.WXK_NUMPAD_F3, wx.WXK_NUMPAD_F4, wx.WXK_NUMPAD_HOME,
+    wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN,
+    wx.WXK_NUMPAD_PRIOR, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_NEXT, wx.WXK_NUMPAD_PAGEDOWN,
+    wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_BEGIN, wx.WXK_NUMPAD_INSERT, wx.WXK_NUMPAD_DELETE,
+    wx.WXK_NUMPAD_EQUAL, wx.WXK_NUMPAD_MULTIPLY, wx.WXK_NUMPAD_ADD, wx.WXK_NUMPAD_SEPARATOR,
+    wx.WXK_NUMPAD_SUBTRACT, wx.WXK_NUMPAD_DECIMAL, wx.WXK_NUMPAD_DIVIDE, wx.WXK_WINDOWS_LEFT,
+    wx.WXK_WINDOWS_RIGHT, wx.WXK_WINDOWS_MENU, wx.WXK_COMMAND,
+    # Hardware-specific buttons
+    wx.WXK_SPECIAL1, wx.WXK_SPECIAL2, wx.WXK_SPECIAL3, wx.WXK_SPECIAL4, wx.WXK_SPECIAL5,
+    wx.WXK_SPECIAL6, wx.WXK_SPECIAL7, wx.WXK_SPECIAL8, wx.WXK_SPECIAL9, wx.WXK_SPECIAL10,
+    wx.WXK_SPECIAL11, wx.WXK_SPECIAL12, wx.WXK_SPECIAL13, wx.WXK_SPECIAL14, wx.WXK_SPECIAL15,
+    wx.WXK_SPECIAL16, wx.WXK_SPECIAL17, wx.WXK_SPECIAL18, wx.WXK_SPECIAL19, wx.WXK_SPECIAL20
+    ]
+    
 
 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
 
 ## Constants for masking. This is where mask characters
 ## are defined.
 ##  maskchars used to identify valid mask characters from all others
 
 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
 
 ## Constants for masking. This is where mask characters
 ## are defined.
 ##  maskchars used to identify valid mask characters from all others
-##   #- allow numeric 0-9 only
-##   A- allow uppercase only. Combine with forceupper to force lowercase to upper
-##   a- allow lowercase only. Combine with forcelower to force upper to lowercase
-##   X- allow any character (string.letters, string.punctuation, string.digits)
+##   # - allow numeric 0-9 only
+##   A - allow uppercase only. Combine with forceupper to force lowercase to upper
+##   a - allow lowercase only. Combine with forcelower to force upper to lowercase
+##   C - allow any letter, upper or lower
+##   X - allow string.letters, string.punctuation, string.digits
+##   & - allow string.punctuation only (doesn't include all unicode symbols)
+##   * - allow any visible character
+
 ## Note: locale settings affect what "uppercase", lowercase, etc comprise.
 ## Note: locale settings affect what "uppercase", lowercase, etc comprise.
+## Note: '|' is not a maskchar, in that it is a mask processing directive, and so
+## does not appear here.
 ##
 maskchars = ("#","A","a","X","C","N",'*','&')
 ansichars = ""
 ##
 maskchars = ("#","A","a","X","C","N",'*','&')
 ansichars = ""
@@ -1272,12 +1322,13 @@ class Field:
               'validRequired': False,           ## Set to True to disallow input that results in an invalid value
               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID
               'description': "",                ## primarily for autoformats, but could be useful elsewhere
               'validRequired': False,           ## Set to True to disallow input that results in an invalid value
               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID
               'description': "",                ## primarily for autoformats, but could be useful elsewhere
+              'raiseOnInvalidPaste': False,     ## if True, paste into field will cause ValueError
               }
 
     # This list contains all parameters that when set at the control level should
     # propagate down to each field:
     propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
               }
 
     # This list contains all parameters that when set at the control level should
     # propagate down to each field:
     propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
-                          'compareNoCase', 'emptyInvalid', 'validRequired')
+                          'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste')
 
     def __init__(self, **kwargs):
         """
 
     def __init__(self, **kwargs):
         """
@@ -1289,7 +1340,9 @@ class Field:
         for key in kwargs.keys():
             if key not in Field.valid_params.keys():
 ####                dbg(indent=0)
         for key in kwargs.keys():
             if key not in Field.valid_params.keys():
 ####                dbg(indent=0)
-                raise TypeError('invalid parameter "%s"' % (key))
+                ae = AttributeError('invalid parameter "%s"' % (key))
+                ae.attribute = key
+                raise ae
 
         # Set defaults for each parameter for this instance, and fully
         # populate initial parameter list for configuration:
 
         # Set defaults for each parameter for this instance, and fully
         # populate initial parameter list for configuration:
@@ -1316,7 +1369,9 @@ class Field:
         for key in kwargs.keys():
             if key not in Field.valid_params.keys():
 ##                dbg(indent=0, suspend=0)
         for key in kwargs.keys():
             if key not in Field.valid_params.keys():
 ##                dbg(indent=0, suspend=0)
-                raise AttributeError('invalid keyword argument "%s"' % key)
+                ae = AttributeError('invalid keyword argument "%s"' % key)
+                ae.attribute = key
+                raise ae
 
 ##        if self._index is not None: dbg('field index:', self._index)
 ##        dbg('parameters:', indent=1)
 
 ##        if self._index is not None: dbg('field index:', self._index)
 ##        dbg('parameters:', indent=1)
@@ -1384,7 +1439,9 @@ class Field:
         # Verify proper numeric format params:
         if self._groupdigits and self._groupChar == self._decimalChar:
 ##            dbg(indent=0, suspend=0)
         # Verify proper numeric format params:
         if self._groupdigits and self._groupChar == self._decimalChar:
 ##            dbg(indent=0, suspend=0)
-            raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar))
+            ae = AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar))
+            ae.attribute = self._groupChar
+            raise ae
 
 
         # Now go do validation, semantic and inter-dependency parameter processing:
 
 
         # Now go do validation, semantic and inter-dependency parameter processing:
@@ -1457,7 +1514,9 @@ class Field:
                             continue
                         if not self.IsValid(choice):
 ##                            dbg(indent=0, suspend=0)
                             continue
                         if not self.IsValid(choice):
 ##                            dbg(indent=0, suspend=0)
-                            raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
+                            ve = ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
+                            ve.value = choice
+                            raise ve
                 self._hasList = True
 
 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
                 self._hasList = True
 
 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
@@ -1742,7 +1801,7 @@ class MaskedEditMixin:
             'C': string.letters,
             'N': string.letters + string.digits,
             '&': string.punctuation,
             'C': string.letters,
             'N': string.letters + string.digits,
             '&': string.punctuation,
-            '*': ansichars
+            '*': ansichars  # to give it a value, but now allows any non-wxcontrol character
         }
 
         ## self._ignoreChange is used by MaskedComboBox, because
         }
 
         ## self._ignoreChange is used by MaskedComboBox, because
@@ -1790,7 +1849,9 @@ class MaskedEditMixin:
             key = key.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling
             if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
 ##                dbg(indent=0, suspend=0)
             key = key.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling
             if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
 ##                dbg(indent=0, suspend=0)
-                raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name))
+                ae = AttributeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name))
+                ae.attribute = key
+                raise ae
             elif key in Field.valid_params.keys():
                 constraint_kwargs[key] = value
             else:
             elif key in Field.valid_params.keys():
                 constraint_kwargs[key] = value
             else:
@@ -1818,7 +1879,9 @@ class MaskedEditMixin:
                 constraint_kwargs[param] = value
 
         elif autoformat and not autoformat in masktags.keys():
                 constraint_kwargs[param] = value
 
         elif autoformat and not autoformat in masktags.keys():
-            raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
+            ae = AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
+            ae.attribute = autoformat
+            raise ae
         else:
 ##            dbg('autoformat not selected')
             if kwargs.has_key('mask'):
         else:
 ##            dbg('autoformat not selected')
             if kwargs.has_key('mask'):
@@ -1847,18 +1910,18 @@ class MaskedEditMixin:
                     field = fields[i]
                     if not isinstance(field, Field):
 ##                        dbg(indent=0, suspend=0)
                     field = fields[i]
                     if not isinstance(field, Field):
 ##                        dbg(indent=0, suspend=0)
-                        raise AttributeError('invalid type for field parameter: %s' % repr(field))
+                        raise TypeError('invalid type for field parameter: %s' % repr(field))
                     self._fields[i] = field
 
             elif type(fields) == types.DictionaryType:
                 for index, field in fields.items():
                     if not isinstance(field, Field):
 ##                        dbg(indent=0, suspend=0)
                     self._fields[i] = field
 
             elif type(fields) == types.DictionaryType:
                 for index, field in fields.items():
                     if not isinstance(field, Field):
 ##                        dbg(indent=0, suspend=0)
-                        raise AttributeError('invalid type for field parameter: %s' % repr(field))
+                        raise TypeError('invalid type for field parameter: %s' % repr(field))
                     self._fields[index] = field
             else:
 ##                dbg(indent=0, suspend=0)
                     self._fields[index] = field
             else:
 ##                dbg(indent=0, suspend=0)
-                raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
+                raise TypeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
 
         # Assign constraint parameters for entire control:
 ####        dbg('control constraints:', indent=1)
 
         # Assign constraint parameters for entire control:
 ####        dbg('control constraints:', indent=1)
@@ -1992,7 +2055,7 @@ class MaskedEditMixin:
                 width = self.GetSize().width
                 height = self.GetBestSize().height
 ##                dbg('setting client size to:', (width, height))
                 width = self.GetSize().width
                 height = self.GetBestSize().height
 ##                dbg('setting client size to:', (width, height))
-                self.SetBestFittingSize((width, height))
+                self.SetInitialSize((width, height))
 
             # Set value/type-specific formatting
             self._applyFormatting()
 
             # Set value/type-specific formatting
             self._applyFormatting()
@@ -2055,7 +2118,9 @@ class MaskedEditMixin:
         parameters.)
         """
         if field_index not in self._field_indices:
         parameters.)
         """
         if field_index not in self._field_indices:
-            raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
+            ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
+            ie.index = field_index
+            raise ie
         # set parameters as requested:
         self._fields[field_index]._SetParameters(**kwargs)
 
         # set parameters as requested:
         self._fields[field_index]._SetParameters(**kwargs)
 
@@ -2076,7 +2141,7 @@ class MaskedEditMixin:
                     #  the outside size that does include the borders.  What you are
                     #  calculating (in _CalcSize) is the client size, but the sizers
                     #  deal with the full size and so that is the minimum size that
                     #  the outside size that does include the borders.  What you are
                     #  calculating (in _CalcSize) is the client size, but the sizers
                     #  deal with the full size and so that is the minimum size that
-                    #  we need to set with SetBestFittingSize.  The root of the problem is
+                    #  we need to set with SetInitialSize.  The root of the problem is
                     #  that in _calcSize the current client size height is returned,
                     #  instead of a height based on the current font.  So I suggest using
                     #  _calcSize to just get the width, and then use GetBestSize to
                     #  that in _calcSize the current client size height is returned,
                     #  instead of a height based on the current font.  So I suggest using
                     #  _calcSize to just get the width, and then use GetBestSize to
@@ -2084,7 +2149,7 @@ class MaskedEditMixin:
                     self.SetClientSize(self._CalcSize())
                     width = self.GetSize().width
                     height = self.GetBestSize().height
                     self.SetClientSize(self._CalcSize())
                     width = self.GetSize().width
                     height = self.GetBestSize().height
-                    self.SetBestFittingSize((width, height))
+                    self.SetInitialSize((width, height))
 
 
             # Set value/type-specific formatting
 
 
             # Set value/type-specific formatting
@@ -2096,11 +2161,15 @@ class MaskedEditMixin:
         Routine provided for getting a parameter of an individual field.
         """
         if field_index not in self._field_indices:
         Routine provided for getting a parameter of an individual field.
         """
         if field_index not in self._field_indices:
-            raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
+            ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
+            ie.index = field_index
+            raise ie
         elif Field.valid_params.has_key(paramname):
             return self._fields[field_index]._GetParameter(paramname)
         else:
         elif Field.valid_params.has_key(paramname):
             return self._fields[field_index]._GetParameter(paramname)
         else:
-            TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
+            ae = AttributeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
+            ae.attribute = paramname
+            raise ae
 
 
     def _SetKeycodeHandler(self, keycode, func):
 
 
     def _SetKeycodeHandler(self, keycode, func):
@@ -2208,9 +2277,13 @@ class MaskedEditMixin:
                 s += ' '
                 self._ctrl_constraints._defaultValue += ' '
 
                 s += ' '
                 self._ctrl_constraints._defaultValue += ' '
 
+
         # Now, go build up a dictionary of booleans, indexed by position,
         # Now, go build up a dictionary of booleans, indexed by position,
-        # indicating whether or not a given position is masked or not
+        # indicating whether or not a given position is masked or not.
+        # Also, strip out any '|' chars, adjusting the mask as necessary,
+        # marking the appropriate positions for field boundaries:
         ismasked = {}
         ismasked = {}
+        explicit_field_boundaries = []
         i = 0
         while i < len(s):
             if s[i] == '\\':            # if escaped character:
         i = 0
         while i < len(s):
             if s[i] == '\\':            # if escaped character:
@@ -2220,14 +2293,19 @@ class MaskedEditMixin:
                     if i+2 < len(s) and s[i+1] == '\\':
                         # if next char also a '\', char is a literal '\'
                         s = s[:i] + s[i+1:]     # elide the 2nd '\' as well
                     if i+2 < len(s) and s[i+1] == '\\':
                         # if next char also a '\', char is a literal '\'
                         s = s[:i] + s[i+1:]     # elide the 2nd '\' as well
+                i += 1                      # increment to next char
+            elif s[i] == '|':
+                s = s[:i] + s[i+1:] #         elide the '|'
+                explicit_field_boundaries.append(i)
+                # keep index where it is:
             else:                       # else if special char, mark position accordingly
                 ismasked[i] = s[i] in maskchars
             else:                       # else if special char, mark position accordingly
                 ismasked[i] = s[i] in maskchars
-####            dbg('ismasked[%d]:' % i, ismasked[i], s)
-            i += 1                      # increment to next char
+####                dbg('ismasked[%d]:' % i, ismasked[i], s)
+                i += 1                      # increment to next char
 ####        dbg('ismasked:', ismasked)
 ##        dbg('new mask: "%s"' % s, indent=0)
 
 ####        dbg('ismasked:', ismasked)
 ##        dbg('new mask: "%s"' % s, indent=0)
 
-        return s, ismasked
+        return s, ismasked, explicit_field_boundaries
 
 
     def _calcFieldExtents(self):
 
 
     def _calcFieldExtents(self):
@@ -2311,6 +2389,8 @@ class MaskedEditMixin:
                         while i < len(self._mask) and self._isMaskChar(i):
                             self._lookupField[i] = field_index
                             i += 1
                         while i < len(self._mask) and self._isMaskChar(i):
                             self._lookupField[i] = field_index
                             i += 1
+                            if i in self._explicit_field_boundaries:
+                                break
 ####                        dbg('edit_end =', i)
                         edit_end = i
                         self._lookupField[i] = field_index
 ####                        dbg('edit_end =', i)
                         edit_end = i
                         self._lookupField[i] = field_index
@@ -2328,6 +2408,7 @@ class MaskedEditMixin:
                                                                 mask=self._mask[edit_start:edit_end])
                     pos = i
                     i = self._findNextEntry(pos, adjustInsert=False)  # go to next field:
                                                                 mask=self._mask[edit_start:edit_end])
                     pos = i
                     i = self._findNextEntry(pos, adjustInsert=False)  # go to next field:
+####                    dbg('next entry:', i)
                     if i > pos:
                         for j in range(pos, i+1):
                             self._lookupField[j] = field_index
                     if i > pos:
                         for j in range(pos, i+1):
                             self._lookupField[j] = field_index
@@ -2348,7 +2429,10 @@ class MaskedEditMixin:
         # Verify that all field indices specified are valid for mask:
         for index in self._fields.keys():
             if index not in [-1] + self._lookupField.values():
         # Verify that all field indices specified are valid for mask:
         for index in self._fields.keys():
             if index not in [-1] + self._lookupField.values():
-                raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
+                ie = IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
+                ie.index = index
+                raise ie
+
 
 
     def _calcTemplate(self, reset_fillchar, reset_default):
 
 
     def _calcTemplate(self, reset_fillchar, reset_default):
@@ -2452,7 +2536,9 @@ class MaskedEditMixin:
 ##            dbg('self._defaultValue:', self._defaultValue)
             if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
 ####                dbg(indent=0)
 ##            dbg('self._defaultValue:', self._defaultValue)
             if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
 ####                dbg(indent=0)
-                raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name))
+                ve = ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name))
+                ve.value = self._defaultValue
+                raise ve
 
             # if no fillchar change, but old value == old template, replace it:
             if newvalue == old_template:
 
             # if no fillchar change, but old value == old template, replace it:
             if newvalue == old_template:
@@ -2540,10 +2626,17 @@ class MaskedEditMixin:
                     valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
                     if not valid_paste:
 ####                        dbg(indent=0)
                     valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
                     if not valid_paste:
 ####                        dbg(indent=0)
-                        raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name))
+                        ve = ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name))
+                        ve.value = choice
+                        ve.index = index
+                        raise ve
                     elif replace_to > end:
 ####                        dbg(indent=0)
                     elif replace_to > end:
 ####                        dbg(indent=0)
-                        raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
+                        ve = ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
+                        ve.value = choice
+                        ve.index = index
+                        raise ve
+
 ####                    dbg(choice, 'valid in field', index)
 
 
 ####                    dbg(choice, 'valid in field', index)
 
 
@@ -2568,7 +2661,7 @@ class MaskedEditMixin:
         # Preprocess specified mask to expand {n} syntax, handle escaped
         # mask characters, etc and build the resulting positionally keyed
         # dictionary for which positions are mask vs. template characters:
         # Preprocess specified mask to expand {n} syntax, handle escaped
         # mask characters, etc and build the resulting positionally keyed
         # dictionary for which positions are mask vs. template characters:
-        self._mask, self.ismasked = self._processMask(mask)
+        self._mask, self._ismasked, self._explicit_field_boundaries = self._processMask(mask)
         self._masklength = len(self._mask)
 ####        dbg('processed mask:', self._mask)
 
         self._masklength = len(self._mask)
 ####        dbg('processed mask:', self._mask)
 
@@ -2883,13 +2976,6 @@ class MaskedEditMixin:
 ##            dbg("field length exceeded:",pos)
             keep_processing = False
 
 ##            dbg("field length exceeded:",pos)
             keep_processing = False
 
-        if keep_processing:
-            if self._isMaskChar(pos):  ## Get string of allowed characters for validation
-                okchars = self._getAllowedChars(pos)
-            else:
-##                dbg('Not a valid position: pos = ', pos,"chars=",maskchars)
-                okchars = ""
-
         key = self._adjustKey(pos, key)     # apply formatting constraints to key:
 
         if self._keyhandlers.has_key(key):
         key = self._adjustKey(pos, key)     # apply formatting constraints to key:
 
         if self._keyhandlers.has_key(key):
@@ -2902,30 +2988,35 @@ class MaskedEditMixin:
 ##                dbg(indent=0)
                 return
             # else skip default processing, but do final formatting
 ##                dbg(indent=0)
                 return
             # else skip default processing, but do final formatting
-        if key < wx.WXK_SPACE or key > 255:
-##            dbg('key < WXK_SPACE or key > 255')
-            event.Skip()                # non alphanumeric
+        if key in wx_control_keycodes:
+##            dbg('key in wx_control_keycodes')
+            event.Skip()                # non-printable; let base control handle it
             keep_processing = False
         else:
             field = self._FindField(pos)
 
             keep_processing = False
         else:
             field = self._FindField(pos)
 
-##            dbg("key ='%s'" % chr(key))
-            if chr(key) == ' ':
-##                dbg('okSpaces?', field._okSpaces)
-                pass
-
-            char = chr(key) # (must work if we got this far)
-
             if 'unicode' in wx.PlatformInfo:
             if 'unicode' in wx.PlatformInfo:
-                char = char.decode(self._defaultEncoding)
+                if key < 256:
+                    char = chr(key) # (must work if we got this far)
+                    char = char.decode(self._defaultEncoding)
+                else:
+                    char = unichr(event.GetUnicodeKey())
+                    dbg('unicode char:', char)
                 excludes = u''
                 if type(field._excludeChars) != types.UnicodeType:
                     excludes += field._excludeChars.decode(self._defaultEncoding)
                 if type(self._ctrl_constraints) != types.UnicodeType:
                     excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding)
             else:
                 excludes = u''
                 if type(field._excludeChars) != types.UnicodeType:
                     excludes += field._excludeChars.decode(self._defaultEncoding)
                 if type(self._ctrl_constraints) != types.UnicodeType:
                     excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding)
             else:
+                char = chr(key) # (must work if we got this far)
                 excludes = field._excludeChars + self._ctrl_constraints._excludeChars
 
                 excludes = field._excludeChars + self._ctrl_constraints._excludeChars
 
+##                dbg("key ='%s'" % chr(key))
+                if chr(key) == ' ':
+##                dbg('okSpaces?', field._okSpaces)
+                    pass
+
+
             if char in excludes:
                 keep_processing = False
 
             if char in excludes:
                 keep_processing = False
 
@@ -3200,7 +3291,7 @@ class MaskedEditMixin:
             Should return False to skip other processing. """
 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
         self.Paste()
             Should return False to skip other processing. """
 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
         self.Paste()
-        dbg(indent=0)
+##        dbg(indent=0)
         return False
 
     def _OnInsert(self, event=None):
         return False
 
     def _OnInsert(self, event=None):
@@ -3910,7 +4001,8 @@ class MaskedEditMixin:
 
     def _findNextEntry(self,pos, adjustInsert=True):
         """ Find the insertion point for the next valid entry character position."""
 
     def _findNextEntry(self,pos, adjustInsert=True):
         """ Find the insertion point for the next valid entry character position."""
-        if self._isTemplateChar(pos):   # if changing fields, pay attn to flag
+##        dbg('MaskedEditMixin::_findNextEntry', indent=1)        
+        if self._isTemplateChar(pos) or pos in self._explicit_field_boundaries:   # if changing fields, pay attn to flag
             adjustInsert = adjustInsert
         else:                           # else within a field; flag not relevant
             adjustInsert = False
             adjustInsert = adjustInsert
         else:                           # else within a field; flag not relevant
             adjustInsert = False
@@ -3927,6 +4019,7 @@ class MaskedEditMixin:
             slice = self._GetValue()[start:end]
             if field._insertRight and field.IsEmpty(slice):
                 pos = end
             slice = self._GetValue()[start:end]
             if field._insertRight and field.IsEmpty(slice):
                 pos = end
+##        dbg('final pos:', pos, indent=0)
         return pos
 
 
         return pos
 
 
@@ -4447,7 +4540,7 @@ class MaskedEditMixin:
         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
         """
         if pos < self._masklength:
         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
         """
         if pos < self._masklength:
-            return self.ismasked[pos]
+            return self._ismasked[pos]
         else:
             return False
 
         else:
             return False
 
@@ -4533,7 +4626,7 @@ class MaskedEditMixin:
                     okChars += ')'
 
 ####            dbg('%s in %s?' % (char, okChars), char in okChars)
                     okChars += ')'
 
 ####            dbg('%s in %s?' % (char, okChars), char in okChars)
-            approved = char in okChars
+            approved = (self.maskdict[pos] == '*' or char in okChars)
 
             if approved and checkRegex:
 ##                dbg("checking appropriate regex's")
 
             if approved and checkRegex:
 ##                dbg("checking appropriate regex's")
@@ -5558,9 +5651,13 @@ class MaskedEditMixin:
             if raise_on_invalid:
 ##                dbg(indent=0, suspend=0)
                 if item == 'control':
             if raise_on_invalid:
 ##                dbg(indent=0, suspend=0)
                 if item == 'control':
-                    raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
+                    ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
+                    ve.value = paste_text
+                    raise ve
                 else:
                 else:
-                    raise ValueError('"%s" will not fit into the selection' % paste_text)
+                    ve = ValueError('"%s" will not fit into the selection' % paste_text)
+                    ve.value = paste_text
+                    raise ve
             else:
 ##                dbg(indent=0, suspend=0)
                 return False, None, None
             else:
 ##                dbg(indent=0, suspend=0)
                 return False, None, None
@@ -5615,13 +5712,18 @@ class MaskedEditMixin:
 
         if not valid_paste and raise_on_invalid:
 ##            dbg('raising exception', indent=0, suspend=0)
 
         if not valid_paste and raise_on_invalid:
 ##            dbg('raising exception', indent=0, suspend=0)
-            raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
+            ve = ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
+            ve.value = paste_text
+            raise ve
+
 
         elif i < len(paste_text):
             valid_paste = False
             if raise_on_invalid:
 ##                dbg('raising exception', indent=0, suspend=0)
 
         elif i < len(paste_text):
             valid_paste = False
             if raise_on_invalid:
 ##                dbg('raising exception', indent=0, suspend=0)
-                raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
+                ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
+                ve.value = paste_text
+                raise ve
 
 ##        dbg('valid_paste?', valid_paste)
         if valid_paste:
 
 ##        dbg('valid_paste?', valid_paste)
         if valid_paste:
@@ -5735,6 +5837,7 @@ class MaskedEditMixin:
                     sel_start = 0
 ##                dbg('adjusted selection:', (sel_start, sel_to))
 
                     sel_start = 0
 ##                dbg('adjusted selection:', (sel_start, sel_to))
 
+            raise_on_invalid = raise_on_invalid or field._raiseOnInvalidPaste
             try:
                 valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
             except:
             try:
                 valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
             except:
@@ -6599,6 +6702,19 @@ __i=0
 
 ## CHANGELOG:
 ## ====================
 
 ## CHANGELOG:
 ## ====================
+##  Version 1.11
+##  1. Added value member to ValueError exceptions, so that people can catch them
+##     and then display their own errors, and added attribute raiseOnInvalidPaste,
+##     so one doesn't have to subclass the controls simply to force generation of
+##     a ValueError on a bad paste operation.
+##  2. Fixed handling of unicode charsets by converting to explicit control char
+##     set testing for passing those keystrokes to the base control, and then 
+##     changing the semantics of the * maskchar to indicate any visible char.
+##  3. Added '|' mask specification character, which allows splitting of contiguous
+##     mask characters into separate fields, allowing finer control of behavior
+##     of a control.
+##     
+##
 ##  Version 1.10
 ##  1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete
 ##     cuts, shift-insert pastes, and ctrl-insert copies.
 ##  Version 1.10
 ##  1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete
 ##     cuts, shift-insert pastes, and ctrl-insert copies.