]> git.saurik.com Git - wxWidgets.git/commitdiff
Added new MaskedEditControl code from Will Sadkin. The modules are
authorRobin Dunn <robin@alldunn.com>
Mon, 19 Apr 2004 23:24:37 +0000 (23:24 +0000)
committerRobin Dunn <robin@alldunn.com>
Mon, 19 Apr 2004 23:24:37 +0000 (23:24 +0000)
now locaed in their own sub-package, wx.lib.masked.  Demos updated.

git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@26874 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

21 files changed:
wxPython/demo/MaskedEditControls.py
wxPython/demo/MaskedNumCtrl.py
wxPython/demo/TimeCtrl.py
wxPython/docs/CHANGES.txt
wxPython/docs/MigrationGuide.txt
wxPython/wx/lib/masked/__init__.py [new file with mode: 0644]
wxPython/wx/lib/masked/combobox.py [new file with mode: 0644]
wxPython/wx/lib/masked/ctrl.py [new file with mode: 0644]
wxPython/wx/lib/masked/ipaddrctrl.py [new file with mode: 0644]
wxPython/wx/lib/masked/maskededit.py [new file with mode: 0644]
wxPython/wx/lib/masked/numctrl.py [new file with mode: 0644]
wxPython/wx/lib/masked/textctrl.py [new file with mode: 0644]
wxPython/wx/lib/masked/timectrl.py [new file with mode: 0644]
wxPython/wx/lib/maskedctrl.py [deleted file]
wxPython/wx/lib/maskededit.py [deleted file]
wxPython/wx/lib/maskednumctrl.py [deleted file]
wxPython/wx/lib/timectrl.py [deleted file]
wxPython/wxPython/lib/maskedctrl.py
wxPython/wxPython/lib/maskededit.py
wxPython/wxPython/lib/maskednumctrl.py
wxPython/wxPython/lib/timectrl.py

index c20988837af7e4e245791d97592d2f220215e420..b88f53b07ec4ec26330d92a787d1f3c78d6b456a 100644 (file)
@@ -4,9 +4,8 @@ import  sys
 import  traceback
 
 import  wx
-import  wx.lib.maskededit       as  med
-import  wx.lib.maskedctrl       as  mctl
-import  wx.lib.scrolledpanel    as  scroll
+import  wx.lib.masked             as  masked
+import  wx.lib.scrolledpanel      as  scroll
 
 
 class demoMixin:
@@ -18,7 +17,7 @@ class demoMixin:
         mask        = wx.StaticText( self, -1, "Mask Value" )
         formatcode  = wx.StaticText( self, -1, "Format" )
         regex       = wx.StaticText( self, -1, "Regexp Validator(opt.)" )
-        ctrl        = wx.StaticText( self, -1, "MaskedTextCtrl" )
+        ctrl        = wx.StaticText( self, -1, "Masked TextCtrl" )
 
         description.SetFont( wx.Font(9, wx.SWISS, wx.NORMAL, wx.BOLD))
         mask.SetFont( wx.Font(9, wx.SWISS, wx.NORMAL, wx.BOLD))
@@ -41,7 +40,7 @@ class demoMixin:
             sizer.Add( wx.StaticText( self, -1, control[4]) )
 
             if control in controls:
-                newControl  = med.MaskedTextCtrl( self, -1, "",
+                newControl  = masked.TextCtrl( self, -1, "",
                                                 mask         = control[1],
                                                 excludeChars = control[2],
                                                 formatcodes  = control[3],
@@ -79,7 +78,7 @@ class demoPage1(scroll.ScrolledPanel, demoMixin):
         self.editList  = []
 
         label = wx.StaticText( self, -1, """\
-Here are some basic MaskedTextCtrls to give you an idea of what you can do
+Here are some basic masked TextCtrls to give you an idea of what you can do
 with this control.  Note that all controls have been auto-sized by including 'F' in
 the format codes.
 
@@ -152,8 +151,8 @@ class demoPage2(scroll.ScrolledPanel, demoMixin):
 
         label = wx.StaticText( self, -1, """\
 All these controls have been created by passing a single parameter, the autoformat code,
-and use the factory class MaskedCtrl with its default controlType.
-The maskededit module contains an internal dictionary of types and formats (autoformats).
+and use the factory class masked.Ctrl with its default controlType.
+The masked package contains an internal dictionary of types and formats (autoformats).
 Many of these already do complicated validation; To see some examples, try
 29 Feb 2002 vs. 2004 for the date formats, or email address validation.
 """)
@@ -163,7 +162,7 @@ Many of these already do complicated validation; To see some examples, try
 
         description = wx.StaticText( self, -1, "Description")
         autofmt     = wx.StaticText( self, -1, "AutoFormat Code")
-        ctrl        = wx.StaticText( self, -1, "MaskedCtrl")
+        ctrl        = wx.StaticText( self, -1, "Masked Ctrl")
 
         description.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) )
         autofmt.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) )
@@ -174,10 +173,10 @@ Many of these already do complicated validation; To see some examples, try
         grid.Add( autofmt,     0, wx.ALIGN_LEFT )
         grid.Add( ctrl,        0, wx.ALIGN_LEFT )
 
-        for autoformat, desc in med.autoformats:
+        for autoformat, desc in masked.autoformats:
             grid.Add( wx.StaticText( self, -1, desc), 0, wx.ALIGN_LEFT )
             grid.Add( wx.StaticText( self, -1, autoformat), 0, wx.ALIGN_LEFT )
-            grid.Add( mctl.MaskedCtrl( self, -1, "",
+            grid.Add( masked.Ctrl( self, -1, "",
                                     autoformat       = autoformat,
                                     demo             = True,
                                     name             = autoformat),
@@ -197,7 +196,7 @@ class demoPage3(scroll.ScrolledPanel, demoMixin):
         self.editList  = []
 
         label = wx.StaticText( self, -1, """\
-Here MaskedTextCtrls that have default values.  The states
+Here masked TextCtrls that have default values.  The states
 control has a list of valid values, and the unsigned integer
 has a legal range specified.
 """)
@@ -215,7 +214,7 @@ has a legal range specified.
 
         controls = [
         #description        mask                    excl format     regexp                              range,list,initial
-       ("U.S. State (2 char)",      "AA",            "", 'F!_',       "[A-Z]{2}",                         '',med.states, med.states[0]),
+       ("U.S. State (2 char)",      "AA",            "", 'F!_',       "[A-Z]{2}",                         '', masked.states, masked.states[0]),
        ("Integer (signed)",         "#{6}",          "", 'F-_',       "",                                 '','', ' 0    '),
        ("Integer (unsigned)\n(1-399)","######",      "", 'F_',        "",                                 (1,399),'', '1     '),
        ("Float (signed)",           "#{6}.#{9}",     "", 'F-_R',      "",                                 '','', '000000.000000000'),
@@ -256,7 +255,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list.
         description  = wx.StaticText( self, -1, "Description" )
         autofmt      = wx.StaticText( self, -1, "AutoFormat Code" )
         fields       = wx.StaticText( self, -1, "Field Objects" )
-        ctrl         = wx.StaticText( self, -1, "MaskedTextCtrl" )
+        ctrl         = wx.StaticText( self, -1, "Masked TextCtrl" )
 
         description.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) )
         autofmt.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) )
@@ -270,7 +269,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list.
         grid.Add( ctrl,        0, wx.ALIGN_LEFT )
 
         autoformat = "USPHONEFULLEXT"
-        fieldsDict = {0: med.Field(choices=["617","781","508","978","413"], choiceRequired=True)}
+        fieldsDict = {0: masked.Field(choices=["617","781","508","978","413"], choiceRequired=True)}
         fieldsLabel = """\
 {0: Field(choices=[
             "617","781",
@@ -279,7 +278,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list.
         grid.Add( wx.StaticText( self, -1, "Restricted Area Code"), 0, wx.ALIGN_LEFT )
         grid.Add( wx.StaticText( self, -1, autoformat), 0, wx.ALIGN_LEFT )
         grid.Add( wx.StaticText( self, -1, fieldsLabel), 0, wx.ALIGN_LEFT )
-        grid.Add( med.MaskedTextCtrl( self, -1, "",
+        grid.Add( masked.TextCtrl( self, -1, "",
                                     autoformat       = autoformat,
                                     fields           = fieldsDict,
                                     demo             = True,
@@ -287,12 +286,12 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list.
                   0, wx.ALIGN_LEFT )
 
         autoformat = "EXPDATEMMYY"
-        fieldsDict = {1: med.Field(choices=["03", "04", "05"], choiceRequired=True)}
+        fieldsDict = {1: masked.Field(choices=["03", "04", "05"], choiceRequired=True)}
         fieldsLabel = """\
 {1: Field(choices=[
             "03", "04", "05"],
           choiceRequired=True)}"""
-        exp =  med.MaskedTextCtrl( self, -1, "",
+        exp =  masked.TextCtrl( self, -1, "",
                                  autoformat       = autoformat,
                                  fields           = fieldsDict,
                                  demo             = True,
@@ -303,15 +302,15 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list.
         grid.Add( wx.StaticText( self, -1, fieldsLabel), 0, wx.ALIGN_LEFT )
         grid.Add( exp, 0, wx.ALIGN_LEFT )
 
-        fieldsDict = {0: med.Field(choices=["02134","02155"], choiceRequired=True),
-                      1: med.Field(choices=["1234", "5678"],  choiceRequired=False)}
+        fieldsDict = {0: masked.Field(choices=["02134","02155"], choiceRequired=True),
+                      1: masked.Field(choices=["1234", "5678"],  choiceRequired=False)}
         fieldsLabel = """\
 {0: Field(choices=["02134","02155"],
           choiceRequired=True),
  1: Field(choices=["1234", "5678"],
           choiceRequired=False)}"""
         autoformat = "USZIPPLUS4"
-        zip =  med.MaskedTextCtrl( self, -1, "",
+        zip =  masked.TextCtrl( self, -1, "",
                                  autoformat       = autoformat,
                                  fields           = fieldsDict,
                                  demo             = True,
@@ -336,7 +335,7 @@ class demoPage5(scroll.ScrolledPanel, demoMixin):
 
 
         labelMaskedCombos = wx.StaticText( self, -1, """\
-These are some examples of MaskedComboBox:""")
+These are some examples of masked.ComboBox:""")
         labelMaskedCombos.SetForegroundColour( "Blue" )
 
 
@@ -344,8 +343,8 @@ These are some examples of MaskedComboBox:""")
 A state selector; only
 "legal" values can be
 entered:""")
-        statecode = med.MaskedComboBox( self, -1, med.states[0],
-                                  choices = med.states,
+        statecode = masked.ComboBox( self, -1, masked.states[0],
+                                  choices = masked.states,
                                   autoformat="USSTATE")
 
         label_statename = wx.StaticText( self, -1, """\
@@ -353,9 +352,9 @@ A state name selector,
 with auto-select:""")
 
         # Create this one using factory function:
-        statename = mctl.MaskedCtrl( self, -1, med.state_names[0],
-                                  controlType = mctl.controlTypes.MASKEDCOMBO,
-                                  choices = med.state_names,
+        statename = masked.Ctrl( self, -1, masked.state_names[0],
+                                  controlType = masked.controlTypes.COMBO,
+                                  choices = masked.state_names,
                                   autoformat="USSTATENAME",
                                   autoSelect=True)
         statename.SetCtrlParameters(formatcodes = 'F!V_')
@@ -363,8 +362,8 @@ with auto-select:""")
 
         numerators = [ str(i) for i in range(1, 4) ]
         denominators = [ string.ljust(str(i), 2) for i in [2,3,4,5,8,16,32,64] ]
-        fieldsDict = {0: med.Field(choices=numerators, choiceRequired=False),
-                      1: med.Field(choices=denominators, choiceRequired=True)}
+        fieldsDict = {0: masked.Field(choices=numerators, choiceRequired=False),
+                      1: masked.Field(choices=denominators, choiceRequired=True)}
         choices = []
         for n in numerators:
             for d in denominators:
@@ -377,8 +376,8 @@ A masked ComboBox for fraction selection.
 Choices for each side of the fraction can
 be selected with PageUp/Down:""")
 
-        fraction = mctl.MaskedCtrl( self, -1, "",
-                                 controlType = mctl.MASKEDCOMBO,
+        fraction = masked.Ctrl( self, -1, "",
+                                 controlType = masked.controlTypes.COMBO,
                                  choices = choices,
                                  choiceRequired = True,
                                  mask = "#/##",
@@ -392,7 +391,7 @@ A masked ComboBox to validate
 text from a list of numeric codes:""")
 
         choices = ["91", "136", "305", "4579"]
-        code = med.MaskedComboBox( self, -1, choices[0],
+        code = masked.ComboBox( self, -1, choices[0],
                                  choices = choices,
                                  choiceRequired = True,
                                  formatcodes = "F_r",
@@ -402,8 +401,8 @@ text from a list of numeric codes:""")
 Programmatically set
 choice sets:""")
         self.list_selector = wx.ComboBox(self, -1, '', choices = ['list1', 'list2', 'list3'])
-        self.dynamicbox = mctl.MaskedCtrl( self, -1, '    ',
-                                      controlType = mctl.controlTypes.MASKEDCOMBO,
+        self.dynamicbox = masked.Ctrl( self, -1, '    ',
+                                      controlType = masked.controlTypes.COMBO,
                                       mask =    'XXXX',
                                       formatcodes = 'F_',
                                       # these are to give dropdown some initial height,
@@ -415,23 +414,23 @@ choice sets:""")
 
 
         labelIpAddrs = wx.StaticText( self, -1, """\
-Here are some examples of IpAddrCtrl, a control derived from MaskedTextCtrl:""")
+Here are some examples of IpAddrCtrl, a control derived from masked.TextCtrl:""")
         labelIpAddrs.SetForegroundColour( "Blue" )
 
 
         label_ipaddr1 = wx.StaticText( self, -1, "An empty control:")
-        ipaddr1 = med.IpAddrCtrl( self, -1, style = wx.TE_PROCESS_TAB )
+        ipaddr1 = masked.IpAddrCtrl( self, -1, style = wx.TE_PROCESS_TAB )
 
 
         label_ipaddr2 = wx.StaticText( self, -1, "A restricted mask:")
-        ipaddr2 = med.IpAddrCtrl( self, -1, mask=" 10.  1.109.###" )
+        ipaddr2 = masked.IpAddrCtrl( self, -1, mask=" 10.  1.109.###" )
 
 
         label_ipaddr3 = wx.StaticText( self, -1, """\
 A control with restricted legal values:
 10. (1|2) . (129..255) . (0..255)""")
-        ipaddr3 = mctl.MaskedCtrl( self, -1,
-                                controlType = mctl.controlTypes.IPADDR,
+        ipaddr3 = masked.Ctrl( self, -1,
+                                controlType = masked.controlTypes.IPADDR,
                                 mask=" 10.  #.###.###")
         ipaddr3.SetFieldParameters(0, validRegex="1|2",validRequired=False )   # requires entry to match or not allowed
 
@@ -441,22 +440,22 @@ A control with restricted legal values:
 
 
         labelNumerics = wx.StaticText( self, -1, """\
-Here are some useful configurations of a MaskedTextCtrl for integer and floating point input that still treat
-the control as a text control.  (For a true numeric control, check out the MaskedNumCtrl class!)""")
+Here are some useful configurations of a masked.TextCtrl for integer and floating point input that still treat
+the control as a text control.  (For a true numeric control, check out the masked.NumCtrl class!)""")
         labelNumerics.SetForegroundColour( "Blue" )
 
         label_intctrl1 = wx.StaticText( self, -1, """\
 An integer entry control with
 shifting insert enabled:""")
-        self.intctrl1 = med.MaskedTextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,F>')
+        self.intctrl1 = masked.TextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,F>')
         label_intctrl2 = wx.StaticText( self, -1, """\
      Right-insert integer entry:""")
-        self.intctrl2 = med.MaskedTextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,Fr')
+        self.intctrl2 = masked.TextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,Fr')
 
         label_floatctrl = wx.StaticText( self, -1, """\
 A floating point entry control
 with right-insert for ordinal:""")
-        self.floatctrl = med.MaskedTextCtrl(self, -1, name='floatctrl', mask="#{9}.#{2}", formatcodes="F,_-R", useParensForNegatives=False)
+        self.floatctrl = masked.TextCtrl(self, -1, name='floatctrl', mask="#{9}.#{2}", formatcodes="F,_-R", useParensForNegatives=False)
         self.floatctrl.SetFieldParameters(0, formatcodes='r<', validRequired=True)  # right-insert, require explicit cursor movement to change fields
         self.floatctrl.SetFieldParameters(1, defaultValue='00')                     # don't allow blank fraction
 
@@ -588,7 +587,7 @@ with right-insert for ordinal:""")
             formatcodes += 'r'
             mask = '###'
         else:
-            choices = med.states
+            choices = masked.states
             mask = 'AA'
             formatcodes += '!'
         self.dynamicbox.SetCtrlParameters( mask = mask,
@@ -628,15 +627,15 @@ def runTest(frame, nb, log):
 
 def RunStandalone():
     app = wx.PySimpleApp()
-    frame = wx.Frame(None, -1, "Test MaskedTextCtrl", size=(640, 480))
+    frame = wx.Frame(None, -1, "Test MaskedEditCtrls", size=(640, 480))
     win = TestMaskedTextCtrls(frame, -1, sys.stdout)
     frame.Show(True)
     app.MainLoop()
 #----------------------------------------------------------------------------
-
+import wx.lib.masked.maskededit as maskededit
 overview = """<html>
 <PRE><FONT SIZE=-1>
-""" + med.__doc__ + """
+""" + maskededit.__doc__ + """
 </FONT></PRE>
 """
 
index e0e4077600da4d75c42b252dea3e91089739f22f..6d5bda9937fc8eaaad3fb4688de40c2c0a15193b 100644 (file)
@@ -4,8 +4,8 @@ import  sys
 import  traceback
 
 import  wx
-import  wx.lib.maskededit       as me
-import  wx.lib.maskednumctrl    as mnum
+from    wx.lib import masked
+
 #----------------------------------------------------------------------
 
 class TestPanel( wx.Panel ):
@@ -16,40 +16,40 @@ class TestPanel( wx.Panel ):
         panel = wx.Panel( self, -1 )
 
         header = wx.StaticText(panel, -1, """\
-This shows the various options for MaskedNumCtrl.
+This shows the various options for masked.NumCtrl.
 The controls at the top reconfigure the resulting control at the bottom.
 """)
         header.SetForegroundColour( "Blue" )
 
         intlabel = wx.StaticText( panel, -1, "Integer width:" )
-        self.integerwidth = mnum.MaskedNumCtrl(
+        self.integerwidth = masked.NumCtrl(
                                 panel, value=10, integerWidth=2, allowNegative=False
                                 )
 
         fraclabel = wx.StaticText( panel, -1, "Fraction width:" )
-        self.fractionwidth = mnum.MaskedNumCtrl(
-                                panel, value=0, integerWidth=2, allowNegative=False 
+        self.fractionwidth = masked.NumCtrl(
+                                panel, value=0, integerWidth=2, allowNegative=False
                                 )
 
         groupcharlabel = wx.StaticText( panel,-1, "Grouping char:" )
-        self.groupchar = me.MaskedTextCtrl( 
+        self.groupchar = masked.TextCtrl(
                                 panel, -1, value=',', mask='&', excludeChars = '-()',
                                 formatcodes='F', emptyInvalid=True, validRequired=True
                                 )
 
         decimalcharlabel = wx.StaticText( panel,-1, "Decimal char:" )
-        self.decimalchar = me.MaskedTextCtrl( 
+        self.decimalchar = masked.TextCtrl(
                                 panel, -1, value='.', mask='&', excludeChars = '-()',
                                 formatcodes='F', emptyInvalid=True, validRequired=True
                                 )
 
         self.set_min = wx.CheckBox( panel, -1, "Set minimum value:" )
-        # Create this MaskedNumCtrl using factory, to show how:
-        self.min = mnum.MaskedNumCtrl( panel, integerWidth=5, fractionWidth=2 )
+        # Create this masked.NumCtrl using factory, to show how:
+        self.min = masked.Ctrl( panel, integerWidth=5, fractionWidth=2, controlType=masked.controlTypes.NUMBER )
         self.min.Enable( False )
 
         self.set_max = wx.CheckBox( panel, -1, "Set maximum value:" )
-        self.max = mnum.MaskedNumCtrl( panel, integerWidth=5, fractionWidth=2 )
+        self.max = masked.NumCtrl( panel, integerWidth=5, fractionWidth=2 )
         self.max.Enable( False )
 
 
@@ -68,7 +68,7 @@ The controls at the top reconfigure the resulting control at the bottom.
         font.SetWeight(wx.BOLD)
         label.SetFont(font)
 
-        self.target_ctl = mnum.MaskedNumCtrl( panel, -1, name="target control" )
+        self.target_ctl = masked.NumCtrl( panel, -1, name="target control" )
 
         label_numselect = wx.StaticText( panel, -1, """\
 Programmatically set the above
@@ -141,15 +141,15 @@ value entry ctrl:""")
         panel.Move( (50,10) )
         self.panel = panel
 
-        self.Bind(mnum.EVT_MASKEDNUM, self.OnSetIntWidth, self.integerwidth )
-        self.Bind(mnum.EVT_MASKEDNUM, self.OnSetFractionWidth, self.fractionwidth )
+        self.Bind(masked.EVT_NUM, self.OnSetIntWidth, self.integerwidth )
+        self.Bind(masked.EVT_NUM, self.OnSetFractionWidth, self.fractionwidth )
         self.Bind(wx.EVT_TEXT, self.OnSetGroupChar, self.groupchar )
         self.Bind(wx.EVT_TEXT, self.OnSetDecimalChar, self.decimalchar )
 
         self.Bind(wx.EVT_CHECKBOX, self.OnSetMin, self.set_min )
         self.Bind(wx.EVT_CHECKBOX, self.OnSetMax, self.set_max )
-        self.Bind(mnum.EVT_MASKEDNUM, self.SetTargetMinMax, self.min )
-        self.Bind(mnum.EVT_MASKEDNUM, self.SetTargetMinMax, self.max )
+        self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.min )
+        self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.max )
 
         self.Bind(wx.EVT_CHECKBOX, self.SetTargetMinMax, self.limit_target )
         self.Bind(wx.EVT_CHECKBOX, self.OnSetAllowNone, self.allow_none )
@@ -158,7 +158,7 @@ value entry ctrl:""")
         self.Bind(wx.EVT_CHECKBOX, self.OnSetUseParens, self.use_parens )
         self.Bind(wx.EVT_CHECKBOX, self.OnSetSelectOnEntry, self.select_on_entry )
 
-        self.Bind(mnum.EVT_MASKEDNUM, self.OnTargetChange, self.target_ctl )
+        self.Bind(masked.EVT_NUM, self.OnTargetChange, self.target_ctl )
         self.Bind(wx.EVT_COMBOBOX, self.OnNumberSelect, self.numselect )
 
 
@@ -323,6 +323,7 @@ def runTest( frame, nb, log ):
     return win
 
 #----------------------------------------------------------------------
+import wx.lib.masked.numctrl as mnum
 overview = mnum.__doc__
 
 if __name__ == '__main__':
index 3f7098d0eddaf48401b787c9ea73fc53a04f4da5..7b730fb0b96881dd1e1c2977c8cdd386bc22a39f 100644 (file)
@@ -1,12 +1,12 @@
-# 
+#
 # 11/21/2003 - Jeff Grimmett (grimmtooth@softhome.net)
 #
 # o presense of spin control causing probs (see spin ctrl demo for details)
-# 
+#
 
-import  wx
-import  wx.lib.timectrl         as  timectl
-import  wx.lib.scrolledpanel    as  scrolled
+import wx
+import wx.lib.scrolledpanel    as scrolled
+import wx.lib.masked           as masked
 
 #----------------------------------------------------------------------
 
@@ -18,21 +18,21 @@ class TestPanel( scrolled.ScrolledPanel ):
 
 
         text1 = wx.StaticText( self, -1, "12-hour format:")
-        self.time12 = timectl.TimeCtrl( self, -1, name="12 hour control" )
+        self.time12 = masked.TimeCtrl( self, -1, name="12 hour control" )
         spin1 = wx.SpinButton( self, -1, wx.DefaultPosition, (-1,20), 0 )
         self.time12.BindSpinButton( spin1 )
 
         text2 = wx.StaticText( self, -1, "24-hour format:")
         spin2 = wx.SpinButton( self, -1, wx.DefaultPosition, (-1,20), 0 )
-        self.time24 = timectl.TimeCtrl(
-                        self, -1, name="24 hour control", fmt24hr=True, 
-                        spinButton = spin2 
+        self.time24 = masked.TimeCtrl(
+                        self, -1, name="24 hour control", fmt24hr=True,
+                        spinButton = spin2
                         )
 
         text3 = wx.StaticText( self, -1, "No seconds\nor spin button:")
-        self.spinless_ctrl = timectl.TimeCtrl(
-                                self, -1, name="spinless control", 
-                                display_seconds = False 
+        self.spinless_ctrl = masked.TimeCtrl(
+                                self, -1, name="spinless control",
+                                display_seconds = False
                                 )
 
         grid = wx.FlexGridSizer( 0, 2, 10, 5 )
@@ -54,8 +54,8 @@ class TestPanel( scrolled.ScrolledPanel ):
 
         buttonChange = wx.Button( self, -1, "Change Controls")
         self.radio12to24 = wx.RadioButton(
-                            self, -1, "Copy 12-hour time to 24-hour control", 
-                            wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP 
+                            self, -1, "Copy 12-hour time to 24-hour control",
+                            wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP
                             )
 
         self.radio24to12 = wx.RadioButton(
@@ -86,17 +86,17 @@ class TestPanel( scrolled.ScrolledPanel ):
         self.set_bounds = wx.CheckBox( self, -1, "Set time bounds:" )
 
         minlabel = wx.StaticText( self, -1, "minimum time:" )
-        self.min = timectl.TimeCtrl( self, -1, name="min", display_seconds = False )
+        self.min = masked.TimeCtrl( self, -1, name="min", display_seconds = False )
         self.min.Enable( False )
 
         maxlabel = wx.StaticText( self, -1, "maximum time:" )
-        self.max = timectl.TimeCtrl( self, -1, name="max", display_seconds = False )
+        self.max = masked.TimeCtrl( self, -1, name="max", display_seconds = False )
         self.max.Enable( False )
 
         self.limit_check = wx.CheckBox( self, -1, "Limit control" )
 
         label = wx.StaticText( self, -1, "Resulting time control:" )
-        self.target_ctrl = timectl.TimeCtrl( self, -1, name="new" )
+        self.target_ctrl = masked.TimeCtrl( self, -1, name="new" )
 
         grid2 = wx.FlexGridSizer( 0, 2, 0, 0 )
         grid2.Add( (20, 0), 0, wx.ALIGN_LEFT|wx.ALL, 5 )
@@ -142,14 +142,14 @@ class TestPanel( scrolled.ScrolledPanel ):
         self.SetupScrolling()
 
         self.Bind(wx.EVT_BUTTON, self.OnButtonClick, buttonChange )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.time12 )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.time24 )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.spinless_ctrl )
+        self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.time12 )
+        self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.time24 )
+        self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.spinless_ctrl )
         self.Bind(wx.EVT_CHECKBOX, self.OnBoundsCheck, self.set_bounds )
         self.Bind(wx.EVT_CHECKBOX, self.SetTargetMinMax, self.limit_check )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.SetTargetMinMax, self.min )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.SetTargetMinMax, self.max )
-        self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.target_ctrl )
+        self.Bind(masked.EVT_TIMEUPDATE, self.SetTargetMinMax, self.min )
+        self.Bind(masked.EVT_TIMEUPDATE, self.SetTargetMinMax, self.max )
+        self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.target_ctrl )
 
 
     def OnTimeChange( self, event ):
@@ -204,7 +204,7 @@ class TestPanel( scrolled.ScrolledPanel ):
             min, max = None, None
 
         cur_min, cur_max = self.target_ctrl.GetBounds()
-
+        print cur_min, min
         if min and (min != cur_min): self.target_ctrl.SetMin( min )
         if max and (max != cur_max): self.target_ctrl.SetMax( max )
 
@@ -225,11 +225,11 @@ def runTest( frame, nb, log ):
     return win
 
 #----------------------------------------------------------------------
-
+import wx.lib.masked.timectrl as timectl
 overview = timectl.__doc__
 
 if __name__ == '__main__':
     import sys,os
     import run
-    run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
+    run.main(['', os.path.basename(sys.argv[0])])
 
index af1127d3e86a68efd46e2df9f91f59e27cc1a190..dcdf99190b61564bbb077f8cfdd584534dc69c8c 100644 (file)
@@ -17,6 +17,9 @@ Added some convenience methods to wx.Bitmap: SetSize, GetSize, and
 wx.EmptyBitmap can be called with a wx.Size (or a 2-element sequence)
 object too.  Similar changes were done for wx.Image as well.
 
+Added new MaskedEditControl code from Will Sadkin.  The modules are
+now locaed in their own sub-package, wx.lib.masked.  Demos updated.
+
 
 
 
index bf4e356de246727ac149346aeeab585ff7023331..3828d4dc728d4a96b058e483b41b672db4c12179 100644 (file)
@@ -619,6 +619,14 @@ Similarly, the wxSystemSettings backwards compatibiility aliases for
 GetSystemColour, GetSystemFont and GetSystemMetric have also gone into
 the bit-bucket.  Use GetColour, GetFont and GetMetric instead.
 
+Use the Python True/False constants instead of the true, TRUE, false,
+FALSE that used to be provided with wxPython.
+
+Use None instead of the ancient and should have been removed a long
+time ago wx.NULL alias.
+
+wx.TreeCtrl no longer needs to be passed the cookie variable as the
+2nd parameter.  It still returns it though, for use with GetNextChild.
 
 The wx.NO_FULL_REPAINT_ON_RESIZE style is now the default style for
 all windows.  The name still exists for compatibility, but it is set
@@ -667,3 +675,8 @@ functions in wxPython for parameters that are expecting an integer.
 If the object is not already an integer then it will be asked to
 convert itself to one.  A similar conversion fragment is in place for
 parameters that expect floating point values.
+
+**[Changed in 2.5.1.6]**  The MaskedEditCtrl modules have been moved
+to their own sub-package, wx.lib.masked.  See the docstrings and demo
+for changes in capabilities, usage, etc.
+
diff --git a/wxPython/wx/lib/masked/__init__.py b/wxPython/wx/lib/masked/__init__.py
new file mode 100644 (file)
index 0000000..1a5a59e
--- /dev/null
@@ -0,0 +1,20 @@
+#----------------------------------------------------------------------
+# Name:        wxPython.lib.masked
+# Purpose:     A package containing the masked edit controls
+#
+# Author:      Will Sadkin, Jeff Childers
+#
+# Created:     6-Mar-2004
+# RCS-ID:      $Id$
+# Copyright:   (c) 2004
+# License:     wxWidgets license
+#----------------------------------------------------------------------
+
+# import relevant external symbols into package namespace:
+from maskededit import *
+from textctrl   import BaseMaskedTextCtrl, TextCtrl
+from combobox   import BaseMaskedComboBox, ComboBox, MaskedComboBoxSelectEvent
+from numctrl    import NumCtrl, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, EVT_NUM, NumberUpdatedEvent
+from timectrl   import TimeCtrl, wxEVT_TIMEVAL_UPDATED, EVT_TIMEUPDATE, TimeUpdatedEvent
+from ipaddrctrl import IpAddrCtrl
+from ctrl       import Ctrl, controlTypes
diff --git a/wxPython/wx/lib/masked/combobox.py b/wxPython/wx/lib/masked/combobox.py
new file mode 100644 (file)
index 0000000..3619c39
--- /dev/null
@@ -0,0 +1,540 @@
+#----------------------------------------------------------------------------
+# Name:         masked.combobox.py
+# Authors:      Will Sadkin
+# Email:        wsadkin@nameconnector.com
+# Created:      02/11/2003
+# Copyright:    (c) 2003 by Will Sadkin, 2003
+# RCS-ID:       $Id$
+# License:      wxWidgets license
+#----------------------------------------------------------------------------
+#
+# This masked edit class allows for the semantics of masked controls
+# to be applied to combo boxes.
+#
+#----------------------------------------------------------------------------
+
+import  wx
+from wx.lib.masked import *
+
+# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
+# be a good place to implement the 2.3 logger class
+from wx.tools.dbg import Logger
+dbg = Logger()
+##dbg(enable=0)
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
+## events, we have to do it ourselves when we auto-complete.
+class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
+    def __init__(self, id, selection = 0, object=None):
+        wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id)
+
+        self.__selection = selection
+        self.SetEventObject(object)
+
+    def GetSelection(self):
+        """Retrieve the value of the control at the time
+        this event was generated."""
+        return self.__selection
+
+
+class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
+    """
+    This masked edit control adds the ability to use a masked input
+    on a combobox, and do auto-complete of such values.
+    """
+    def __init__( self, parent, id=-1, value = '',
+                  pos = wx.DefaultPosition,
+                  size = wx.DefaultSize,
+                  choices = [],
+                  style = wx.CB_DROPDOWN,
+                  validator = wx.DefaultValidator,
+                  name = "maskedComboBox",
+                  setupEventHandling = True,        ## setup event handling by default):
+                  **kwargs):
+
+
+        # This is necessary, because wxComboBox currently provides no
+        # method for determining later if this was specified in the
+        # constructor for the control...
+        self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY
+
+        kwargs['choices'] = choices                 ## set up maskededit to work with choice list too
+
+        ## Since combobox completion is case-insensitive, always validate same way
+        if not kwargs.has_key('compareNoCase'):
+            kwargs['compareNoCase'] = True
+
+        MaskedEditMixin.__init__( self, name, **kwargs )
+
+        self._choices = self._ctrl_constraints._choices
+##        dbg('self._choices:', self._choices)
+
+        if self._ctrl_constraints._alignRight:
+            choices = [choice.rjust(self._masklength) for choice in choices]
+        else:
+            choices = [choice.ljust(self._masklength) for choice in choices]
+
+        wx.ComboBox.__init__(self, parent, id, value='',
+                            pos=pos, size = size,
+                            choices=choices, style=style|wx.WANTS_CHARS,
+                            validator=validator,
+                            name=name)
+
+        self.controlInitialized = True
+
+        # Set control font - fixed width by default
+        self._setFont()
+
+        if self._autofit:
+            self.SetClientSize(self._CalcSize())
+
+        if value:
+            # ensure value is width of the mask of the control:
+            if self._ctrl_constraints._alignRight:
+                value = value.rjust(self._masklength)
+            else:
+                value = value.ljust(self._masklength)
+
+        if self.__readonly:
+            self.SetStringSelection(value)
+        else:
+            self._SetInitialValue(value)
+
+
+        self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice)
+        self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice)
+
+        if setupEventHandling:
+            ## Setup event handlers
+            self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
+            self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
+            self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
+            self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
+            self.Bind(wx.EVT_CHAR, self._OnChar )               ## handle each keypress
+            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown )         ## for special processing of up/down keys
+            self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## for processing the rest of the control keys
+                                                                ## (next in evt chain)
+            self.Bind(wx.EVT_TEXT, self._OnTextChange )         ## color control appropriately & keep
+                                                                ## track of previous value for undo
+
+
+
+    def __repr__(self):
+        return "<MaskedComboBox: %s>" % self.GetValue()
+
+
+    def _CalcSize(self, size=None):
+        """
+        Calculate automatic size if allowed; augment base mixin function
+        to account for the selector button.
+        """
+        size = self._calcSize(size)
+        return (size[0]+20, size[1])
+
+
+    def _GetSelection(self):
+        """
+        Allow mixin to get the text selection of this control.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return self.GetMark()
+
+    def _SetSelection(self, sel_start, sel_to):
+        """
+        Allow mixin to set the text selection of this control.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return self.SetMark( sel_start, sel_to )
+
+
+    def _GetInsertionPoint(self):
+        return self.GetInsertionPoint()
+
+    def _SetInsertionPoint(self, pos):
+        self.SetInsertionPoint(pos)
+
+
+    def _GetValue(self):
+        """
+        Allow mixin to get the raw value of the control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return self.GetValue()
+
+    def _SetValue(self, value):
+        """
+        Allow mixin to set the raw value of the control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        # For wxComboBox, ensure that values are properly padded so that
+        # if varying length choices are supplied, they always show up
+        # in the window properly, and will be the appropriate length
+        # to match the mask:
+        if self._ctrl_constraints._alignRight:
+            value = value.rjust(self._masklength)
+        else:
+            value = value.ljust(self._masklength)
+
+        # Record current selection and insertion point, for undo
+        self._prevSelection = self._GetSelection()
+        self._prevInsertionPoint = self._GetInsertionPoint()
+        wx.ComboBox.SetValue(self, value)
+        # text change events don't always fire, so we check validity here
+        # to make certain formatting is applied:
+        self._CheckValid()
+
+    def SetValue(self, value):
+        """
+        This function redefines the externally accessible .SetValue to be
+        a smart "paste" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+        if not self._mask:
+            wx.ComboBox.SetValue(value)   # revert to base control behavior
+            return
+        # else...
+        # empty previous contents, replacing entire value:
+        self._SetInsertionPoint(0)
+        self._SetSelection(0, self._masklength)
+
+        if( len(value) < self._masklength                # value shorter than control
+            and (self._isFloat or self._isInt)            # and it's a numeric control
+            and self._ctrl_constraints._alignRight ):   # and it's a right-aligned control
+            # try to intelligently "pad out" the value to the right size:
+            value = self._template[0:self._masklength - len(value)] + value
+##            dbg('padded value = "%s"' % value)
+
+        # For wxComboBox, ensure that values are properly padded so that
+        # if varying length choices are supplied, they always show up
+        # in the window properly, and will be the appropriate length
+        # to match the mask:
+        elif self._ctrl_constraints._alignRight:
+            value = value.rjust(self._masklength)
+        else:
+            value = value.ljust(self._masklength)
+
+
+        # make SetValue behave the same as if you had typed the value in:
+        try:
+            value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
+            if self._isFloat:
+                self._isNeg = False     # (clear current assumptions)
+                value = self._adjustFloat(value)
+            elif self._isInt:
+                self._isNeg = False     # (clear current assumptions)
+                value = self._adjustInt(value)
+            elif self._isDate and not self.IsValid(value) and self._4digityear:
+                value = self._adjustDate(value, fixcentury=True)
+        except ValueError:
+            # If date, year might be 2 digits vs. 4; try adjusting it:
+            if self._isDate and self._4digityear:
+                dateparts = value.split(' ')
+                dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
+                value = string.join(dateparts, ' ')
+##                dbg('adjusted value: "%s"' % value)
+                value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
+            else:
+                raise
+
+        self._SetValue(value)
+####        dbg('queuing insertion after .SetValue', self._masklength)
+        wx.CallAfter(self._SetInsertionPoint, self._masklength)
+        wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
+
+
+    def _Refresh(self):
+        """
+        Allow mixin to refresh the base control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        wx.ComboBox.Refresh(self)
+
+    def Refresh(self):
+        """
+        This function redefines the externally accessible .Refresh() to
+        validate the contents of the masked control as it refreshes.
+        NOTE: this must be done in the class derived from the base wx control.
+        """
+        self._CheckValid()
+        self._Refresh()
+
+
+    def _IsEditable(self):
+        """
+        Allow mixin to determine if the base control is editable with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return not self.__readonly
+
+
+    def Cut(self):
+        """
+        This function redefines the externally accessible .Cut to be
+        a smart "erase" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+        if self._mask:
+            self._Cut()             # call the mixin's Cut method
+        else:
+            wx.ComboBox.Cut(self)    # else revert to base control behavior
+
+
+    def Paste(self):
+        """
+        This function redefines the externally accessible .Paste to be
+        a smart "paste" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+        if self._mask:
+            self._Paste()           # call the mixin's Paste method
+        else:
+            wx.ComboBox.Paste(self)  # else revert to base control behavior
+
+
+    def Undo(self):
+        """
+        This function defines the undo operation for the control. (The default
+        undo is 1-deep.)
+        """
+        if self._mask:
+            self._Undo()
+        else:
+            wx.ComboBox.Undo()       # else revert to base control behavior
+
+
+    def Append( self, choice, clientData=None ):
+        """
+        This function override is necessary so we can keep track of any additions to the list
+        of choices, because wxComboBox doesn't have an accessor for the choice list.
+        The code here is the same as in the SetParameters() mixin function, but is
+        done for the individual value as appended, so the list can be built incrementally
+        without speed penalty.
+        """
+        if self._mask:
+            if type(choice) not in (types.StringType, types.UnicodeType):
+                raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
+            elif not self.IsValid(choice):
+                raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
+
+            if not self._ctrl_constraints._choices:
+                self._ctrl_constraints._compareChoices = []
+                self._ctrl_constraints._choices = []
+                self._hasList = True
+
+            compareChoice = choice.strip()
+
+            if self._ctrl_constraints._compareNoCase:
+                compareChoice = compareChoice.lower()
+
+            if self._ctrl_constraints._alignRight:
+                choice = choice.rjust(self._masklength)
+            else:
+                choice = choice.ljust(self._masklength)
+            if self._ctrl_constraints._fillChar != ' ':
+                choice = choice.replace(' ', self._fillChar)
+##            dbg('updated choice:', choice)
+
+
+            self._ctrl_constraints._compareChoices.append(compareChoice)
+            self._ctrl_constraints._choices.append(choice)
+            self._choices = self._ctrl_constraints._choices     # (for shorthand)
+
+            if( not self.IsValid(choice) and
+               (not self._ctrl_constraints.IsEmpty(choice) or
+                (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ):
+                raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name))
+
+        wx.ComboBox.Append(self, choice, clientData)
+
+
+
+    def Clear( self ):
+        """
+        This function override is necessary so we can keep track of any additions to the list
+        of choices, because wxComboBox doesn't have an accessor for the choice list.
+        """
+        if self._mask:
+            self._choices = []
+            self._ctrl_constraints._autoCompleteIndex = -1
+            if self._ctrl_constraints._choices:
+                self.SetCtrlParameters(choices=[])
+        wx.ComboBox.Clear(self)
+
+
+    def _OnCtrlParametersChanged(self):
+        """
+        Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
+        we can update the base control:
+        """
+        if self.controlInitialized and self._choices != self._ctrl_constraints._choices:
+            wx.ComboBox.Clear(self)
+            self._choices = self._ctrl_constraints._choices
+            for choice in self._choices:
+                wx.ComboBox.Append( self, choice )
+
+
+    def GetMark(self):
+        """
+        This function is a hack to make up for the fact that wxComboBox has no
+        method for returning the selected portion of its edit control.  It
+        works, but has the nasty side effect of generating lots of intermediate
+        events.
+        """
+##        dbg(suspend=1)  # turn off debugging around this function
+##        dbg('MaskedComboBox::GetMark', indent=1)
+        if self.__readonly:
+##            dbg(indent=0)
+            return 0, 0 # no selection possible for editing
+##        sel_start, sel_to = wxComboBox.GetMark(self)        # what I'd *like* to have!
+        sel_start = sel_to = self.GetInsertionPoint()
+##        dbg("current sel_start:", sel_start)
+        value = self.GetValue()
+##        dbg('value: "%s"' % value)
+
+        self._ignoreChange = True               # tell _OnTextChange() to ignore next event (if any)
+
+        wx.ComboBox.Cut(self)
+        newvalue = self.GetValue()
+##        dbg("value after Cut operation:", newvalue)
+
+        if newvalue != value:                   # something was selected; calculate extent
+##            dbg("something selected")
+            sel_to = sel_start + len(value) - len(newvalue)
+            wx.ComboBox.SetValue(self, value)    # restore original value and selection (still ignoring change)
+            wx.ComboBox.SetInsertionPoint(self, sel_start)
+            wx.ComboBox.SetMark(self, sel_start, sel_to)
+
+        self._ignoreChange = False              # tell _OnTextChange() to pay attn again
+
+##        dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
+        return sel_start, sel_to
+
+
+    def SetSelection(self, index):
+        """
+        Necessary for bookkeeping on choice selection, to keep current value
+        current.
+        """
+##        dbg('MaskedComboBox::SetSelection(%d)' % index)
+        if self._mask:
+            self._prevValue = self._curValue
+            self._curValue = self._choices[index]
+            self._ctrl_constraints._autoCompleteIndex = index
+        wx.ComboBox.SetSelection(self, index)
+
+
+    def OnKeyDown(self, event):
+        """
+        This function is necessary because navigation and control key
+        events do not seem to normally be seen by the wxComboBox's
+        EVT_CHAR routine.  (Tabs don't seem to be visible no matter
+        what... {:-( )
+        """
+        if event.GetKeyCode() in self._nav + self._control:
+            self._OnChar(event)
+            return
+        else:
+            event.Skip()    # let mixin default KeyDown behavior occur
+
+
+    def OnSelectChoice(self, event):
+        """
+        This function appears to be necessary, because the processing done
+        on the text of the control somehow interferes with the combobox's
+        selection mechanism for the arrow keys.
+        """
+##        dbg('MaskedComboBox::OnSelectChoice', indent=1)
+
+        if not self._mask:
+            event.Skip()
+            return
+
+        value = self.GetValue().strip()
+
+        if self._ctrl_constraints._compareNoCase:
+            value = value.lower()
+
+        if event.GetKeyCode() == wx.WXK_UP:
+            direction = -1
+        else:
+            direction = 1
+        match_index, partial_match = self._autoComplete(
+                                                direction,
+                                                self._ctrl_constraints._compareChoices,
+                                                value,
+                                                self._ctrl_constraints._compareNoCase,
+                                                current_index = self._ctrl_constraints._autoCompleteIndex)
+        if match_index is not None:
+##            dbg('setting selection to', match_index)
+            # issue appropriate event to outside:
+            self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
+            self._CheckValid()
+            keep_processing = False
+        else:
+            pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+            field = self._FindField(pos)
+            if self.IsEmpty() or not field._hasList:
+##                dbg('selecting 1st value in list')
+                self._OnAutoSelect(self._ctrl_constraints, match_index=0)
+                self._CheckValid()
+                keep_processing = False
+            else:
+                # attempt field-level auto-complete
+##                dbg(indent=0)
+                keep_processing = self._OnAutoCompleteField(event)
+##        dbg('keep processing?', keep_processing, indent=0)
+        return keep_processing
+
+
+    def _OnAutoSelect(self, field, match_index):
+        """
+        Override mixin (empty) autocomplete handler, so that autocompletion causes
+        combobox to update appropriately.
+        """
+##        dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
+##        field._autoCompleteIndex = match_index
+        if field == self._ctrl_constraints:
+            self.SetSelection(match_index)
+##            dbg('issuing combo selection event')
+            self.GetEventHandler().ProcessEvent(
+                MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
+        self._CheckValid()
+##        dbg('field._autoCompleteIndex:', match_index)
+##        dbg('self.GetSelection():', self.GetSelection())
+##        dbg(indent=0)
+
+
+    def _OnReturn(self, event):
+        """
+        For wxComboBox, it seems that if you hit return when the dropdown is
+        dropped, the event that dismisses the dropdown will also blank the
+        control, because of the implementation of wxComboBox.  So here,
+        we look and if the selection is -1, and the value according to
+        (the base control!) is a value in the list, then we schedule a
+        programmatic wxComboBox.SetSelection() call to pick the appropriate
+        item in the list. (and then do the usual OnReturn bit.)
+        """
+##        dbg('MaskedComboBox::OnReturn', indent=1)
+##        dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
+        if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
+            wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
+
+        event.m_keyCode = wx.WXK_TAB
+        event.Skip()
+##        dbg(indent=0)
+
+
+class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
+    """
+    This extra level of inheritance allows us to add the generic set of
+    masked edit parameters only to this class while allowing other
+    classes to derive from the "base" masked combobox control, and provide
+    a smaller set of valid accessor functions.
+    """
+    pass
+
+
diff --git a/wxPython/wx/lib/masked/ctrl.py b/wxPython/wx/lib/masked/ctrl.py
new file mode 100644 (file)
index 0000000..897d066
--- /dev/null
@@ -0,0 +1,109 @@
+#----------------------------------------------------------------------------
+# Name:         wxPython.lib.masked.ctrl.py
+# Author:       Will Sadkin
+# Created:      09/24/2003
+# Copyright:   (c) 2003 by Will Sadkin
+# RCS-ID:      $Id$
+# License:     wxWindows license
+#----------------------------------------------------------------------------
+# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Updated for wx namespace (minor)
+#
+# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Removed wx prefix
+#
+
+"""<html><body>
+<P>
+<B>masked.Ctrl</B> is actually a factory function for several types of
+masked edit controls:
+<P>
+<UL>
+    <LI><b>masked.TextCtrl</b>   - standard masked edit text box</LI>
+    <LI><b>masked.ComboBox</b>   - adds combobox capabilities</LI>
+    <LI><b>masked.IpAddrCtrl</b> - adds logical input semantics for IP address entry</LI>
+    <LI><b>masked.TimeCtrl</b>   - special subclass handling lots of time formats as values</LI>
+    <LI><b>masked.NumCtrl</b>    - special subclass handling numeric values</LI>
+</UL>
+<P>
+<B>masked.Ctrl</B> works by looking for a special <b><i>controlType</i></b>
+parameter in the variable arguments of the control, to determine
+what kind of instance to return.
+controlType can be one of:
+<PRE><FONT SIZE=-1>
+    controlTypes.TEXT
+    controlTypes.COMBO
+    controlTypes.IPADDR
+    controlTypes.TIME
+    controlTypes.NUMBER
+</FONT></PRE>
+These constants are also available individually, ie, you can
+use either of the following:
+<PRE><FONT SIZE=-1>
+    from wxPython.wx.lib.masked import Ctrl, COMBO, TEXT, NUMBER, TIME
+    from wxPython.wx.lib.masked import Ctrl, controlTypes
+</FONT></PRE>
+If not specified as a keyword argument, the default controlType is
+controlTypes.TEXT.
+<P>
+Each of the above classes has its own unique arguments, but MaskedCtrl
+provides a single "unified" interface for masked controls.  Masked.TextCtrl,
+masked.ComboBox and masked.IpAddrCtrl are all documented below; the others have
+their own demo pages and interface descriptions.
+</body></html>
+"""
+
+from wx.lib.masked   import TextCtrl, ComboBox, IpAddrCtrl
+from wx.lib.masked   import NumCtrl
+from wx.lib.masked   import TimeCtrl
+
+
+# "type" enumeration for class instance factory function
+TEXT        = 0
+COMBO       = 1
+IPADDR      = 2
+TIME        = 3
+NUMBER      = 4
+
+# for ease of import
+class controlTypes:
+    TEXT        = TEXT
+    COMBO       = COMBO
+    IPADDR      = IPADDR
+    TIME        = TIME
+    NUMBER      = NUMBER
+
+
+def Ctrl( *args, **kwargs):
+    """
+    Actually a factory function providing a unifying
+    interface for generating masked controls.
+    """
+    if not kwargs.has_key('controlType'):
+        controlType = TEXT
+    else:
+        controlType = kwargs['controlType']
+        del kwargs['controlType']
+
+    if controlType == TEXT:
+        return TextCtrl(*args, **kwargs)
+
+    elif controlType == COMBO:
+        return ComboBox(*args, **kwargs)
+
+    elif controlType == IPADDR:
+        return IpAddrCtrl(*args, **kwargs)
+
+    elif controlType == TIME:
+        return TimeCtrl(*args, **kwargs)
+
+    elif controlType == NUMBER:
+        return NumCtrl(*args, **kwargs)
+
+    else:
+        raise AttributeError(
+            "invalid controlType specified: %s" % repr(controlType))
+
+
diff --git a/wxPython/wx/lib/masked/ipaddrctrl.py b/wxPython/wx/lib/masked/ipaddrctrl.py
new file mode 100644 (file)
index 0000000..9fea97f
--- /dev/null
@@ -0,0 +1,187 @@
+#----------------------------------------------------------------------------
+# Name:         masked.ipaddrctrl.py
+# Authors:      Will Sadkin
+# Email:        wsadkin@nameconnector.com
+# Created:      02/11/2003
+# Copyright:    (c) 2003 by Will Sadkin, 2003
+# RCS-ID:       $Id$
+# License:      wxWidgets license
+#----------------------------------------------------------------------------
+# NOTE:
+#   Masked.IpAddrCtrl is a minor modification to masked.TextCtrl, that is
+#   specifically tailored for entering IP addresses.  It allows for
+#   right-insert fields and provides an accessor to obtain the entered
+#   address with extra whitespace removed.
+#
+#----------------------------------------------------------------------------
+
+import  wx
+from wx.lib.masked import BaseMaskedTextCtrl
+
+# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
+# be a good place to implement the 2.3 logger class
+from wx.tools.dbg import Logger
+dbg = Logger()
+##dbg(enable=0)
+
+class IpAddrCtrlAccessorsMixin:
+    # Define IpAddrCtrl's list of attributes having their own
+    # Get/Set functions, exposing only those that make sense for
+    # an IP address control.
+
+    exposed_basectrl_params = (
+        'fields',
+        'retainFieldValidation',
+        'formatcodes',
+        'fillChar',
+        'defaultValue',
+        'description',
+
+        'useFixedWidthFont',
+        'signedForegroundColour',
+        'emptyBackgroundColour',
+        'validBackgroundColour',
+        'invalidBackgroundColour',
+
+        'emptyInvalid',
+        'validFunc',
+        'validRequired',
+        )
+
+    for param in exposed_basectrl_params:
+        propname = param[0].upper() + param[1:]
+        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+        if param.find('Colour') != -1:
+            # add non-british spellings, for backward-compatibility
+            propname.replace('Colour', 'Color')
+
+            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+
+class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
+    """
+    This class is a particular type of MaskedTextCtrl that accepts
+    and understands the semantics of IP addresses, reformats input
+    as you move from field to field, and accepts '.' as a navigation
+    character, so that typing an IP address can be done naturally.
+    """
+
+
+
+    def __init__( self, parent, id=-1, value = '',
+                  pos = wx.DefaultPosition,
+                  size = wx.DefaultSize,
+                  style = wx.TE_PROCESS_TAB,
+                  validator = wx.DefaultValidator,
+                  name = 'IpAddrCtrl',
+                  setupEventHandling = True,        ## setup event handling by default
+                  **kwargs):
+
+        if not kwargs.has_key('mask'):
+           kwargs['mask'] = mask = "###.###.###.###"
+        if not kwargs.has_key('formatcodes'):
+            kwargs['formatcodes'] = 'F_Sr<'
+        if not kwargs.has_key('validRegex'):
+            kwargs['validRegex'] = "(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}"
+
+
+        BaseMaskedTextCtrl.__init__(
+                self, parent, id=id, value = value,
+                pos=pos, size=size,
+                style = style,
+                validator = validator,
+                name = name,
+                setupEventHandling = setupEventHandling,
+                **kwargs)
+
+
+        # set up individual field parameters as well:
+        field_params = {}
+        field_params['validRegex'] = "(   |  \d| \d |\d  | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))"
+
+        # require "valid" string; this prevents entry of any value > 255, but allows
+        # intermediate constructions; overall control validation requires well-formatted value.
+        field_params['formatcodes'] = 'V'
+
+        if field_params:
+            for i in self._field_indices:
+                self.SetFieldParameters(i, **field_params)
+
+        # This makes '.' act like tab:
+        self._AddNavKey('.', handler=self.OnDot)
+        self._AddNavKey('>', handler=self.OnDot)    # for "shift-."
+
+
+    def OnDot(self, event):
+##        dbg('IpAddrCtrl::OnDot', indent=1)
+        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+        oldvalue = self.GetValue()
+        edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
+        if not event.ShiftDown():
+            if pos > edit_start and pos < edit_end:
+                # clip data in field to the right of pos, if adjusting fields
+                # when not at delimeter; (assumption == they hit '.')
+                newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:]
+                self._SetValue(newvalue)
+                self._SetInsertionPoint(pos)
+##        dbg(indent=0)
+        return self._OnChangeField(event)
+
+
+
+    def GetAddress(self):
+        value = BaseMaskedTextCtrl.GetValue(self)
+        return value.replace(' ','')    # remove spaces from the value
+
+
+    def _OnCtrl_S(self, event):
+##        dbg("IpAddrCtrl::_OnCtrl_S")
+        if self._demo:
+            print "value:", self.GetAddress()
+        return False
+
+    def SetValue(self, value):
+##        dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1)
+        if type(value) not in (types.StringType, types.UnicodeType):
+##            dbg(indent=0)
+            raise ValueError('%s must be a string', str(value))
+
+        bValid = True   # assume True
+        parts = value.split('.')
+        if len(parts) != 4:
+            bValid = False
+        else:
+            for i in range(4):
+                part = parts[i]
+                if not 0 <= len(part) <= 3:
+                    bValid = False
+                    break
+                elif part.strip():  # non-empty part
+                    try:
+                        j = string.atoi(part)
+                        if not 0 <= j <= 255:
+                            bValid = False
+                            break
+                        else:
+                            parts[i] = '%3d' % j
+                    except:
+                        bValid = False
+                        break
+                else:
+                    # allow empty sections for SetValue (will result in "invalid" value,
+                    # but this may be useful for initializing the control:
+                    parts[i] = '   '    # convert empty field to 3-char length
+
+        if not bValid:
+##            dbg(indent=0)
+            raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value))
+        else:
+##            dbg('parts:', parts)
+            value = string.join(parts, '.')
+            BaseMaskedTextCtrl.SetValue(self, value)
+##        dbg(indent=0)
+
+
diff --git a/wxPython/wx/lib/masked/maskededit.py b/wxPython/wx/lib/masked/maskededit.py
new file mode 100644 (file)
index 0000000..1c528c9
--- /dev/null
@@ -0,0 +1,6676 @@
+#----------------------------------------------------------------------------
+# Name:         maskededit.py
+# Authors:      Jeff Childers, Will Sadkin
+# Email:        jchilders_98@yahoo.com, wsadkin@nameconnector.com
+# Created:      02/11/2003
+# Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
+# Portions:     (c) 2002 by Will Sadkin, 2002-2003
+# RCS-ID:       $Id$
+# License:      wxWindows license
+#----------------------------------------------------------------------------
+# NOTE:
+#   MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
+#   Jason Hihn, and borrows liberally from Will Sadkin's original masked edit
+#   control for time entry, TimeCtrl (which is now rewritten using this
+#   control!).
+#
+#   MaskedEdit controls do not normally use validators, because they do
+#   careful manipulation of the cursor in the text window on each keystroke,
+#   and validation is cursor-position specific, so the control intercepts the
+#   key codes before the validator would fire.  However, validators can be
+#   provided to do data transfer to the controls.
+#
+#----------------------------------------------------------------------------
+#
+# This file now contains the bulk of the logic behind all masked controls,
+# the MaskedEditMixin class, the Field class, and the autoformat codes.
+#
+#----------------------------------------------------------------------------
+#
+# 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com)
+#
+# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files,
+# o Reorganized code into masked package
+#
+# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Updated for wx namespace. No guarantees. This is one huge file.
+#
+# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Missed wx.DateTime stuff earlier.
+#
+# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o MaskedEditMixin -> MaskedEditMixin
+# o wxMaskedTextCtrl -> maskedTextCtrl
+# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent
+# o wxMaskedComboBox -> MaskedComboBox
+# o wxIpAddrCtrl -> IpAddrCtrl
+# o wxTimeCtrl -> TimeCtrl
+#
+
+"""\
+<b>Masked Edit Overview:
+=====================</b>
+<b>masked.TextCtrl</b>
+    is a sublassed text control that can carefully control the user's input
+    based on a mask string you provide.
+
+    General usage example:
+        control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####')
+
+    The example above will create a text control that allows only numbers to be
+    entered and then only in the positions indicated in the mask by the # sign.
+
+<b>masked.ComboBox</b>
+    is a similar subclass of wxComboBox that allows the same sort of masking,
+    but also can do auto-complete of values, and can require the value typed
+    to be in the list of choices to be colored appropriately.
+
+<b>masked.Ctrl</b>
+    is actually a factory function for several types of masked edit controls:
+
+    <b>masked.TextCtrl</b>   - standard masked edit text box
+    <b>masked.ComboBox</b>   - adds combobox capabilities
+    <b>masked.IpAddrCtrl</b> - adds special semantics for IP address entry
+    <b>masked.TimeCtrl</b>   - special subclass handling lots of types as values
+    <b>masked.NumCtrl</b>    - special subclass handling numeric values
+
+    It works by looking for a <b><i>controlType</i></b> parameter in the keyword
+    arguments of the control, to determine what kind of instance to return.
+    If not specified as a keyword argument, the default control type returned
+    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 wxMaskedCtrl()
+    to select the above control types.)
+
+
+<b>INITILIZATION PARAMETERS
+========================
+mask=</b>
+Allowed mask characters and function:
+    Character   Function
+            #       Allow numeric only (0-9)
+            N       Allow letters and numbers (0-9)
+            A       Allow uppercase letters only
+            a       Allow lowercase letters only
+            C       Allow any letter, upper or lower
+            X       Allow string.letters, string.punctuation, string.digits
+            &amp;       Allow string.punctuation only
+
+
+    These controls define these sets of characters using string.letters,
+    string.uppercase, etc.  These sets are affected by the system locale
+    setting, so in order to have the masked controls accept characters
+    that are specific to your users' language, your application should
+    set the locale.
+    For example, to allow international characters to be used in the
+    above masks, you can place the following in your code as part of
+    your application's initialization code:
+
+      import locale
+      locale.setlocale(locale.LC_ALL, '')
+
+
+  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
+  number, etc.  If any of these characters are needed as template rather
+  than mask characters, they can be escaped with \, ie. \N means "literal N".
+  (use \\ for literal backslash, as in: r'CCC\\NNN'.)
+
+
+  <b>Note:</b>
+      Masks containing only # characters and one optional decimal point
+      character are handled specially, as "numeric" controls.  Such
+      controls have special handling for typing the '-' key, handling
+      the "decimal point" character as truncating the integer portion,
+      optionally allowing grouping characters and so forth.
+      There are several parameters and format codes that only make sense
+      when combined with such masks, eg. groupChar, decimalChar, and so
+      forth (see below).  These allow you to construct reasonable
+      numeric entry controls.
+
+  <b>Note:</b>
+      Changing the mask for a control deletes any previous field classes
+      (and any associated validation or formatting constraints) for them.
+
+<b>useFixedWidthFont=</b>
+  By default, masked edit controls use a fixed width font, so that
+  the mask characters are fixed within the control, regardless of
+  subsequent modifications to the value.  Set to False if having
+  the control font be the same as other controls is required.
+
+
+<b>formatcodes=</b>
+  These other properties can be passed to the class when instantiating it:
+    Formatcodes are specified as a string of single character formatting
+    codes that modify  behavior of the control:
+            _  Allow spaces
+            !  Force upper
+            ^  Force lower
+            R  Right-align field(s)
+            r  Right-insert in field(s) (implies R)
+            &lt;  Stay in field until explicit navigation out of it
+
+            &gt;  Allow insert/delete within partially filled fields (as
+               opposed to the default "overwrite" mode for fixed-width
+               masked edit controls.)  This allows single-field controls
+               or each field within a multi-field control to optionally
+               behave more like standard text controls.
+               (See EMAIL or phone number autoformat examples.)
+
+               <i>Note: This also governs whether backspace/delete operations
+               shift contents of field to right of cursor, or just blank the
+               erased section.
+
+               Also, when combined with 'r', this indicates that the field
+               or control allows right insert anywhere within the current
+               non-empty value in the field.  (Otherwise right-insert behavior
+               is only performed to when the entire right-insertable field is
+               selected or the cursor is at the right edge of the field.</i>
+
+
+            ,  Allow grouping character in integer fields of numeric controls
+               and auto-group/regroup digits (if the result fits) when leaving
+               such a field.  (If specified, .SetValue() will attempt to
+               auto-group as well.)
+               ',' is also the default grouping character.  To change the
+               grouping character and/or decimal character, use the groupChar
+               and decimalChar parameters, respectively.
+               Note: typing the "decimal point" character in such fields will
+               clip the value to that left of the cursor for integer
+               fields of controls with "integer" or "floating point" masks.
+               If the ',' format code is specified, this will also cause the
+               resulting digits to be regrouped properly, using the current
+               grouping character.
+            -  Prepend and reserve leading space for sign to mask and allow
+               signed values (negative #s shown in red by default.) Can be
+               used with argument useParensForNegatives (see below.)
+            0  integer fields get leading zeros
+            D  Date[/time] field
+            T  Time field
+            F  Auto-Fit: the control calulates its size from
+               the length of the template mask
+            V  validate entered chars against validRegex before allowing them
+               to be entered vs. being allowed by basic mask and then having
+               the resulting value just colored as invalid.
+               (See USSTATE autoformat demo for how this can be used.)
+            S  select entire field when navigating to new field
+
+<b>fillChar=
+defaultValue=</b>
+  These controls have two options for the initial state of the control.
+  If a blank control with just the non-editable characters showing
+  is desired, simply leave the constructor variable fillChar as its
+  default (' ').  If you want some other character there, simply
+  change the fillChar to that value.  Note: changing the control's fillChar
+  will implicitly reset all of the fields' fillChars to this value.
+
+  If you need different default characters in each mask position,
+  you can specify a defaultValue parameter in the constructor, or
+  set them for each field individually.
+  This value must satisfy the non-editable characters of the mask,
+  but need not conform to the replaceable characters.
+
+<b>groupChar=
+decimalChar=</b>
+  These parameters govern what character is used to group numbers
+  and is used to indicate the decimal point for numeric format controls.
+  The default groupChar is ',', the default decimalChar is '.'
+  By changing these, you can customize the presentation of numbers
+  for your location.
+  eg: formatcodes = ',', groupChar="'"                   allows  12'345.34
+      formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34
+
+<b>shiftDecimalChar=</b>
+  The default "shiftDecimalChar" (used for "backwards-tabbing" until
+  shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for
+  other keyboards, you may want to customize this, eg '?' for shift ',' on
+  AZERTY keyboards, ':' or ';' for other European keyboards, etc.
+
+<b>useParensForNegatives=False</b>
+  This option can be used with signed numeric format controls to
+  indicate signs via () rather than '-'.
+
+<b>autoSelect=False</b>
+  This option can be used to have a field or the control try to
+  auto-complete on each keystroke if choices have been specified.
+
+<b>autoCompleteKeycodes=[]</b>
+  By default, DownArrow, PageUp and PageDown will auto-complete a
+  partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp
+  and PageDown will also auto-complete, but if the field already
+  contains a matched value, these keys will cycle through the list
+  of choices forward or backward as appropriate.  Shift-Up and
+  Shift-Down also take you to the next/previous field after any
+  auto-complete action.
+
+  Additional auto-complete keys can be specified via this parameter.
+  Any keys so specified will act like PageDown.
+
+
+
+<b>Validating User Input:
+======================</b>
+  There are a variety of initialization parameters that are used to validate
+  user input.  These parameters can apply to the control as a whole, and/or
+  to individual fields:
+
+        excludeChars=   A string of characters to exclude even if otherwise allowed
+        includeChars=   A string of characters to allow even if otherwise disallowed
+        validRegex=     Use a regular expression to validate the contents of the text box
+        validRange=     Pass a rangeas list (low,high) to limit numeric fields/values
+        choices=        A list of strings that are allowed choices for the control.
+        choiceRequired= value must be member of choices list
+        compareNoCase=  Perform case-insensitive matching when validating against list
+                        <i>Note: for masked.ComboBox, this defaults to True.</i>
+        emptyInvalid=   Boolean indicating whether an empty value should be considered invalid
+
+        validFunc=      A function to call of the form: bool = func(candidate_value)
+                        which will return True if the candidate_value satisfies some
+                        external criteria for the control in addition to the the
+                        other validation, or False if not.  (This validation is
+                        applied last in the chain of validations.)
+
+        validRequired=  Boolean indicating whether or not keys that are allowed by the
+                        mask, but result in an invalid value are allowed to be entered
+                        into the control.  Setting this to True implies that a valid
+                        default value is set for the control.
+
+        retainFieldValidation=
+                        False by default; if True, this allows individual fields to
+                        retain their own validation constraints independently of any
+                        subsequent changes to the control's overall parameters.
+
+        validator=      Validators are not normally needed for masked controls, because
+                        of the nature of the validation and control of input.  However,
+                        you can supply one to provide data transfer routines for the
+                        controls.
+
+
+<b>Coloring Behavior:
+==================</b>
+  The following parameters have been provided to allow you to change the default
+  coloring behavior of the control.   These can be set at construction, or via
+  the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow':
+
+        emptyBackgroundColour=      Control Background color when identified as empty. Default=White
+        invalidBackgroundColour=    Control Background color when identified as Not valid. Default=Yellow
+        validBackgroundColour=      Control Background color when identified as Valid. Default=white
+
+
+  The following parameters control the default foreground color coloring behavior of the
+  control. Pass a color as string e.g. 'Yellow':
+        foregroundColour=           Control foreground color when value is not negative.  Default=Black
+        signedForegroundColour=     Control foreground color when value is negative. Default=Red
+
+
+<b>Fields:
+=======</b>
+  Each part of the mask that allows user input is considered a field.  The fields
+  are represented by their own class instances.  You can specify field-specific
+  constraints by constructing or accessing the field instances for the control
+  and then specifying those constraints via parameters.
+
+<b>fields=</b>
+  This parameter allows you to specify Field instances containing
+  constraints for the individual fields of a control, eg: local
+  choice lists, validation rules, functions, regexps, etc.
+  It can be either an ordered list or a dictionary.  If a list,
+  the fields will be applied as fields 0, 1, 2, etc.
+  If a dictionary, it should be keyed by field index.
+  the values should be a instances of maskededit.Field.
+
+  Any field not represented by the list or dictionary will be
+  implicitly created by the control.
+
+  eg:
+    fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ]
+  or
+    fields = {
+              1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']),
+              3: ( Field(choices=['01', '02', '03'], choiceRequired=True)
+             }
+
+  The following parameters are available for individual fields, with the
+  same semantics as for the whole control but applied to the field in question:
+
+    fillChar        # if set for a field, it will override the control's fillChar for that field
+    groupChar       # if set for a field, it will override the control's default
+    defaultValue    # sets field-specific default value; overrides any default from control
+    compareNoCase   # overrides control's settings
+    emptyInvalid    # determines whether field is required to be filled at all times
+    validRequired   # if set, requires field to contain valid value
+
+  If any of the above parameters are subsequently specified for the control as a
+  whole, that new value will be propagated to each field, unless the
+  retainFieldValidation control-level parameter is set.
+
+    formatcodes     # Augments control's settings
+    excludeChars    #     '       '        '
+    includeChars    #     '       '        '
+    validRegex      #     '       '        '
+    validRange      #     '       '        '
+    choices         #     '       '        '
+    choiceRequired  #     '       '        '
+    validFunc       #     '       '        '
+
+
+
+<b>Control Class Functions:
+========================
+  .GetPlainValue(value=None)</b>
+                    Returns the value specified (or the control's text value
+                    not specified) without the formatting text.
+                    In the example above, might return phone no='3522640075',
+                    whereas control.GetValue() would return '(352) 264-0075'
+  <b>.ClearValue()</b>
+                    Returns the control's value to its default, and places the
+                    cursor at the beginning of the control.
+  <b>.SetValue()</b>
+                    Does "smart replacement" of passed value into the control, as does
+                    the .Paste() method.  As with other text entry controls, the
+                    .SetValue() text replacement begins at left-edge of the control,
+                    with missing mask characters inserted as appropriate.
+                    .SetValue will also adjust integer, float or date mask entry values,
+                    adding commas, auto-completing years, etc. as appropriate.
+                    For "right-aligned" numeric controls, it will also now automatically
+                    right-adjust any value whose length is less than the width of the
+                    control before attempting to set the value.
+                    If a value does not follow the format of the control's mask, or will
+                    not fit into the control, a ValueError exception will be raised.
+                    Eg:
+                      mask = '(###) ###-####'
+                          .SetValue('1234567890')           => '(123) 456-7890'
+                          .SetValue('(123)4567890')         => '(123) 456-7890'
+                          .SetValue('(123)456-7890')        => '(123) 456-7890'
+                          .SetValue('123/4567-890')         => illegal paste; ValueError
+
+                      mask = '#{6}.#{2}', formatcodes = '_,-',
+                          .SetValue('111')                  => ' 111   .  '
+                          .SetValue(' %9.2f' % -111.12345 ) => '   -111.12'
+                          .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00'
+                          .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError
+
+                      mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control
+                          .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit
+                          .SetValue('%.2f' % 111 )          => '    111.00'
+                          .SetValue('%.2f' % -111.12345 )   => '   -111.12'
+
+
+  <b>.IsValid(value=None)</b>
+                    Returns True if the value specified (or the value of the control
+                    if not specified) passes validation tests
+  <b>.IsEmpty(value=None)</b>
+                    Returns True if the value specified (or the value of the control
+                    if not specified) is equal to an "empty value," ie. all
+                    editable characters == the fillChar for their respective fields.
+  <b>.IsDefault(value=None)</b>
+                    Returns True if the value specified (or the value of the control
+                    if not specified) is equal to the initial value of the control.
+
+  <b>.Refresh()</b>
+                    Recolors the control as appropriate to its current settings.
+
+  <b>.SetCtrlParameters(**kwargs)</b>
+                    This function allows you to set up and/or change the control parameters
+                    after construction; it takes a list of key/value pairs as arguments,
+                    where the keys can be any of the mask-specific parameters in the constructor.
+                    Eg:
+                        ctl = masked.TextCtrl( self, -1 )
+                        ctl.SetCtrlParameters( mask='###-####',
+                                               defaultValue='555-1212',
+                                               formatcodes='F')
+
+  <b>.GetCtrlParameter(parametername)</b>
+                    This function allows you to retrieve the current value of a parameter
+                    from the control.
+
+  <b><i>Note:</i></b> Each of the control parameters can also be set using its
+      own Set and Get function.  These functions follow a regular form:
+      All of the parameter names start with lower case; for their
+      corresponding Set/Get function, the parameter name is capitalized.
+      Eg: ctl.SetMask('###-####')
+          ctl.SetDefaultValue('555-1212')
+          ctl.GetChoiceRequired()
+          ctl.GetFormatcodes()
+
+  <b><i>Note:</i></b> After any change in parameters, the choices for the
+      control are reevaluated to ensure that they are still legal.  If you
+      have large choice lists, it is therefore more efficient to set parameters
+      before setting the choices available.
+
+  <b>.SetFieldParameters(field_index, **kwargs)</b>
+                    This function allows you to specify change individual field
+                    parameters after construction. (Indices are 0-based.)
+
+  <b>.GetFieldParameter(field_index, parametername)</b>
+                    Allows the retrieval of field parameters after construction
+
+
+The control detects certain common constructions. In order to use the signed feature
+(negative numbers and coloring), the mask has to be all numbers with optionally one
+decimal point. Without a decimal (e.g. '######', the control will treat it as an integer
+value. With a decimal (e.g. '###.##'), the control will act as a floating point control
+(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
+integer control truncates the value.  However, for a true numeric control,
+masked.NumCtrl provides all this, and true numeric input/output support as well.
+
+
+Check your controls by calling each control's .IsValid() function and the
+.IsEmpty() function to determine which controls have been a) filled in and
+b) filled in properly.
+
+
+Regular expression validations can be used flexibly and creatively.
+Take a look at the demo; the zip-code validation succeeds as long as the
+first five numerals are entered. the last four are optional, but if
+any are entered, there must be 4 to be valid.
+
+<B>masked.Ctrl Configuration
+==========================</B>
+masked.Ctrl works by looking for a special <b><i>controlType</i></b>
+parameter in the variable arguments of the control, to determine
+what kind of instance to return.
+controlType can be one of:
+
+    controlTypes.TEXT
+    controlTypes.COMBO
+    controlTypes.IPADDR
+    controlTypes.TIME
+    controlTypes.NUMBER
+
+These constants are also available individually, ie, you can
+use either of the following:
+
+    from wxPython.wx.lib.masked import MaskedCtrl, controlTypes
+    from wxPython.wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR
+
+If not specified as a keyword argument, the default controlType is
+controlTypes.TEXT.
+"""
+
+"""
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+DEVELOPER COMMENTS:
+
+Naming Conventions
+------------------
+  All methods of the Mixin that are not meant to be exposed to the external
+  interface are prefaced with '_'.  Those functions that are primarily
+  intended to be internal subroutines subsequently start with a lower-case
+  letter; those that are primarily intended to be used and/or overridden
+  by derived subclasses start with a capital letter.
+
+  The following methods must be used and/or defined when deriving a control
+  from MaskedEditMixin.  NOTE: if deriving from a *masked edit* control
+  (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary,
+  as it's already been done for you in the base class.
+
+        ._SetInitialValue()
+                        This function must be called after the associated base
+                        control has been initialized in the subclass __init__
+                        function.  It sets the initial value of the control,
+                        either to the value specified if non-empty, the
+                        default value if specified, or the "template" for
+                        the empty control as necessary.  It will also set/reset
+                        the font if necessary and apply formatting to the
+                        control at this time.
+
+        ._GetSelection()
+                        REQUIRED
+                        Each class derived from MaskedEditMixin must define
+                        the function for getting the start and end of the
+                        current text selection.  The reason for this is
+                        that not all controls have the same function name for
+                        doing this; eg. wxTextCtrl uses .GetSelection(),
+                        whereas we had to write a .GetMark() function for
+                        wxComboBox, because .GetSelection() for the control
+                        gets the currently selected list item from the combo
+                        box, and the control doesn't (yet) natively provide
+                        a means of determining the text selection.
+        ._SetSelection()
+                        REQUIRED
+                        Similarly to _GetSelection, each class derived from
+                        MaskedEditMixin must define the function for setting
+                        the start and end of the current text selection.
+                        (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for
+                        masked.ComboBox.
+
+        ._GetInsertionPoint()
+        ._SetInsertionPoint()
+                        REQUIRED
+                        For consistency, and because the mixin shouldn't rely
+                        on fixed names for any manipulations it does of any of
+                        the base controls, we require each class derived from
+                        MaskedEditMixin to define these functions as well.
+
+        ._GetValue()
+        ._SetValue()    REQUIRED
+                        Each class derived from MaskedEditMixin must define
+                        the functions used to get and set the raw value of the
+                        control.
+                        This is necessary so that recursion doesn't take place
+                        when setting the value, and so that the mixin can
+                        call the appropriate function after doing all its
+                        validation and manipulation without knowing what kind
+                        of base control it was mixed in with.  To handle undo
+                        functionality, the ._SetValue() must record the current
+                        selection prior to setting the value.
+
+        .Cut()
+        .Paste()
+        .Undo()
+        .SetValue()     REQUIRED
+                        Each class derived from MaskedEditMixin must redefine
+                        these functions to call the _Cut(), _Paste(), _Undo()
+                        and _SetValue() methods, respectively for the control,
+                        so as to prevent programmatic corruption of the control's
+                        value.  This must be done in each derivation, as the
+                        mixin cannot itself override a member of a sibling class.
+
+        ._Refresh()     REQUIRED
+                        Each class derived from MaskedEditMixin must define
+                        the function used to refresh the base control.
+
+        .Refresh()      REQUIRED
+                        Each class derived from MaskedEditMixin must redefine
+                        this function so that it checks the validity of the
+                        control (via self._CheckValid) and then refreshes
+                        control using the base class method.
+
+        ._IsEditable()  REQUIRED
+                        Each class derived from MaskedEditMixin must define
+                        the function used to determine if the base control is
+                        editable or not.  (For masked.ComboBox, this has to
+                        be done with code, rather than specifying the proper
+                        function in the base control, as there isn't one...)
+        ._CalcSize()    REQUIRED
+                        Each class derived from MaskedEditMixin must define
+                        the function used to determine how wide the control
+                        should be given the mask.  (The mixin function
+                        ._calcSize() provides a baseline estimate.)
+
+
+Event Handling
+--------------
+  Event handlers are "chained", and MaskedEditMixin usually
+  swallows most of the events it sees, thereby preventing any other
+  handlers from firing in the chain.  It is therefore required that
+  each class derivation using the mixin to have an option to hook up
+  the event handlers itself or forego this operation and let a
+  subclass of the masked control do so.  For this reason, each
+  subclass should probably include the following code:
+
+    if setupEventHandling:
+        ## Setup event handlers
+        EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection
+        EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator
+        EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick
+        EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu
+        EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab.
+        EVT_CHAR( self, self._OnChar )              ## handle each keypress
+        EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep
+                                                            ## track of previous value for undo
+
+  where setupEventHandling is an argument to its constructor.
+
+  These 5 handlers must be "wired up" for the masked edit
+  controls to provide default behavior.  (The setupEventHandling
+  is an argument to masked.TextCtrl and masked.ComboBox, so
+  that controls derived from *them* may replace one of these
+  handlers if they so choose.)
+
+  If your derived control wants to preprocess events before
+  taking action, it should then set up the event handling itself,
+  so it can be first in the event handler chain.
+
+
+  The following routines are available to facilitate changing
+  the default behavior of masked edit controls:
+
+        ._SetKeycodeHandler(keycode, func)
+        ._SetKeyHandler(char, func)
+                        Use to replace default handling for any given keycode.
+                        func should take the key event as argument and return
+                        False if no further action is required to handle the
+                        key. Eg:
+                            self._SetKeycodeHandler(WXK_UP, self.IncrementValue)
+                            self._SetKeyHandler('-', self._OnChangeSign)
+
+        "Navigation" keys are assumed to change the cursor position, and
+        therefore don't cause automatic motion of the cursor as insertable
+        characters do.
+
+        ._AddNavKeycode(keycode, handler=None)
+        ._AddNavKey(char, handler=None)
+                        Allows controls to specify other keys (and optional handlers)
+                        to be treated as navigational characters. (eg. '.' in IpAddrCtrl)
+
+        ._GetNavKeycodes()  Returns the current list of navigational keycodes.
+
+        ._SetNavKeycodes(key_func_tuples)
+                        Allows replacement of the current list of keycode
+                        processed as navigation keys, and bind associated
+                        optional keyhandlers. argument is a list of key/handler
+                        tuples.  Passing a value of None for the handler in a
+                        given tuple indicates that default processing for the key
+                        is desired.
+
+        ._FindField(pos) Returns the Field object associated with this position
+                        in the control.
+
+        ._FindFieldExtent(pos, getslice=False, value=None)
+                        Returns edit_start, edit_end of the field corresponding
+                        to the specified position within the control, and
+                        optionally also returns the current contents of that field.
+                        If value is specified, it will retrieve the slice the corresponding
+                        slice from that value, rather than the current value of the
+                        control.
+
+        ._AdjustField(pos)
+                        This is, the function that gets called for a given position
+                        whenever the cursor is adjusted to leave a given field.
+                        By default, it adjusts the year in date fields if mask is a date,
+                        It can be overridden by a derived class to
+                        adjust the value of the control at that time.
+                        (eg. IpAddrCtrl reformats the address in this way.)
+
+        ._Change()      Called by internal EVT_TEXT handler. Return False to force
+                        skip of the normal class change event.
+        ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force
+                        skip of the normal class keypress event.
+        ._LostFocus()   Called by internal EVT_KILL_FOCUS handler
+
+        ._OnKeyDown(event)
+                        This is the default EVT_KEY_DOWN routine; it just checks for
+                        "navigation keys", and if event.ControlDown(), it fires the
+                        mixin's _OnChar() routine, as such events are not always seen
+                        by the "cooked" EVT_CHAR routine.
+
+        ._OnChar(event) This is the main EVT_CHAR handler for the
+                        MaskedEditMixin.
+
+    The following routines are used to handle standard actions
+    for control keys:
+        _OnArrow(event)         used for arrow navigation events
+        _OnCtrl_A(event)        'select all'
+        _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive)
+        _OnCtrl_S(event)        'save' (does nothing)
+        _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste
+        _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection
+        _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any)
+
+        _OnChangeField(event)   primarily used for tab events, but can be
+                                used for other keys (eg. '.' in IpAddrCtrl)
+
+        _OnErase(event)         used for backspace and delete
+        _OnHome(event)
+        _OnEnd(event)
+
+    The following routine provides a hook back to any class derivations, so that
+    they can react to parameter changes before any value is set/reset as a result of
+    those changes.  (eg. masked.ComboBox needs to detect when the choices list is
+    modified, either implicitly or explicitly, so it can reset the base control
+    to have the appropriate choice list *before* the initial value is reset to match.)
+
+        _OnCtrlParametersChanged()
+
+Accessor Functions
+------------------
+    For convenience, each class derived from MaskedEditMixin should
+    define an accessors mixin, so that it exposes only those parameters
+    that make sense for the derivation.  This is done with an intermediate
+    level of inheritance, ie:
+
+    class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ):
+
+    class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
+    class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
+    class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ):
+    class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
+    class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ):
+
+    etc.
+
+    Each accessors mixin defines Get/Set functions for the base class parameters
+    that are appropriate for that derivation.
+    This allows the base classes to be "more generic," exposing the widest
+    set of options, while not requiring derived classes to be so general.
+"""
+
+import  copy
+import  difflib
+import  re
+import  string
+import  types
+
+import  wx
+
+# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
+# be a good place to implement the 2.3 logger class
+from wx.tools.dbg import Logger
+
+dbg = Logger()
+##dbg(enable=0)
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## Constants for identifying control keys and classes of keys:
+
+WXK_CTRL_A = (ord('A')+1) - ord('A')   ## These keys are not already defined in wx
+WXK_CTRL_C = (ord('C')+1) - ord('A')
+WXK_CTRL_S = (ord('S')+1) - ord('A')
+WXK_CTRL_V = (ord('V')+1) - ord('A')
+WXK_CTRL_X = (ord('X')+1) - ord('A')
+WXK_CTRL_Z = (ord('Z')+1) - ord('A')
+
+nav = (
+    wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB,
+    wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT
+    )
+
+control = (
+    wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V,
+    WXK_CTRL_X, WXK_CTRL_Z
+    )
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## 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)
+## Note: locale settings affect what "uppercase", lowercase, etc comprise.
+##
+maskchars = ("#","A","a","X","C","N", '&')
+
+months = '(01|02|03|04|05|06|07|08|09|10|11|12)'
+charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
+charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
+                   'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
+
+days   = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)'
+hours  = '(0\d| \d|1[012])'
+milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)'
+minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
+16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\
+36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\
+56|57|58|59)"""
+seconds = minutes
+am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde'
+
+states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',')
+
+state_names = ['Alabama','Alaska','Arizona','Arkansas',
+               'California','Colorado','Connecticut',
+               'Delaware','District of Columbia',
+               'Florida','Georgia','Hawaii',
+               'Idaho','Illinois','Indiana','Iowa',
+               'Kansas','Kentucky','Louisiana',
+               'Maine','Maryland','Massachusetts','Michigan',
+               'Minnesota','Mississippi','Missouri','Montana',
+               'Nebraska','Nevada','New Hampshire','New Jersey',
+               'New Mexico','New York','North Carolina','North Dakokta',
+               'Ohio','Oklahoma','Oregon',
+               'Pennsylvania','Puerto Rico','Rhode Island',
+               'South Carolina','South Dakota',
+               'Tennessee','Texas','Utah',
+               'Vermont','Virginia',
+               'Washington','West Virginia',
+               'Wisconsin','Wyoming']
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## The following dictionary defines the current set of autoformats:
+
+masktags = {
+       "USPHONEFULLEXT": {
+           'mask': "(###) ###-#### x:###",
+           'formatcodes': 'F^->',
+           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
+           'description': "Phone Number w/opt. ext"
+           },
+       "USPHONETIGHTEXT": {
+           'mask': "###-###-#### x:###",
+           'formatcodes': 'F^->',
+           'validRegex': "^\d{3}-\d{3}-\d{4}",
+           'description': "Phone Number\n (w/hyphens and opt. ext)"
+           },
+       "USPHONEFULL": {
+           'mask': "(###) ###-####",
+           'formatcodes': 'F^->',
+           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
+           'description': "Phone Number only"
+           },
+       "USPHONETIGHT": {
+           'mask': "###-###-####",
+           'formatcodes': 'F^->',
+           'validRegex': "^\d{3}-\d{3}-\d{4}",
+           'description': "Phone Number\n(w/hyphens)"
+           },
+       "USSTATE": {
+           'mask': "AA",
+           'formatcodes': 'F!V',
+           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'),
+           'choices': states,
+           'choiceRequired': True,
+           'description': "US State Code"
+           },
+       "USSTATENAME": {
+           'mask': "ACCCCCCCCCCCCCCCCCCC",
+           'formatcodes': 'F_',
+           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'),
+           'choices': state_names,
+           'choiceRequired': True,
+           'description': "US State Name"
+           },
+
+       "USDATETIMEMMDDYYYY/HHMMSS": {
+           'mask': "##/##/#### ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "US Date + Time"
+           },
+       "USDATETIMEMMDDYYYY-HHMMSS": {
+           'mask': "##-##-#### ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "US Date + Time\n(w/hypens)"
+           },
+       "USDATE24HRTIMEMMDDYYYY/HHMMSS": {
+           'mask': "##/##/#### ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "US Date + 24Hr (Military) Time"
+           },
+       "USDATE24HRTIMEMMDDYYYY-HHMMSS": {
+           'mask': "##-##-#### ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "US Date + 24Hr Time\n(w/hypens)"
+           },
+       "USDATETIMEMMDDYYYY/HHMM": {
+           'mask': "##/##/#### ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "US Date + Time\n(without seconds)"
+           },
+       "USDATE24HRTIMEMMDDYYYY/HHMM": {
+           'mask': "##/##/#### ##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,
+           'description': "US Date + 24Hr Time\n(without seconds)"
+           },
+       "USDATETIMEMMDDYYYY-HHMM": {
+           'mask': "##-##-#### ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "US Date + Time\n(w/hypens and w/o secs)"
+           },
+       "USDATE24HRTIMEMMDDYYYY-HHMM": {
+           'mask': "##-##-#### ##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes,
+           'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)"
+           },
+       "USDATEMMDDYYYY/": {
+           'mask': "##/##/####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '/' + days + '/' + '\d{4}',
+           'description': "US Date\n(MMDDYYYY)"
+           },
+       "USDATEMMDDYY/": {
+           'mask': "##/##/##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '/' + days + '/\d\d',
+           'description': "US Date\n(MMDDYY)"
+           },
+       "USDATEMMDDYYYY-": {
+           'mask': "##-##-####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + months + '-' + days + '-' +'\d{4}',
+           'description': "MM-DD-YYYY"
+           },
+
+       "EUDATEYYYYMMDD/": {
+           'mask': "####/##/##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days,
+           'description': "YYYY/MM/DD"
+           },
+       "EUDATEYYYYMMDD.": {
+           'mask': "####.##.##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days,
+           'description': "YYYY.MM.DD"
+           },
+       "EUDATEDDMMYYYY/": {
+           'mask': "##/##/####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '/' + months + '/' + '\d{4}',
+           'description': "DD/MM/YYYY"
+           },
+       "EUDATEDDMMYYYY.": {
+           'mask': "##.##.####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '.' + months + '.' + '\d{4}',
+           'description': "DD.MM.YYYY"
+           },
+       "EUDATEDDMMMYYYY.": {
+           'mask': "##.CCC.####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}',
+           'description': "DD.Month.YYYY"
+           },
+       "EUDATEDDMMMYYYY/": {
+           'mask': "##/CCC/####",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}',
+           'description': "DD/Month/YYYY"
+           },
+
+       "EUDATETIMEYYYYMMDD/HHMMSS": {
+           'mask': "####/##/## ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "YYYY/MM/DD HH:MM:SS"
+           },
+       "EUDATETIMEYYYYMMDD.HHMMSS": {
+           'mask': "####.##.## ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "YYYY.MM.DD HH:MM:SS"
+           },
+       "EUDATETIMEDDMMYYYY/HHMMSS": {
+           'mask': "##/##/#### ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "DD/MM/YYYY HH:MM:SS"
+           },
+       "EUDATETIMEDDMMYYYY.HHMMSS": {
+           'mask': "##.##.#### ##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "DD.MM.YYYY HH:MM:SS"
+           },
+
+       "EUDATETIMEYYYYMMDD/HHMM": {
+           'mask': "####/##/## ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "YYYY/MM/DD HH:MM"
+           },
+       "EUDATETIMEYYYYMMDD.HHMM": {
+           'mask': "####.##.## ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "YYYY.MM.DD HH:MM"
+           },
+       "EUDATETIMEDDMMYYYY/HHMM": {
+           'mask': "##/##/#### ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "DD/MM/YYYY HH:MM"
+           },
+       "EUDATETIMEDDMMYYYY.HHMM": {
+           'mask': "##.##.#### ##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'DF!',
+           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
+           'description': "DD.MM.YYYY HH:MM"
+           },
+
+       "EUDATE24HRTIMEYYYYMMDD/HHMMSS": {
+           'mask': "####/##/## ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "YYYY/MM/DD 24Hr Time"
+           },
+       "EUDATE24HRTIMEYYYYMMDD.HHMMSS": {
+           'mask': "####.##.## ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "YYYY.MM.DD 24Hr Time"
+           },
+       "EUDATE24HRTIMEDDMMYYYY/HHMMSS": {
+           'mask': "##/##/#### ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "DD/MM/YYYY 24Hr Time"
+           },
+       "EUDATE24HRTIMEDDMMYYYY.HHMMSS": {
+           'mask': "##.##.#### ##:##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
+           'description': "DD.MM.YYYY 24Hr Time"
+           },
+       "EUDATE24HRTIMEYYYYMMDD/HHMM": {
+           'mask': "####/##/## ##:##",
+           'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes,
+           'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)"
+           },
+       "EUDATE24HRTIMEYYYYMMDD.HHMM": {
+           'mask': "####.##.## ##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes,
+           'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)"
+           },
+       "EUDATE24HRTIMEDDMMYYYY/HHMM": {
+           'mask': "##/##/#### ##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes,
+           'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)"
+           },
+       "EUDATE24HRTIMEDDMMYYYY.HHMM": {
+           'mask': "##.##.#### ##:##",
+           'formatcodes': 'DF',
+           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes,
+           'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)"
+           },
+
+       "TIMEHHMMSS": {
+           'mask': "##:##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'TF!',
+           'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
+           'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)"
+           },
+       "TIMEHHMM": {
+           'mask': "##:## AM",
+           'excludeChars': am_pm_exclude,
+           'formatcodes': 'TF!',
+           'validRegex': '^' + hours + ':' + minutes + ' (A|P)M',
+           'description': "HH:MM (A|P)M\n(see TimeCtrl)"
+           },
+       "24HRTIMEHHMMSS": {
+           'mask': "##:##:##",
+           'formatcodes': 'TF',
+           'validRegex': '^' + milhours + ':' + minutes + ':' + seconds,
+           'description': "24Hr HH:MM:SS\n(see TimeCtrl)"
+           },
+       "24HRTIMEHHMM": {
+           'mask': "##:##",
+           'formatcodes': 'TF',
+           'validRegex': '^' + milhours + ':' + minutes,
+           'description': "24Hr HH:MM\n(see TimeCtrl)"
+           },
+       "USSOCIALSEC": {
+           'mask': "###-##-####",
+           'formatcodes': 'F',
+           'validRegex': "\d{3}-\d{2}-\d{4}",
+           'description': "Social Sec#"
+           },
+       "CREDITCARD": {
+           'mask': "####-####-####-####",
+           'formatcodes': 'F',
+           'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}",
+           'description': "Credit Card"
+           },
+       "EXPDATEMMYY": {
+           'mask': "##/##",
+           'formatcodes': "F",
+           'validRegex': "^" + months + "/\d\d",
+           'description': "Expiration MM/YY"
+           },
+       "USZIP": {
+           'mask': "#####",
+           'formatcodes': 'F',
+           'validRegex': "^\d{5}",
+           'description': "US 5-digit zip code"
+           },
+       "USZIPPLUS4": {
+           'mask': "#####-####",
+           'formatcodes': 'F',
+           'validRegex': "\d{5}-(\s{4}|\d{4})",
+           'description': "US zip+4 code"
+           },
+       "PERCENT": {
+           'mask': "0.##",
+           'formatcodes': 'F',
+           'validRegex': "^0.\d\d",
+           'description': "Percentage"
+           },
+       "AGE": {
+           'mask': "###",
+           'formatcodes': "F",
+           'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]",
+           'description': "Age"
+           },
+       "EMAIL": {
+           'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
+           'excludeChars': " \\/*&%$#!+='\"",
+           'formatcodes': "F>",
+           'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$",
+           'description': "Email address"
+           },
+       "IPADDR": {
+           'mask': "###.###.###.###",
+           'formatcodes': 'F_Sr',
+           'validRegex': "(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}",
+           'description': "IP Address\n(see IpAddrCtrl)"
+           }
+       }
+
+# build demo-friendly dictionary of descriptions of autoformats
+autoformats = []
+for key, value in masktags.items():
+    autoformats.append((key, value['description']))
+autoformats.sort()
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class Field:
+    valid_params = {
+              'index': None,                    ## which field of mask; set by parent control.
+              'mask': "",                       ## mask chars for this field
+              'extent': (),                     ## (edit start, edit_end) of field; set by parent control.
+              'formatcodes':  "",               ## codes indicating formatting options for the control
+              'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given
+              'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits
+              'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction
+              'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard
+              'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers.
+              'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar
+              'excludeChars': "",               ## optional string of chars to exclude even if main mask type does
+              'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't
+              'validRegex':   "",               ## optional regular expression to use to validate the control
+              'validRange':   (),               ## Optional hi-low range for numerics
+              'choices':    [],                 ## Optional list for character expressions
+              'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list
+              'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search
+              'autoSelect': False,              ## Set to True to try auto-completion on each keystroke:
+              'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl
+              '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
+              }
+
+    # 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')
+
+    def __init__(self, **kwargs):
+        """
+        This is the "constructor" for setting up parameters for fields.
+        a field_index of -1 is used to indicate "the entire control."
+        """
+####        dbg('Field::Field', indent=1)
+        # Validate legitimate set of parameters:
+        for key in kwargs.keys():
+            if key not in Field.valid_params.keys():
+####                dbg(indent=0)
+                raise TypeError('invalid parameter "%s"' % (key))
+
+        # Set defaults for each parameter for this instance, and fully
+        # populate initial parameter list for configuration:
+        for key, value in Field.valid_params.items():
+            setattr(self, '_' + key, copy.copy(value))
+            if not kwargs.has_key(key):
+                kwargs[key] = copy.copy(value)
+
+        self._autoCompleteIndex = -1
+        self._SetParameters(**kwargs)
+        self._ValidateParameters(**kwargs)
+
+####        dbg(indent=0)
+
+
+    def _SetParameters(self, **kwargs):
+        """
+        This function can be used to set individual or multiple parameters for
+        a masked edit field parameter after construction.
+        """
+##        dbg(suspend=1)
+##        dbg('maskededit.Field::_SetParameters', indent=1)
+        # Validate keyword arguments:
+        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)
+
+        if self._index is not None: dbg('field index:', self._index)
+##        dbg('parameters:', indent=1)
+        for key, value in kwargs.items():
+##            dbg('%s:' % key, value)
+            pass
+##        dbg(indent=0)
+
+
+        old_fillChar = self._fillChar   # store so we can change choice lists accordingly if it changes
+
+        # First, Assign all parameters specified:
+        for key in Field.valid_params.keys():
+            if kwargs.has_key(key):
+                setattr(self, '_' + key, kwargs[key] )
+
+        if kwargs.has_key('formatcodes'):   # (set/changed)
+            self._forceupper  = '!' in self._formatcodes
+            self._forcelower  = '^' in self._formatcodes
+            self._groupdigits = ',' in self._formatcodes
+            self._okSpaces    = '_' in self._formatcodes
+            self._padZero     = '0' in self._formatcodes
+            self._autofit     = 'F' in self._formatcodes
+            self._insertRight = 'r' in self._formatcodes
+            self._allowInsert = '>' in self._formatcodes
+            self._alignRight  = 'R' in self._formatcodes or 'r' in self._formatcodes
+            self._moveOnFieldFull = not '<' in self._formatcodes
+            self._selectOnFieldEntry = 'S' in self._formatcodes
+
+            if kwargs.has_key('groupChar'):
+                self._groupChar = kwargs['groupChar']
+            if kwargs.has_key('decimalChar'):
+                self._decimalChar = kwargs['decimalChar']
+            if kwargs.has_key('shiftDecimalChar'):
+                self._shiftDecimalChar = kwargs['shiftDecimalChar']
+
+        if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'):
+            self._regexMask   = 'V' in self._formatcodes and self._validRegex
+
+        if kwargs.has_key('fillChar'):
+            self._old_fillChar = old_fillChar
+####            dbg("self._old_fillChar: '%s'" % self._old_fillChar)
+
+        if kwargs.has_key('mask') or kwargs.has_key('validRegex'):  # (set/changed)
+            self._isInt = isInteger(self._mask)
+##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask)
+
+##        dbg(indent=0, suspend=0)
+
+
+    def _ValidateParameters(self, **kwargs):
+        """
+        This function can be used to validate individual or multiple parameters for
+        a masked edit field parameter after construction.
+        """
+##        dbg(suspend=1)
+##        dbg('maskededit.Field::_ValidateParameters', indent=1)
+        if self._index is not None: dbg('field index:', self._index)
+####        dbg('parameters:', indent=1)
+##        for key, value in kwargs.items():
+####            dbg('%s:' % key, value)
+####        dbg(indent=0)
+####        dbg("self._old_fillChar: '%s'" % self._old_fillChar)
+
+        # 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))
+
+
+        # Now go do validation, semantic and inter-dependency parameter processing:
+        if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed)
+
+            self._compareChoices = [choice.strip() for choice in self._choices]
+
+            if self._compareNoCase and self._choices:
+                self._compareChoices = [item.lower() for item in self._compareChoices]
+
+            if kwargs.has_key('choices'):
+                self._autoCompleteIndex = -1
+
+
+        if kwargs.has_key('validRegex'):    # (set/changed)
+            if self._validRegex:
+                try:
+                    if self._compareNoCase:
+                        self._filter = re.compile(self._validRegex, re.IGNORECASE)
+                    else:
+                        self._filter = re.compile(self._validRegex)
+                except:
+##                    dbg(indent=0, suspend=0)
+                    raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
+            else:
+                self._filter = None
+
+        if kwargs.has_key('validRange'):    # (set/changed)
+            self._hasRange  = False
+            self._rangeHigh = 0
+            self._rangeLow  = 0
+            if self._validRange:
+                if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]:
+##                    dbg(indent=0, suspend=0)
+                    raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b'
+                                    % (str(self._index), repr(self._validRange)) )
+
+                self._hasRange  = True
+                self._rangeLow  = self._validRange[0]
+                self._rangeHigh = self._validRange[1]
+
+        if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)):       # (set/changed)
+            self._hasList   = False
+            if self._choices and type(self._choices) not in (types.TupleType, types.ListType):
+##                dbg(indent=0, suspend=0)
+                raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
+            elif len( self._choices) > 0:
+                for choice in self._choices:
+                    if type(choice) not in (types.StringType, types.UnicodeType):
+##                        dbg(indent=0, suspend=0)
+                        raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
+
+                length = len(self._mask)
+##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight)
+                if len(self._choices) and length:
+                    if len(self._choices[0]) > length:
+                        # changed mask without respecifying choices; readjust the width as appropriate:
+                        self._choices = [choice.strip() for choice in self._choices]
+                    if self._alignRight:
+                        self._choices = [choice.rjust( length ) for choice in self._choices]
+                    else:
+                        self._choices = [choice.ljust( length ) for choice in self._choices]
+##                    dbg('aligned choices:', self._choices)
+
+                if hasattr(self, '_template'):
+                    # Verify each choice specified is valid:
+                    for choice in self._choices:
+                        if self.IsEmpty(choice) and not self._validRequired:
+                            # allow empty values even if invalid, (just colored differently)
+                            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))
+                self._hasList = True
+
+####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
+####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar)
+        if kwargs.has_key('fillChar') and len(self._choices) > 0:
+            if kwargs['fillChar'] != ' ':
+                self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices]
+            else:
+                self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices]
+##            dbg('updated choices:', self._choices)
+
+
+        if kwargs.has_key('autoSelect') and kwargs['autoSelect']:
+            if not self._hasList:
+##                dbg('no list to auto complete; ignoring "autoSelect=True"')
+                self._autoSelect = False
+
+        # reset field validity assumption:
+        self._valid = True
+##        dbg(indent=0, suspend=0)
+
+
+    def _GetParameter(self, paramname):
+        """
+        Routine for retrieving the value of any given parameter
+        """
+        if Field.valid_params.has_key(paramname):
+            return getattr(self, '_' + paramname)
+        else:
+            TypeError('Field._GetParameter: invalid parameter "%s"' % key)
+
+
+    def IsEmpty(self, slice):
+        """
+        Indicates whether the specified slice is considered empty for the
+        field.
+        """
+##        dbg('Field::IsEmpty("%s")' % slice, indent=1)
+        if not hasattr(self, '_template'):
+##            dbg(indent=0)
+            raise AttributeError('_template')
+
+##        dbg('self._template: "%s"' % self._template)
+##        dbg('self._defaultValue: "%s"' % str(self._defaultValue))
+        if slice == self._template and not self._defaultValue:
+##            dbg(indent=0)
+            return True
+
+        elif slice == self._template:
+            empty = True
+            for pos in range(len(self._template)):
+####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos])
+                if slice[pos] not in (' ', self._fillChar):
+                    empty = False
+                    break
+##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0)
+            return empty
+        else:
+##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0)
+            return False
+
+
+    def IsValid(self, slice):
+        """
+        Indicates whether the specified slice is considered a valid value for the
+        field.
+        """
+##        dbg(suspend=1)
+##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1)
+        valid = True    # assume true to start
+
+        if self.IsEmpty(slice):
+##            dbg(indent=0, suspend=0)
+            if self._emptyInvalid:
+                return False
+            else:
+                return True
+
+        elif self._hasList and self._choiceRequired:
+##            dbg("(member of list required)")
+            # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices):
+            if self._fillChar != ' ':
+                slice = slice.replace(self._fillChar, ' ')
+##                dbg('updated slice:"%s"' % slice)
+            compareStr = slice.strip()
+
+            if self._compareNoCase:
+                compareStr = compareStr.lower()
+            valid = compareStr in self._compareChoices
+
+        elif self._hasRange and not self.IsEmpty(slice):
+##            dbg('validating against range')
+            try:
+                # allow float as well as int ranges (int comparisons for free.)
+                valid = self._rangeLow <= float(slice) <= self._rangeHigh
+            except:
+                valid = False
+
+        elif self._validRegex and self._filter:
+##            dbg('validating against regex')
+            valid = (re.match( self._filter, slice) is not None)
+
+        if valid and self._validFunc:
+##            dbg('validating against supplied function')
+            valid = self._validFunc(slice)
+##        dbg('valid?', valid, indent=0, suspend=0)
+        return valid
+
+
+    def _AdjustField(self, slice):
+        """ 'Fixes' an integer field. Right or left-justifies, as required."""
+##        dbg('Field::_AdjustField("%s")' % slice, indent=1)
+        length = len(self._mask)
+####        dbg('length(self._mask):', length)
+####        dbg('self._useParensForNegatives?', self._useParensForNegatives)
+        if self._isInt:
+            if self._useParensForNegatives:
+                signpos = slice.find('(')
+                right_signpos = slice.find(')')
+                intStr = slice.replace('(', '').replace(')', '')    # drop sign, if any
+            else:
+                signpos = slice.find('-')
+                intStr = slice.replace( '-', '' )                   # drop sign, if any
+                right_signpos = -1
+
+            intStr = intStr.replace(' ', '')                        # drop extra spaces
+            intStr = string.replace(intStr,self._fillChar,"")       # drop extra fillchars
+            intStr = string.replace(intStr,"-","")                  # drop sign, if any
+            intStr = string.replace(intStr, self._groupChar, "")    # lose commas/dots
+####            dbg('intStr:"%s"' % intStr)
+            start, end = self._extent
+            field_len = end - start
+            if not self._padZero and len(intStr) != field_len and intStr.strip():
+                intStr = str(long(intStr))
+####            dbg('raw int str: "%s"' % intStr)
+####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes)
+            if self._groupdigits:
+                new = ''
+                cnt = 1
+                for i in range(len(intStr)-1, -1, -1):
+                    new = intStr[i] + new
+                    if (cnt) % 3 == 0:
+                        new = self._groupChar + new
+                    cnt += 1
+                if new and new[0] == self._groupChar:
+                    new = new[1:]
+                if len(new) <= length:
+                    # expanded string will still fit and leave room for sign:
+                    intStr = new
+                # else... leave it without the commas...
+
+##            dbg('padzero?', self._padZero)
+##            dbg('len(intStr):', len(intStr), 'field length:', length)
+            if self._padZero and len(intStr) < length:
+                intStr = '0' * (length - len(intStr)) + intStr
+                if signpos != -1:   # we had a sign before; restore it
+                    if self._useParensForNegatives:
+                        intStr = '(' + intStr[1:]
+                        if right_signpos != -1:
+                            intStr += ')'
+                    else:
+                        intStr = '-' + intStr[1:]
+            elif signpos != -1 and slice[0:signpos].strip() == '':    # - was before digits
+                if self._useParensForNegatives:
+                    intStr = '(' + intStr
+                    if right_signpos != -1:
+                        intStr += ')'
+                else:
+                    intStr = '-' + intStr
+            elif right_signpos != -1:
+                # must have had ')' but '(' was before field; re-add ')'
+                intStr += ')'
+            slice = intStr
+
+        slice = slice.strip() # drop extra spaces
+
+        if self._alignRight:     ## Only if right-alignment is enabled
+            slice = slice.rjust( length )
+        else:
+            slice = slice.ljust( length )
+        if self._fillChar != ' ':
+            slice = slice.replace(' ', self._fillChar)
+##        dbg('adjusted slice: "%s"' % slice, indent=0)
+        return slice
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class MaskedEditMixin:
+    """
+    This class allows us to abstract the masked edit functionality that could
+    be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.)
+    """
+    valid_ctrl_params = {
+              'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control
+              'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary
+              'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask
+              'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year
+              'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete
+              'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control
+              'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently,
+                                                ## from field validation constraints
+              'emptyBackgroundColour': "White",
+              'validBackgroundColour': "White",
+              'invalidBackgroundColour': "Yellow",
+              'foregroundColour': "Black",
+              'signedForegroundColour': "Red",
+              'demo': False}
+
+
+    def __init__(self, name = 'MaskedEdit', **kwargs):
+        """
+        This is the "constructor" for setting up the mixin variable parameters for the composite class.
+        """
+
+        self.name = name
+
+        # set up flag for doing optional things to base control if possible
+        if not hasattr(self, 'controlInitialized'):
+            self.controlInitialized = False
+
+        # Set internal state var for keeping track of whether or not a character
+        # action results in a modification of the control, since .SetValue()
+        # doesn't modify the base control's internal state:
+        self.modified = False
+        self._previous_mask = None
+
+        # Validate legitimate set of parameters:
+        for key in kwargs.keys():
+            if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
+                raise TypeError('%s: invalid parameter "%s"' % (name, key))
+
+        ## Set up dictionary that can be used by subclasses to override or add to default
+        ## behavior for individual characters.  Derived subclasses needing to change
+        ## default behavior for keys can either redefine the default functions for the
+        ## common keys or add functions for specific keys to this list.  Each function
+        ## added should take the key event as argument, and return False if the key
+        ## requires no further processing.
+        ##
+        ## Initially populated with navigation and function control keys:
+        self._keyhandlers = {
+            # default navigation keys and handlers:
+            wx.WXK_BACK:   self._OnErase,
+            wx.WXK_LEFT:   self._OnArrow,
+            wx.WXK_RIGHT:  self._OnArrow,
+            wx.WXK_UP:     self._OnAutoCompleteField,
+            wx.WXK_DOWN:   self._OnAutoCompleteField,
+            wx.WXK_TAB:    self._OnChangeField,
+            wx.WXK_HOME:   self._OnHome,
+            wx.WXK_END:    self._OnEnd,
+            wx.WXK_RETURN: self._OnReturn,
+            wx.WXK_PRIOR:  self._OnAutoCompleteField,
+            wx.WXK_NEXT:   self._OnAutoCompleteField,
+
+            # default function control keys and handlers:
+            wx.WXK_DELETE: self._OnErase,
+            WXK_CTRL_A: self._OnCtrl_A,
+            WXK_CTRL_C: self._OnCtrl_C,
+            WXK_CTRL_S: self._OnCtrl_S,
+            WXK_CTRL_V: self._OnCtrl_V,
+            WXK_CTRL_X: self._OnCtrl_X,
+            WXK_CTRL_Z: self._OnCtrl_Z,
+            }
+
+        ## bind standard navigational and control keycodes to this instance,
+        ## so that they can be augmented and/or changed in derived classes:
+        self._nav = list(nav)
+        self._control = list(control)
+
+        ## Dynamically evaluate and store string constants for mask chars
+        ## so that locale settings can be made after this module is imported
+        ## and the controls created after that is done can allow the
+        ## appropriate characters:
+        self.maskchardict  = {
+            '#': string.digits,
+            'A': string.uppercase,
+            'a': string.lowercase,
+            'X': string.letters + string.punctuation + string.digits,
+            'C': string.letters,
+            'N': string.letters + string.digits,
+            '&': string.punctuation
+        }
+
+        ## self._ignoreChange is used by MaskedComboBox, because
+        ## of the hack necessary to determine the selection; it causes
+        ## EVT_TEXT messages from the combobox to be ignored if set.
+        self._ignoreChange = False
+
+        # These are used to keep track of previous value, for undo functionality:
+        self._curValue  = None
+        self._prevValue = None
+
+        self._valid     = True
+
+        # Set defaults for each parameter for this instance, and fully
+        # populate initial parameter list for configuration:
+        for key, value in MaskedEditMixin.valid_ctrl_params.items():
+            setattr(self, '_' + key, copy.copy(value))
+            if not kwargs.has_key(key):
+####                dbg('%s: "%s"' % (key, repr(value)))
+                kwargs[key] = copy.copy(value)
+
+        # Create a "field" that holds global parameters for control constraints
+        self._ctrl_constraints = self._fields[-1] = Field(index=-1)
+        self.SetCtrlParameters(**kwargs)
+
+
+
+    def SetCtrlParameters(self, **kwargs):
+        """
+        This public function can be used to set individual or multiple masked edit
+        parameters after construction.
+        """
+##        dbg(suspend=1)
+##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1)
+####        dbg('kwargs:', indent=1)
+##        for key, value in kwargs.items():
+####            dbg(key, '=', value)
+####        dbg(indent=0)
+
+        # Validate keyword arguments:
+        constraint_kwargs = {}
+        ctrl_kwargs = {}
+        for key, value in kwargs.items():
+            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))
+            elif key in Field.valid_params.keys():
+                constraint_kwargs[key] = value
+            else:
+                ctrl_kwargs[key] = value
+
+        mask = None
+        reset_args = {}
+
+        if ctrl_kwargs.has_key('autoformat'):
+            autoformat = ctrl_kwargs['autoformat']
+        else:
+            autoformat = None
+
+        # handle "parochial name" backward compatibility:
+        if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys():
+            autoformat = autoformat.replace('MILTIME', '24HRTIME')
+
+        if autoformat != self._autoformat and autoformat in masktags.keys():
+##            dbg('autoformat:', autoformat)
+            self._autoformat                  = autoformat
+            mask                              = masktags[self._autoformat]['mask']
+            # gather rest of any autoformat parameters:
+            for param, value in masktags[self._autoformat].items():
+                if param == 'mask': continue    # (must be present; already accounted for)
+                constraint_kwargs[param] = value
+
+        elif autoformat and not autoformat in masktags.keys():
+            raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
+        else:
+##            dbg('autoformat not selected')
+            if kwargs.has_key('mask'):
+                mask = kwargs['mask']
+##                dbg('mask:', mask)
+
+        ## Assign style flags
+        if mask is None:
+##            dbg('preserving previous mask')
+            mask = self._previous_mask   # preserve previous mask
+        else:
+##            dbg('mask (re)set')
+            reset_args['reset_mask'] = mask
+            constraint_kwargs['mask'] = mask
+
+            # wipe out previous fields; preserve new control-level constraints
+            self._fields = {-1: self._ctrl_constraints}
+
+
+        if ctrl_kwargs.has_key('fields'):
+            # do field parameter type validation, and conversion to internal dictionary
+            # as appropriate:
+            fields = ctrl_kwargs['fields']
+            if type(fields) in (types.ListType, types.TupleType):
+                for i in range(len(fields)):
+                    field = fields[i]
+                    if not isinstance(field, Field):
+##                        dbg(indent=0, suspend=0)
+                        raise AttributeError('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)
+                        raise AttributeError('invalid type for field parameter: %s' % repr(field))
+                    self._fields[index] = field
+            else:
+##                dbg(indent=0, suspend=0)
+                raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
+
+        # Assign constraint parameters for entire control:
+####        dbg('control constraints:', indent=1)
+##        for key, value in constraint_kwargs.items():
+####            dbg('%s:' % key, value)
+####        dbg(indent=0)
+
+        # determine if changing parameters that should affect the entire control:
+        for key in MaskedEditMixin.valid_ctrl_params.keys():
+            if key in ( 'mask', 'fields' ): continue    # (processed separately)
+            if ctrl_kwargs.has_key(key):
+                setattr(self, '_' + key, ctrl_kwargs[key])
+
+        # Validate color parameters, converting strings to named colors and validating
+        # result if appropriate:
+        for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour',
+                    'foregroundColour', 'signedForegroundColour'):
+            if ctrl_kwargs.has_key(key):
+                if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType):
+                    c = wx.NamedColour(ctrl_kwargs[key])
+                    if c.Get() == (-1, -1, -1):
+                        raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
+                    else:
+                        # replace attribute with wxColour object:
+                        setattr(self, '_' + key, c)
+                        # attach a python dynamic attribute to wxColour for debug printouts
+                        c._name = ctrl_kwargs[key]
+
+                elif type(ctrl_kwargs[key]) != type(wx.BLACK):
+                    raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
+
+
+##        dbg('self._retainFieldValidation:', self._retainFieldValidation)
+        if not self._retainFieldValidation:
+            # Build dictionary of any changing parameters which should be propagated to the
+            # component fields:
+            for arg in Field.propagating_params:
+####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg))
+####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg))
+                reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg)
+####                dbg('reset_args[%s]?' % arg, reset_args[arg])
+
+        # Set the control-level constraints:
+        self._ctrl_constraints._SetParameters(**constraint_kwargs)
+
+        # This routine does the bulk of the interdependent parameter processing, determining
+        # the field extents of the mask if changed, resetting parameters as appropriate,
+        # determining the overall template value for the control, etc.
+        self._configure(mask, **reset_args)
+
+        # now that we've propagated the field constraints and mask portions to the
+        # various fields, validate the constraints
+        self._ctrl_constraints._ValidateParameters(**constraint_kwargs)
+
+        # Validate that all choices for given fields are at least of the
+        # necessary length, and that they all would be valid pastes if pasted
+        # into their respective fields:
+####        dbg('validating choices')
+        self._validateChoices()
+
+
+        self._autofit = self._ctrl_constraints._autofit
+        self._isNeg      = False
+
+        self._isDate     = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask)
+        self._isTime     = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask)
+        if self._isDate:
+            # Set _dateExtent, used in date validation to locate date in string;
+            # always set as though year will be 4 digits, even if mask only has
+            # 2 digits, so we can always properly process the intended year for
+            # date validation (leap years, etc.)
+            if self._mask.find('CCC') != -1: self._dateExtent = 11
+            else:                            self._dateExtent = 10
+
+            self._4digityear = len(self._mask) > 8 and self._mask[9] == '#'
+
+        if self._isDate and self._autoformat:
+            # Auto-decide datestyle:
+            if self._autoformat.find('MDDY')    != -1: self._datestyle = 'MDY'
+            elif self._autoformat.find('YMMD')  != -1: self._datestyle = 'YMD'
+            elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD'
+            elif self._autoformat.find('DMMY')  != -1: self._datestyle = 'DMY'
+            elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY'
+
+        # Give derived controls a chance to react to parameter changes before
+        # potentially changing current value of the control.
+        self._OnCtrlParametersChanged()
+
+        if self.controlInitialized:
+            # Then the base control is available for configuration;
+            # take action on base control based on new settings, as appropriate.
+            if kwargs.has_key('useFixedWidthFont'):
+                # Set control font - fixed width by default
+                self._setFont()
+
+            if reset_args.has_key('reset_mask'):
+##                dbg('reset mask')
+                curvalue = self._GetValue()
+                if curvalue.strip():
+                    try:
+##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue())
+                        self._SetInitialValue(self._GetValue())
+                    except Exception, e:
+##                        dbg('exception caught:', e)
+##                        dbg("current value doesn't work; attempting to reset to template")
+                        self._SetInitialValue()
+                else:
+##                    dbg('attempting to _SetInitialValue() with template')
+                    self._SetInitialValue()
+
+            elif kwargs.has_key('useParensForNegatives'):
+                newvalue = self._getSignedValue()[0]
+
+                if newvalue is not None:
+                    # Adjust for new mask:
+                    if len(newvalue) < len(self._mask):
+                        newvalue += ' '
+                    elif len(newvalue) > len(self._mask):
+                        if newvalue[-1] in (' ', ')'):
+                            newvalue = newvalue[:-1]
+
+##                    dbg('reconfiguring value for parens:"%s"' % newvalue)
+                    self._SetValue(newvalue)
+
+                    if self._prevValue != newvalue:
+                        self._prevValue = newvalue  # disallow undo of sign type
+
+            if self._autofit:
+##                dbg('setting client size to:', self._CalcSize())
+                size = self._CalcSize()
+                self.SetSizeHints(size)
+                self.SetClientSize(size)
+
+            # Set value/type-specific formatting
+            self._applyFormatting()
+##        dbg(indent=0, suspend=0)
+
+    def SetMaskParameters(self, **kwargs):
+        """ old name for this function """
+        return self.SetCtrlParameters(**kwargs)
+
+
+    def GetCtrlParameter(self, paramname):
+        """
+        Routine for retrieving the value of any given parameter
+        """
+        if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')):
+            return getattr(self, '_' + paramname.replace('Color', 'Colour'))
+        elif Field.valid_params.has_key(paramname):
+            return self._ctrl_constraints._GetParameter(paramname)
+        else:
+            TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname))
+
+    def GetMaskParameter(self, paramname):
+        """ old name for this function """
+        return self.GetCtrlParameter(paramname)
+
+
+## This idea worked, but Boa was unable to use this solution...
+##    def _attachMethod(self, func):
+##        import new
+##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__))
+##
+##
+##    def _DefinePropertyFunctions(exposed_params):
+##        for param in exposed_params:
+##            propname = param[0].upper() + param[1:]
+##
+##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+##            self._attachMethod(locals()['Set%s' % propname])
+##            self._attachMethod(locals()['Get%s' % propname])
+##
+##            if param.find('Colour') != -1:
+##                # add non-british spellings, for backward-compatibility
+##                propname.replace('Colour', 'Color')
+##
+##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+##                self._attachMethod(locals()['Set%s' % propname])
+##                self._attachMethod(locals()['Get%s' % propname])
+##
+
+
+    def SetFieldParameters(self, field_index, **kwargs):
+        """
+        Routine provided to modify the parameters of a given field.
+        Because changes to fields can affect the overall control,
+        direct access to the fields is prevented, and the control
+        is always "reconfigured" after setting a field parameter.
+        """
+        if field_index not in self._field_indices:
+            raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
+        # set parameters as requested:
+        self._fields[field_index]._SetParameters(**kwargs)
+
+        # Possibly reprogram control template due to resulting changes, and ensure
+        # control-level params are still propagated to fields:
+        self._configure(self._previous_mask)
+        self._fields[field_index]._ValidateParameters(**kwargs)
+
+        if self.controlInitialized:
+            if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'):
+                self._SetInitialValue()
+
+                if self._autofit:
+                    size = self._CalcSize()
+                    self.SetSizeHints(size)
+                    self.SetClientSize(size)
+
+            # Set value/type-specific formatting
+            self._applyFormatting()
+
+
+    def GetFieldParameter(self, field_index, paramname):
+        """
+        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))
+        elif Field.valid_params.has_key(paramname):
+            return self._fields[field_index]._GetParameter(paramname)
+        else:
+            TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
+
+
+    def _SetKeycodeHandler(self, keycode, func):
+        """
+        This function adds and/or replaces key event handling functions
+        used by the control.  <func> should take the event as argument
+        and return False if no further action on the key is necessary.
+        """
+        self._keyhandlers[keycode] = func
+
+
+    def _SetKeyHandler(self, char, func):
+        """
+        This function adds and/or replaces key event handling functions
+        for ascii characters.  <func> should take the event as argument
+        and return False if no further action on the key is necessary.
+        """
+        self._SetKeycodeHandler(ord(char), func)
+
+
+    def _AddNavKeycode(self, keycode, handler=None):
+        """
+        This function allows a derived subclass to augment the list of
+        keycodes that are considered "navigational" keys.
+        """
+        self._nav.append(keycode)
+        if handler:
+            self._keyhandlers[keycode] = handler
+
+
+    def _AddNavKey(self, char, handler=None):
+        """
+        This function is a convenience function so you don't have to
+        remember to call ord() for ascii chars to be used for navigation.
+        """
+        self._AddNavKeycode(ord(char), handler)
+
+
+    def _GetNavKeycodes(self):
+        """
+        This function retrieves the current list of navigational keycodes for
+        the control.
+        """
+        return self._nav
+
+
+    def _SetNavKeycodes(self, keycode_func_tuples):
+        """
+        This function allows you to replace the current list of keycode processed
+        as navigation keys, and bind associated optional keyhandlers.
+        """
+        self._nav = []
+        for keycode, func in keycode_func_tuples:
+            self._nav.append(keycode)
+            if func:
+                self._keyhandlers[keycode] = func
+
+
+    def _processMask(self, mask):
+        """
+        This subroutine expands {n} syntax in mask strings, and looks for escaped
+        special characters and returns the expanded mask, and an dictionary
+        of booleans indicating whether or not a given position in the mask is
+        a mask character or not.
+        """
+##        dbg('_processMask: mask', mask, indent=1)
+        # regular expression for parsing c{n} syntax:
+        rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}')
+        s = mask
+        match = rex.search(s)
+        while match:    # found an(other) occurrence
+            maskchr = s[match.start(1):match.end(1)]            # char to be repeated
+            repcount = int(s[match.start(2):match.end(2)])      # the number of times
+            replacement = string.join( maskchr * repcount, "")  # the resulting substr
+            s = s[:match.start(1)] + replacement + s[match.end(2)+1:]   #account for trailing '}'
+            match = rex.search(s)                               # look for another such entry in mask
+
+        self._decimalChar = self._ctrl_constraints._decimalChar
+        self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar
+
+        self._isFloat      = isFloatingPoint(s) and not self._ctrl_constraints._validRegex
+        self._isInt      = isInteger(s) and not self._ctrl_constraints._validRegex
+        self._signOk     = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt)
+        self._useParens  = self._ctrl_constraints._useParensForNegatives
+        self._isNeg      = False
+####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens)
+####        dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s),
+##            'ctrl regex:', self._ctrl_constraints._validRegex)
+
+        if self._signOk and s[0] != ' ':
+            s = ' ' + s
+            if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ':
+                self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue
+            self._signpos = 0
+
+            if self._useParens:
+                s += ' '
+                self._ctrl_constraints._defaultValue += ' '
+
+        # Now, go build up a dictionary of booleans, indexed by position,
+        # indicating whether or not a given position is masked or not
+        ismasked = {}
+        i = 0
+        while i < len(s):
+            if s[i] == '\\':            # if escaped character:
+                ismasked[i] = False     #     mark position as not a mask char
+                if i+1 < len(s):        #     if another char follows...
+                    s = s[:i] + s[i+1:] #         elide the '\'
+                    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
+            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:', ismasked)
+##        dbg('new mask: "%s"' % s, indent=0)
+
+        return s, ismasked
+
+
+    def _calcFieldExtents(self):
+        """
+        Subroutine responsible for establishing/configuring field instances with
+        indices and editable extents appropriate to the specified mask, and building
+        the lookup table mapping each position to the corresponding field.
+        """
+        self._lookupField = {}
+        if self._mask:
+
+            ## Create dictionary of positions,characters in mask
+            self.maskdict = {}
+            for charnum in range( len( self._mask)):
+                self.maskdict[charnum] = self._mask[charnum:charnum+1]
+
+            # For the current mask, create an ordered list of field extents
+            # and a dictionary of positions that map to field indices:
+
+            if self._signOk: start = 1
+            else: start = 0
+
+            if self._isFloat:
+                # Skip field "discovery", and just construct a 2-field control with appropriate
+                # constraints for a floating-point entry.
+
+                # .setdefault always constructs 2nd argument even if not needed, so we do this
+                # the old-fashioned way...
+                if not self._fields.has_key(0):
+                    self._fields[0] = Field()
+                if not self._fields.has_key(1):
+                    self._fields[1] = Field()
+
+                self._decimalpos = string.find( self._mask, '.')
+##                dbg('decimal pos =', self._decimalpos)
+
+                formatcodes = self._fields[0]._GetParameter('formatcodes')
+                if 'R' not in formatcodes: formatcodes += 'R'
+                self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos),
+                                               mask=self._mask[start:self._decimalpos], formatcodes=formatcodes)
+                end = len(self._mask)
+                if self._signOk and self._useParens:
+                    end -= 1
+                self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end),
+                                               mask=self._mask[self._decimalpos+1:end])
+
+                for i in range(self._decimalpos+1):
+                    self._lookupField[i] = 0
+
+                for i in range(self._decimalpos+1, len(self._mask)+1):
+                    self._lookupField[i] = 1
+
+            elif self._isInt:
+                # Skip field "discovery", and just construct a 1-field control with appropriate
+                # constraints for a integer entry.
+                if not self._fields.has_key(0):
+                    self._fields[0] = Field(index=0)
+                end = len(self._mask)
+                if self._signOk and self._useParens:
+                    end -= 1
+                self._fields[0]._SetParameters(index=0, extent=(start, end),
+                                               mask=self._mask[start:end])
+                for i in range(len(self._mask)+1):
+                    self._lookupField[i] = 0
+            else:
+                # generic control; parse mask to figure out where the fields are:
+                field_index = 0
+                pos = 0
+                i = self._findNextEntry(pos,adjustInsert=False)  # go to 1st entry point:
+                if i < len(self._mask):   # no editable chars!
+                    for j in range(pos, i+1):
+                        self._lookupField[j] = field_index
+                    pos = i       # figure out field for 1st editable space:
+
+                while i <= len(self._mask):
+####                    dbg('searching: outer field loop: i = ', i)
+                    if self._isMaskChar(i):
+####                        dbg('1st char is mask char; recording edit_start=', i)
+                        edit_start = i
+                        # Skip to end of editable part of current field:
+                        while i < len(self._mask) and self._isMaskChar(i):
+                            self._lookupField[i] = field_index
+                            i += 1
+####                        dbg('edit_end =', i)
+                        edit_end = i
+                        self._lookupField[i] = field_index
+####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index))
+                        if not self._fields.has_key(field_index):
+                            kwargs = Field.valid_params.copy()
+                            kwargs['index'] = field_index
+                            kwargs['extent'] = (edit_start, edit_end)
+                            kwargs['mask'] = self._mask[edit_start:edit_end]
+                            self._fields[field_index] = Field(**kwargs)
+                        else:
+                            self._fields[field_index]._SetParameters(
+                                                                index=field_index,
+                                                                extent=(edit_start, edit_end),
+                                                                mask=self._mask[edit_start:edit_end])
+                    pos = i
+                    i = self._findNextEntry(pos, adjustInsert=False)  # go to next field:
+                    if i > pos:
+                        for j in range(pos, i+1):
+                            self._lookupField[j] = field_index
+                    if i >= len(self._mask):
+                        break           # if past end, we're done
+                    else:
+                        field_index += 1
+####                        dbg('next field:', field_index)
+
+        indices = self._fields.keys()
+        indices.sort()
+        self._field_indices = indices[1:]
+####        dbg('lookupField map:', indent=1)
+##        for i in range(len(self._mask)):
+####            dbg('pos %d:' % i, self._lookupField[i])
+####        dbg(indent=0)
+
+        # 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))
+
+
+    def _calcTemplate(self, reset_fillchar, reset_default):
+        """
+        Subroutine for processing current fillchars and default values for
+        whole control and individual fields, constructing the resulting
+        overall template, and adjusting the current value as necessary.
+        """
+        default_set = False
+        if self._ctrl_constraints._defaultValue:
+            default_set = True
+        else:
+            for field in self._fields.values():
+                if field._defaultValue and not reset_default:
+                    default_set = True
+##        dbg('default set?', default_set)
+
+        # Determine overall new template for control, and keep track of previous
+        # values, so that current control value can be modified as appropriate:
+        if self.controlInitialized: curvalue = list(self._GetValue())
+        else:                       curvalue = None
+
+        if hasattr(self, '_fillChar'): old_fillchars = self._fillChar
+        else:                          old_fillchars = None
+
+        if hasattr(self, '_template'): old_template = self._template
+        else:                          old_template = None
+
+        self._template = ""
+
+        self._fillChar = {}
+        reset_value = False
+
+        for field in self._fields.values():
+            field._template = ""
+
+        for pos in range(len(self._mask)):
+####            dbg('pos:', pos)
+            field = self._FindField(pos)
+####            dbg('field:', field._index)
+            start, end = field._extent
+
+            if pos == 0 and self._signOk:
+                self._template = ' ' # always make 1st 1st position blank, regardless of fillchar
+            elif self._isFloat and pos == self._decimalpos:
+                self._template += self._decimalChar
+            elif self._isMaskChar(pos):
+                if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar:
+                    fillChar = field._fillChar
+                else:
+                    fillChar = self._ctrl_constraints._fillChar
+                self._fillChar[pos] = fillChar
+
+                # Replace any current old fillchar with new one in current value;
+                # if action required, set reset_value flag so we can take that action
+                # after we're all done
+                if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue:
+                    if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar:
+                        reset_value = True
+                        curvalue[pos] = fillChar
+
+                if not field._defaultValue and not self._ctrl_constraints._defaultValue:
+####                    dbg('no default value')
+                    self._template += fillChar
+                    field._template += fillChar
+
+                elif field._defaultValue and not reset_default:
+####                    dbg('len(field._defaultValue):', len(field._defaultValue))
+####                    dbg('pos-start:', pos-start)
+                    if len(field._defaultValue) > pos-start:
+####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start])
+                        self._template += field._defaultValue[pos-start]
+                        field._template += field._defaultValue[pos-start]
+                    else:
+####                        dbg('field default not long enough; using fillChar')
+                        self._template += fillChar
+                        field._template += fillChar
+                else:
+                    if len(self._ctrl_constraints._defaultValue) > pos:
+####                        dbg('using control default')
+                        self._template += self._ctrl_constraints._defaultValue[pos]
+                        field._template += self._ctrl_constraints._defaultValue[pos]
+                    else:
+####                        dbg('ctrl default not long enough; using fillChar')
+                        self._template += fillChar
+                        field._template += fillChar
+####                dbg('field[%d]._template now "%s"' % (field._index, field._template))
+####                dbg('self._template now "%s"' % self._template)
+            else:
+                self._template += self._mask[pos]
+
+        self._fields[-1]._template = self._template     # (for consistency)
+
+        if curvalue:    # had an old value, put new one back together
+            newvalue = string.join(curvalue, "")
+        else:
+            newvalue = None
+
+        if default_set:
+            self._defaultValue = self._template
+##            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))
+
+            # if no fillchar change, but old value == old template, replace it:
+            if newvalue == old_template:
+                newvalue = self._template
+                reset_value = True
+        else:
+            self._defaultValue = None
+
+        if reset_value:
+##            dbg('resetting value to: "%s"' % newvalue)
+            pos = self._GetInsertionPoint()
+            sel_start, sel_to = self._GetSelection()
+            self._SetValue(newvalue)
+            self._SetInsertionPoint(pos)
+            self._SetSelection(sel_start, sel_to)
+
+
+    def _propagateConstraints(self, **reset_args):
+        """
+        Subroutine for propagating changes to control-level constraints and
+        formatting to the individual fields as appropriate.
+        """
+        parent_codes = self._ctrl_constraints._formatcodes
+        parent_includes = self._ctrl_constraints._includeChars
+        parent_excludes = self._ctrl_constraints._excludeChars
+        for i in self._field_indices:
+            field = self._fields[i]
+            inherit_args = {}
+            if len(self._field_indices) == 1:
+                inherit_args['formatcodes'] = parent_codes
+                inherit_args['includeChars'] = parent_includes
+                inherit_args['excludeChars'] = parent_excludes
+            else:
+                field_codes = current_codes = field._GetParameter('formatcodes')
+                for c in parent_codes:
+                    if c not in field_codes: field_codes += c
+                if field_codes != current_codes:
+                    inherit_args['formatcodes'] = field_codes
+
+                include_chars = current_includes = field._GetParameter('includeChars')
+                for c in parent_includes:
+                    if not c in include_chars: include_chars += c
+                if include_chars != current_includes:
+                    inherit_args['includeChars'] = include_chars
+
+                exclude_chars = current_excludes = field._GetParameter('excludeChars')
+                for c in parent_excludes:
+                    if not c in exclude_chars: exclude_chars += c
+                if exclude_chars != current_excludes:
+                    inherit_args['excludeChars'] = exclude_chars
+
+            if reset_args.has_key('defaultValue') and reset_args['defaultValue']:
+                inherit_args['defaultValue'] = ""   # (reset for field)
+
+            for param in Field.propagating_params:
+####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param))
+####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param])
+                if reset_args.has_key(param):
+                    inherit_args[param] = self.GetCtrlParameter(param)
+####                    dbg('inherit_args[%s]' % param, inherit_args[param])
+
+            if inherit_args:
+                field._SetParameters(**inherit_args)
+                field._ValidateParameters(**inherit_args)
+
+
+    def _validateChoices(self):
+        """
+        Subroutine that validates that all choices for given fields are at
+        least of the necessary length, and that they all would be valid pastes
+        if pasted into their respective fields.
+        """
+        for field in self._fields.values():
+            if field._choices:
+                index = field._index
+                if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices:
+##                    dbg('skipping (duplicate) choice validation of field 0')
+                    continue
+####                dbg('checking for choices for field', field._index)
+                start, end = field._extent
+                field_length = end - start
+####                dbg('start, end, length:', start, end, field_length)
+                for choice in field._choices:
+####                    dbg('testing "%s"' % choice)
+                    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))
+                    elif replace_to > end:
+####                        dbg(indent=0)
+                        raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
+####                    dbg(choice, 'valid in field', index)
+
+
+    def _configure(self, mask, **reset_args):
+        """
+        This function sets flags for automatic styling options.  It is
+        called whenever a control or field-level parameter is set/changed.
+
+        This routine does the bulk of the interdependent parameter processing, determining
+        the field extents of the mask if changed, resetting parameters as appropriate,
+        determining the overall template value for the control, etc.
+
+        reset_args is supplied if called from control's .SetCtrlParameters()
+        routine, and indicates which if any parameters which can be
+        overridden by individual fields have been reset by request for the
+        whole control.
+
+        """
+##        dbg(suspend=1)
+##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1)
+
+        # 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._masklength = len(self._mask)
+####        dbg('processed mask:', self._mask)
+
+        # Preserve original mask specified, for subsequent reprocessing
+        # if parameters change.
+##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask)
+        self._previous_mask = mask    # save unexpanded mask for next time
+            # Set expanded mask and extent of field -1 to width of entire control:
+        self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength))
+
+        # Go parse mask to determine where each field is, construct field
+        # instances as necessary, configure them with those extents, and
+        # build lookup table mapping each position for control to its corresponding
+        # field.
+####        dbg('calculating field extents')
+
+        self._calcFieldExtents()
+
+
+        # Go process defaultValues and fillchars to construct the overall
+        # template, and adjust the current value as necessary:
+        reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar']
+        reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue']
+
+####        dbg('calculating template')
+        self._calcTemplate(reset_fillchar, reset_default)
+
+        # Propagate control-level formatting and character constraints to each
+        # field if they don't already have them; if only one field, propagate
+        # control-level validation constraints to field as well:
+####        dbg('propagating constraints')
+        self._propagateConstraints(**reset_args)
+
+
+        if self._isFloat and self._fields[0]._groupChar == self._decimalChar:
+            raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' %
+                                 (self._fields[0]._groupChar, self._decimalChar) )
+
+####        dbg('fields:', indent=1)
+##        for i in [-1] + self._field_indices:
+####            dbg('field %d:' % i, self._fields[i].__dict__)
+####        dbg(indent=0)
+
+        # Set up special parameters for numeric control, if appropriate:
+        if self._signOk:
+            self._signpos = 0   # assume it starts here, but it will move around on floats
+            signkeys = ['-', '+', ' ']
+            if self._useParens:
+                signkeys += ['(', ')']
+            for key in signkeys:
+                keycode = ord(key)
+                if not self._keyhandlers.has_key(keycode):
+                    self._SetKeyHandler(key, self._OnChangeSign)
+
+
+
+        if self._isFloat or self._isInt:
+            if self.controlInitialized:
+                value = self._GetValue()
+####                dbg('value: "%s"' % value, 'len(value):', len(value),
+##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask))
+                if len(value) < len(self._ctrl_constraints._mask):
+                    newvalue = value
+                    if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1:
+                        newvalue += ' '
+                    if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1:
+                        newvalue = ' ' + newvalue
+                    if len(newvalue) < len(self._ctrl_constraints._mask):
+                        if self._ctrl_constraints._alignRight:
+                            newvalue = newvalue.rjust(len(self._ctrl_constraints._mask))
+                        else:
+                            newvalue = newvalue.ljust(len(self._ctrl_constraints._mask))
+##                    dbg('old value: "%s"' % value)
+##                    dbg('new value: "%s"' % newvalue)
+                    try:
+                        self._SetValue(newvalue)
+                    except Exception, e:
+##                        dbg('exception raised:', e, 'resetting to initial value')
+                        self._SetInitialValue()
+
+                elif len(value) > len(self._ctrl_constraints._mask):
+                    newvalue = value
+                    if not self._useParens and newvalue[-1] == ' ':
+                        newvalue = newvalue[:-1]
+                    if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask):
+                        newvalue = newvalue[1:]
+                    if not self._signOk:
+                        newvalue, signpos, right_signpos = self._getSignedValue(newvalue)
+
+##                    dbg('old value: "%s"' % value)
+##                    dbg('new value: "%s"' % newvalue)
+                    try:
+                        self._SetValue(newvalue)
+                    except Exception, e:
+##                        dbg('exception raised:', e, 'resetting to initial value')
+                        self._SetInitialValue()
+                elif not self._signOk and ('(' in value or '-' in value):
+                    newvalue, signpos, right_signpos = self._getSignedValue(value)
+##                    dbg('old value: "%s"' % value)
+##                    dbg('new value: "%s"' % newvalue)
+                    try:
+                        self._SetValue(newvalue)
+                    except e:
+##                        dbg('exception raised:', e, 'resetting to initial value')
+                        self._SetInitialValue()
+
+            # Replace up/down arrow default handling:
+            # make down act like tab, up act like shift-tab:
+
+####            dbg('Registering numeric navigation and control handlers (if not already set)')
+            if not self._keyhandlers.has_key(wx.WXK_DOWN):
+                self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField)
+            if not self._keyhandlers.has_key(wx.WXK_UP):
+                self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric)  # (adds "shift" to up arrow, and calls _OnChangeField)
+
+            # On ., truncate contents right of cursor to decimal point (if any)
+            # leaves cusor after decimal point if floating point, otherwise at 0.
+            if not self._keyhandlers.has_key(ord(self._decimalChar)):
+                self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint)
+            if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)):
+                self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField)   # (Shift-'.' == '>' on US keyboards)
+
+            # Allow selective insert of groupchar in numbers:
+            if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)):
+                self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar)
+
+##        dbg(indent=0, suspend=0)
+
+
+    def _SetInitialValue(self, value=""):
+        """
+        fills the control with the generated or supplied default value.
+        It will also set/reset the font if necessary and apply
+        formatting to the control at this time.
+        """
+##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
+        if not value:
+            self._prevValue = self._curValue = self._template
+            # don't apply external validation rules in this case, as template may
+            # not coincide with "legal" value...
+            try:
+                self._SetValue(self._curValue)  # note the use of "raw" ._SetValue()...
+            except Exception, e:
+##                dbg('exception thrown:', e, indent=0)
+                raise
+        else:
+            # Otherwise apply validation as appropriate to passed value:
+####            dbg('value = "%s", length:' % value, len(value))
+            self._prevValue = self._curValue = value
+            try:
+                self.SetValue(value)            # use public (validating) .SetValue()
+            except Exception, e:
+##                dbg('exception thrown:', e, indent=0)
+                raise
+
+
+        # Set value/type-specific formatting
+        self._applyFormatting()
+##        dbg(indent=0)
+
+
+    def _calcSize(self, size=None):
+        """ Calculate automatic size if allowed; must be called after the base control is instantiated"""
+####        dbg('MaskedEditMixin::_calcSize', indent=1)
+        cont = (size is None or size == wx.DefaultSize)
+
+        if cont and self._autofit:
+            sizing_text = 'M' * self._masklength
+            if wx.Platform != "__WXMSW__":   # give it a little extra space
+                sizing_text += 'M'
+            if wx.Platform == "__WXMAC__":   # give it even a little more...
+                sizing_text += 'M'
+####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
+            w, h = self.GetTextExtent(sizing_text)
+            size = (w+4, self.GetClientSize().height)
+####            dbg('size:', size, indent=0)
+        return size
+
+
+    def _setFont(self):
+        """ Set the control's font typeface -- pass the font name as str."""
+####        dbg('MaskedEditMixin::_setFont', indent=1)
+        if not self._useFixedWidthFont:
+            self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
+        else:
+            font = self.GetFont()   # get size, weight, etc from current font
+
+            # Set to teletype font (guaranteed to be mappable to all wxWindows
+            # platforms:
+            self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(),
+                                 font.GetWeight(), font.GetUnderlined())
+####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString())
+
+        self.SetFont(self._font)
+####        dbg(indent=0)
+
+
+    def _OnTextChange(self, event):
+        """
+        Handler for EVT_TEXT event.
+        self._Change() is provided for subclasses, and may return False to
+        skip this method logic.  This function returns True if the event
+        detected was a legitimate event, or False if it was a "bogus"
+        EVT_TEXT event.  (NOTE: There is currently an issue with calling
+        .SetValue from within the EVT_CHAR handler that causes duplicate
+        EVT_TEXT events for the same change.)
+        """
+        newvalue = self._GetValue()
+##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1)
+        bValid = False
+        if self._ignoreChange:      # ie. if an "intermediate text change event"
+##            dbg(indent=0)
+            return bValid
+
+        ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue
+        ## call is generating two (2) EVT_TEXT events.
+        ## This is the only mechanism I can find to mask this problem:
+        if newvalue == self._curValue:
+##            dbg('ignoring bogus text change event', indent=0)
+            pass
+        else:
+##            dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue))
+            if self._Change():
+                if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
+##                    dbg('clearing self._isNeg')
+                    self._isNeg = False
+                    text, self._signpos, self._right_signpos = self._getSignedValue()
+                self._CheckValid()  # Recolor control as appropriate
+##            dbg('calling event.Skip()')
+            event.Skip()
+            bValid = True
+        self._prevValue = self._curValue    # save for undo
+        self._curValue = newvalue           # Save last seen value for next iteration
+##        dbg(indent=0)
+        return bValid
+
+
+    def _OnKeyDown(self, event):
+        """
+        This function allows the control to capture Ctrl-events like Ctrl-tab,
+        that are not normally seen by the "cooked" EVT_CHAR routine.
+        """
+        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
+        key    = event.GetKeyCode()
+        if key in self._nav and event.ControlDown():
+            # then this is the only place we will likely see these events;
+            # process them now:
+##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar')
+            self._OnChar(event)
+            return
+        # else allow regular EVT_CHAR key processing
+        event.Skip()
+
+
+    def _OnChar(self, event):
+        """
+        This is the engine of MaskedEdit controls.  It examines each keystroke,
+        decides if it's allowed, where it should go or what action to take.
+        """
+##        dbg('MaskedEditMixin::_OnChar', indent=1)
+
+        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
+        key = event.GetKeyCode()
+        orig_pos = self._GetInsertionPoint()
+        orig_value = self._GetValue()
+##        dbg('keycode = ', key)
+##        dbg('current pos = ', orig_pos)
+##        dbg('current selection = ', self._GetSelection())
+
+        if not self._Keypress(key):
+##            dbg(indent=0)
+            return
+
+        # If no format string for this control, or the control is marked as "read-only",
+        # skip the rest of the special processing, and just "do the standard thing:"
+        if not self._mask or not self._IsEditable():
+            event.Skip()
+##            dbg(indent=0)
+            return
+
+        # Process navigation and control keys first, with
+        # position/selection unadulterated:
+        if key in self._nav + self._control:
+            if self._keyhandlers.has_key(key):
+                keep_processing = self._keyhandlers[key](event)
+                if self._GetValue() != orig_value:
+                    self.modified = True
+                if not keep_processing:
+##                    dbg(indent=0)
+                    return
+                self._applyFormatting()
+##                dbg(indent=0)
+                return
+
+        # Else... adjust the position as necessary for next input key,
+        # and determine resulting selection:
+        pos = self._adjustPos( orig_pos, key )    ## get insertion position, adjusted as needed
+        sel_start, sel_to = self._GetSelection()                ## check for a range of selected text
+##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to)
+
+        keep_processing = True
+        # Capture user past end of format field
+        if pos > len(self.maskdict):
+##            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):
+            # there's an override for default behavior; use override function instead
+##            dbg('using supplied key handler:', self._keyhandlers[key])
+            keep_processing = self._keyhandlers[key](event)
+            if self._GetValue() != orig_value:
+                self.modified = True
+            if not keep_processing:
+##                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
+            keep_processing = False
+        else:
+            field = self._FindField(pos)
+##            dbg("key ='%s'" % chr(key))
+            if chr(key) == ' ':
+##                dbg('okSpaces?', field._okSpaces)
+                pass
+
+
+            if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars:
+                keep_processing = False
+
+            if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ):
+##                dbg("key allowed by mask")
+                # insert key into candidate new value, but don't change control yet:
+                oldstr = self._GetValue()
+                newstr, newpos, new_select_to, match_field, match_index = self._insertKey(
+                                chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True)
+##                dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr)
+                if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
+##                    dbg('not valid; checking to see if adjusted string is:')
+                    keep_processing = False
+                    if self._isFloat and newstr != self._template:
+                        newstr = self._adjustFloat(newstr)
+##                        dbg('adjusted str:', newstr)
+                        if self.IsValid(newstr):
+##                            dbg("it is!")
+                            keep_processing = True
+                            wx.CallAfter(self._SetInsertionPoint, self._decimalpos)
+                    if not keep_processing:
+##                        dbg("key disallowed by validation")
+                        if not wx.Validator_IsSilent() and orig_pos == pos:
+                            wx.Bell()
+
+                if keep_processing:
+                    unadjusted = newstr
+
+                    # special case: adjust date value as necessary:
+                    if self._isDate and newstr != self._template:
+                        newstr = self._adjustDate(newstr)
+##                    dbg('adjusted newstr:', newstr)
+
+                    if newstr != orig_value:
+                        self.modified = True
+
+                    wx.CallAfter(self._SetValue, newstr)
+
+                    # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits:
+                    if not self.IsDefault() and self._isDate and self._4digityear:
+                        year2dig = self._dateExtent - 2
+                        if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]:
+                            newpos = pos+2
+
+                    wx.CallAfter(self._SetInsertionPoint, newpos)
+
+                    if match_field is not None:
+##                        dbg('matched field')
+                        self._OnAutoSelect(match_field, match_index)
+
+                    if new_select_to != newpos:
+##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to))
+                        wx.CallAfter(self._SetSelection, newpos, new_select_to)
+                    else:
+                        newfield = self._FindField(newpos)
+                        if newfield != field and newfield._selectOnFieldEntry:
+##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1]))
+                            wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
+                    keep_processing = False
+
+            elif keep_processing:
+##                dbg('char not allowed')
+                keep_processing = False
+                if (not wx.Validator_IsSilent()) and orig_pos == pos:
+                    wx.Bell()
+
+        self._applyFormatting()
+
+        # Move to next insertion point
+        if keep_processing and key not in self._nav:
+            pos = self._GetInsertionPoint()
+            next_entry = self._findNextEntry( pos )
+            if pos != next_entry:
+##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals())
+                wx.CallAfter(self._SetInsertionPoint, next_entry )
+
+            if self._isTemplateChar(pos):
+                self._AdjustField(pos)
+##        dbg(indent=0)
+
+
+    def _FindFieldExtent(self, pos=None, getslice=False, value=None):
+        """ returns editable extent of field corresponding to
+        position pos, and, optionally, the contents of that field
+        in the control or the value specified.
+        Template chars are bound to the preceding field.
+        For masks beginning with template chars, these chars are ignored
+        when calculating the current field.
+
+        Eg: with template (###) ###-####,
+        >>> self._FindFieldExtent(pos=0)
+        1, 4
+        >>> self._FindFieldExtent(pos=1)
+        1, 4
+        >>> self._FindFieldExtent(pos=5)
+        1, 4
+        >>> self._FindFieldExtent(pos=6)
+        6, 9
+        >>> self._FindFieldExtent(pos=10)
+        10, 14
+        etc.
+        """
+##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1)
+
+        field = self._FindField(pos)
+        if not field:
+            if getslice:
+                return None, None, ""
+            else:
+                return None, None
+        edit_start, edit_end = field._extent
+        if getslice:
+            if value is None: value = self._GetValue()
+            slice = value[edit_start:edit_end]
+##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice)
+##            dbg(indent=0)
+            return edit_start, edit_end, slice
+        else:
+##            dbg('edit_start:', edit_start, 'edit_end:', edit_end)
+##            dbg(indent=0)
+            return edit_start, edit_end
+
+
+    def _FindField(self, pos=None):
+        """
+        Returns the field instance in which pos resides.
+        Template chars are bound to the preceding field.
+        For masks beginning with template chars, these chars are ignored
+        when calculating the current field.
+
+        """
+####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
+        if pos is None: pos = self._GetInsertionPoint()
+        elif pos < 0 or pos > self._masklength:
+            raise IndexError('position %s out of range of control' % str(pos))
+
+        if len(self._fields) == 0:
+##            dbg(indent=0)
+            return None
+
+        # else...
+####        dbg(indent=0)
+        return self._fields[self._lookupField[pos]]
+
+
+    def ClearValue(self):
+        """ Blanks the current control value by replacing it with the default value."""
+##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)")
+        self._SetValue( self._template )
+        self._SetInsertionPoint(0)
+        self.Refresh()
+
+
+    def _baseCtrlEventHandler(self, event):
+        """
+        This function is used whenever a key should be handled by the base control.
+        """
+        event.Skip()
+        return False
+
+
+    def _OnUpNumeric(self, event):
+        """
+        Makes up-arrow act like shift-tab should; ie. take you to start of
+        previous field.
+        """
+##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1)
+        event.m_shiftDown = 1
+##        dbg('event.ShiftDown()?', event.ShiftDown())
+        self._OnChangeField(event)
+##        dbg(indent=0)
+
+
+    def _OnArrow(self, event):
+        """
+        Used in response to left/right navigation keys; makes these actions skip
+        over mask template chars.
+        """
+##        dbg("MaskedEditMixin::_OnArrow", indent=1)
+        pos = self._GetInsertionPoint()
+        keycode = event.GetKeyCode()
+        sel_start, sel_to = self._GetSelection()
+        entry_end = self._goEnd(getPosOnly=True)
+        if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN):
+            if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
+                or ( self._isTemplateChar(pos) and pos >= entry_end) ):
+##                dbg("can't advance", indent=0)
+                return False
+            elif self._isTemplateChar(pos):
+                self._AdjustField(pos)
+        elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1):
+##            dbg('adjusting field')
+            self._AdjustField(pos)
+
+        # treat as shifted up/down arrows as tab/reverse tab:
+        if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN):
+            # remove "shifting" and treat as (forward) tab:
+            event.m_shiftDown = False
+            keep_processing = self._OnChangeField(event)
+
+        elif self._FindField(pos)._selectOnFieldEntry:
+            if( keycode in (wx.WXK_UP, wx.WXK_LEFT)
+                and sel_start != 0
+                and self._isTemplateChar(sel_start-1)
+                and sel_start != self._masklength
+                and not self._signOk and not self._useParens):
+
+                # call _OnChangeField to handle "ctrl-shifted event"
+                # (which moves to previous field and selects it.)
+                event.m_shiftDown = True
+                event.m_ControlDown = True
+                keep_processing = self._OnChangeField(event)
+            elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT)
+                  and sel_to != self._masklength
+                  and self._isTemplateChar(sel_to)):
+
+                # when changing field to the right, ensure don't accidentally go left instead
+                event.m_shiftDown = False
+                keep_processing = self._OnChangeField(event)
+            else:
+                # treat arrows as normal, allowing selection
+                # as appropriate:
+##                dbg('using base ctrl event processing')
+                event.Skip()
+        else:
+            if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT)
+                or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ):
+                if not wx.Validator_IsSilent():
+                    wx.Bell()
+            else:
+                # treat arrows as normal, allowing selection
+                # as appropriate:
+##                dbg('using base event processing')
+                event.Skip()
+
+        keep_processing = False
+##        dbg(indent=0)
+        return keep_processing
+
+
+    def _OnCtrl_S(self, event):
+        """ Default Ctrl-S handler; prints value information if demo enabled. """
+##        dbg("MaskedEditMixin::_OnCtrl_S")
+        if self._demo:
+            print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue())
+            print "Valid? => " + str(self.IsValid())
+            print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True))
+        return False
+
+
+    def _OnCtrl_X(self, event=None):
+        """ Handles ctrl-x keypress in control and Cut operation on context menu.
+            Should return False to skip other processing. """
+##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1)
+        self.Cut()
+##        dbg(indent=0)
+        return False
+
+    def _OnCtrl_C(self, event=None):
+        """ Handles ctrl-C keypress in control and Copy operation on context menu.
+            Uses base control handling. Should return False to skip other processing."""
+        self.Copy()
+        return False
+
+    def _OnCtrl_V(self, event=None):
+        """ Handles ctrl-V keypress in control and Paste operation on context menu.
+            Should return False to skip other processing. """
+##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
+        self.Paste()
+##        dbg(indent=0)
+        return False
+
+    def _OnCtrl_Z(self, event=None):
+        """ Handles ctrl-Z keypress in control and Undo operation on context menu.
+            Should return False to skip other processing. """
+##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1)
+        self.Undo()
+##        dbg(indent=0)
+        return False
+
+    def _OnCtrl_A(self,event=None):
+        """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
+        end = self._goEnd(getPosOnly=True)
+        if not event or event.ShiftDown():
+            wx.CallAfter(self._SetInsertionPoint, 0)
+            wx.CallAfter(self._SetSelection, 0, self._masklength)
+        else:
+            wx.CallAfter(self._SetInsertionPoint, 0)
+            wx.CallAfter(self._SetSelection, 0, end)
+        return False
+
+
+    def _OnErase(self, event=None):
+        """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
+##        dbg("MaskedEditMixin::_OnErase", indent=1)
+        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
+
+        if event is None:   # called as action routine from Cut() operation.
+            key = wx.WXK_DELETE
+        else:
+            key = event.GetKeyCode()
+
+        field = self._FindField(sel_to)
+        start, end = field._extent
+        value = self._GetValue()
+        oldstart = sel_start
+
+        # If trying to erase beyond "legal" bounds, disallow operation:
+        if( (sel_to == 0 and key == wx.WXK_BACK)
+            or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK)
+            or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight)
+            or (self._signOk and self._useParens
+                and sel_start == sel_to
+                and sel_to == self._masklength - 1
+                and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ):
+            if not wx.Validator_IsSilent():
+                wx.Bell()
+##            dbg(indent=0)
+            return False
+
+
+        if( field._insertRight                                  # an insert-right field
+            and value[start:end] != self._template[start:end]   # and field not empty
+            and sel_start >= start                              # and selection starts in field
+            and ((sel_to == sel_start                           # and no selection
+                  and sel_to == end                             # and cursor at right edge
+                  and key in (wx.WXK_BACK, wx.WXK_DELETE))            # and either delete or backspace key
+                 or                                             # or
+                 (key == wx.WXK_BACK                               # backspacing
+                    and (sel_to == end                          # and selection ends at right edge
+                         or sel_to < end and field._allowInsert)) ) ):  # or allow right insert at any point in field
+
+##            dbg('delete left')
+            # if backspace but left of cursor is empty, adjust cursor right before deleting
+            while( key == wx.WXK_BACK
+                   and sel_start == sel_to
+                   and sel_start < end
+                   and value[start:sel_start] == self._template[start:sel_start]):
+                sel_start += 1
+                sel_to = sel_start
+
+##            dbg('sel_start, start:', sel_start, start)
+
+            if sel_start == sel_to:
+                keep = sel_start -1
+            else:
+                keep = sel_start
+            newfield = value[start:keep] + value[sel_to:end]
+
+            # handle sign char moving from outside field into the field:
+            move_sign_into_field = False
+            if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('):
+                signchar = value[0]
+                newfield = signchar + newfield
+                move_sign_into_field = True
+##            dbg('cut newfield: "%s"' % newfield)
+
+            # handle what should fill in from the left:
+            left = ""
+            for i in range(start, end - len(newfield)):
+                if field._padZero:
+                    left += '0'
+                elif( self._signOk and self._isNeg and i == 1
+                      and ((self._useParens and newfield.find('(') == -1)
+                           or (not self._useParens and newfield.find('-') == -1)) ):
+                    left += ' '
+                else:
+                    left += self._template[i]   # this can produce strange results in combination with default values...
+            newfield = left + newfield
+##            dbg('filled newfield: "%s"' % newfield)
+
+            newstr = value[:start] + newfield + value[end:]
+
+            # (handle sign located in "mask position" in front of field prior to delete)
+            if move_sign_into_field:
+                newstr = ' ' + newstr[1:]
+            pos = sel_to
+        else:
+            # handle erasure of (left) sign, moving selection accordingly...
+            if self._signOk and sel_start == 0:
+                newstr = value = ' ' + value[1:]
+                sel_start += 1
+
+            if field._allowInsert and sel_start >= start:
+                # selection (if any) falls within current insert-capable field:
+                select_len = sel_to - sel_start
+                # determine where cursor should end up:
+                if key == wx.WXK_BACK:
+                    if select_len == 0:
+                        newpos = sel_start -1
+                    else:
+                        newpos = sel_start
+                    erase_to = sel_to
+                else:
+                    newpos = sel_start
+                    if sel_to == sel_start:
+                        erase_to = sel_to + 1
+                    else:
+                        erase_to = sel_to
+
+                if self._isTemplateChar(newpos) and select_len == 0:
+                    if self._signOk:
+                        if value[newpos] in ('(', '-'):
+                            newpos += 1     # don't move cusor
+                            newstr = ' ' + value[newpos:]
+                        elif value[newpos] == ')':
+                            # erase right sign, but don't move cursor; (matching left sign handled later)
+                            newstr = value[:newpos] + ' '
+                        else:
+                            # no deletion; just move cursor
+                            newstr = value
+                    else:
+                        # no deletion; just move cursor
+                        newstr = value
+                else:
+                    if erase_to > end: erase_to = end
+                    erase_len = erase_to - newpos
+
+                    left = value[start:newpos]
+##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end])
+                    right = value[erase_to:end] + self._template[end-erase_len:end]
+                    pos_adjust = 0
+                    if field._alignRight:
+                        rstripped = right.rstrip()
+                        if rstripped != right:
+                            pos_adjust = len(right) - len(rstripped)
+                        right = rstripped
+
+                    if not field._insertRight and value[-1] == ')' and end == self._masklength - 1:
+                        # need to shift ) into the field:
+                        right = right[:-1] + ')'
+                        value = value[:-1] + ' '
+
+                    newfield = left+right
+                    if pos_adjust:
+                        newfield = newfield.rjust(end-start)
+                        newpos += pos_adjust
+##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield))
+                    newstr = value[:start] + newfield + value[end:]
+
+                pos = newpos
+
+            else:
+                if sel_start == sel_to:
+##                    dbg("current sel_start, sel_to:", sel_start, sel_to)
+                    if key == wx.WXK_BACK:
+                        sel_start, sel_to = sel_to-1, sel_to-1
+##                        dbg("new sel_start, sel_to:", sel_start, sel_to)
+
+                    if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''):
+                        # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0:
+                        newchar = '0'
+                    else:
+                        newchar = self._template[sel_to] ## get an original template character to "clear" the current char
+##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start]))
+
+                    if self._isTemplateChar(sel_to):
+                        if sel_to == 0 and self._signOk and value[sel_to] == '-':   # erasing "template" sign char
+                            newstr = ' ' + value[1:]
+                            sel_to += 1
+                        elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('):
+                            # allow "change sign" by removing both parens:
+                            newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' '
+                        else:
+                            newstr = value
+                        newpos = sel_to
+                    else:
+                        if field._insertRight and sel_start == sel_to:
+                            # force non-insert-right behavior, by selecting char to be replaced:
+                            sel_to += 1
+                        newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value)
+
+                else:
+                    # selection made
+                    newstr = self._eraseSelection(value, sel_start, sel_to)
+
+                pos = sel_start  # put cursor back at beginning of selection
+
+        if self._signOk and self._useParens:
+            # account for resultant unbalanced parentheses:
+            left_signpos = newstr.find('(')
+            right_signpos = newstr.find(')')
+
+            if left_signpos == -1 and right_signpos != -1:
+                # erased left-sign marker; get rid of right sign marker:
+                newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:]
+
+            elif left_signpos != -1 and right_signpos == -1:
+                # erased right-sign marker; get rid of left-sign marker:
+                newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:]
+
+##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart)
+##        dbg("newstr:'%s'" % newstr, 'pos:', pos)
+
+        # if erasure results in an invalid field, disallow it:
+##        dbg('field._validRequired?', field._validRequired)
+##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end]))
+        if field._validRequired and not field.IsValid(newstr[start:end]):
+            if not wx.Validator_IsSilent():
+                wx.Bell()
+##            dbg(indent=0)
+            return False
+
+        # if erasure results in an invalid value, disallow it:
+        if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
+            if not wx.Validator_IsSilent():
+                wx.Bell()
+##            dbg(indent=0)
+            return False
+
+##        dbg('setting value (later) to', newstr)
+        wx.CallAfter(self._SetValue, newstr)
+##        dbg('setting insertion point (later) to', pos)
+        wx.CallAfter(self._SetInsertionPoint, pos)
+##        dbg(indent=0)
+        if newstr != value:
+            self.modified = True
+        return False
+
+
+    def _OnEnd(self,event):
+        """ Handles End keypress in control. Should return False to skip other processing. """
+##        dbg("MaskedEditMixin::_OnEnd", indent=1)
+        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+        if not event.ControlDown():
+            end = self._masklength  # go to end of control
+            if self._signOk and self._useParens:
+                end = end - 1       # account for reserved char at end
+        else:
+            end_of_input = self._goEnd(getPosOnly=True)
+            sel_start, sel_to = self._GetSelection()
+            if sel_to < pos: sel_to = pos
+            field = self._FindField(sel_to)
+            field_end = self._FindField(end_of_input)
+
+            # pick different end point if either:
+            # - cursor not in same field
+            # - or at or past last input already
+            # - or current selection = end of current field:
+####            dbg('field != field_end?', field != field_end)
+####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input)
+            if field != field_end or sel_to >= end_of_input:
+                edit_start, edit_end = field._extent
+####                dbg('edit_end:', edit_end)
+####                dbg('sel_to:', sel_to)
+####                dbg('sel_to == edit_end?', sel_to == edit_end)
+####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1])
+
+                if sel_to == edit_end and field._index < self._field_indices[-1]:
+                    edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end))  # go to end of next field:
+                    end = edit_end
+##                    dbg('end moved to', end)
+
+                elif sel_to == edit_end and field._index == self._field_indices[-1]:
+                    # already at edit end of last field; select to end of control:
+                    end = self._masklength
+##                    dbg('end moved to', end)
+                else:
+                    end = edit_end  # select to end of current field
+##                    dbg('end moved to ', end)
+            else:
+                # select to current end of input
+                end = end_of_input
+
+
+####        dbg('pos:', pos, 'end:', end)
+
+        if event.ShiftDown():
+            if not event.ControlDown():
+##                dbg("shift-end; select to end of control")
+                pass
+            else:
+##                dbg("shift-ctrl-end; select to end of non-whitespace")
+                pass
+            wx.CallAfter(self._SetInsertionPoint, pos)
+            wx.CallAfter(self._SetSelection, pos, end)
+        else:
+            if not event.ControlDown():
+##                dbg('go to end of control:')
+                pass
+            wx.CallAfter(self._SetInsertionPoint, end)
+            wx.CallAfter(self._SetSelection, end, end)
+
+##        dbg(indent=0)
+        return False
+
+
+    def _OnReturn(self, event):
+         """
+         Changes the event to look like a tab event, so we can then call
+         event.Skip() on it, and have the parent form "do the right thing."
+         """
+##         dbg('MaskedEditMixin::OnReturn')
+         event.m_keyCode = wx.WXK_TAB
+         event.Skip()
+
+
+    def _OnHome(self,event):
+        """ Handles Home keypress in control. Should return False to skip other processing."""
+##        dbg("MaskedEditMixin::_OnHome", indent=1)
+        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+        sel_start, sel_to = self._GetSelection()
+
+        # There are 5 cases here:
+
+        # 1) shift: select from start of control to end of current
+        #    selection.
+        if event.ShiftDown() and not event.ControlDown():
+##            dbg("shift-home; select to start of control")
+            start = 0
+            end = sel_start
+
+        # 2) no shift, no control: move cursor to beginning of control.
+        elif not event.ControlDown():
+##            dbg("home; move to start of control")
+            start = 0
+            end = 0
+
+        # 3) No shift, control: move cursor back to beginning of field; if
+        #    there already, go to beginning of previous field.
+        # 4) shift, control, start of selection not at beginning of control:
+        #    move sel_start back to start of field; if already there, go to
+        #    start of previous field.
+        elif( event.ControlDown()
+              and (not event.ShiftDown()
+                   or (event.ShiftDown() and sel_start > 0) ) ):
+            if len(self._field_indices) > 1:
+                field = self._FindField(sel_start)
+                start, ignore = field._extent
+                if sel_start == start and field._index != self._field_indices[0]:  # go to start of previous field:
+                    start, ignore = self._FindFieldExtent(sel_start-1)
+                elif sel_start == start:
+                    start = 0   # go to literal beginning if edit start
+                                # not at that point
+                end_of_field = True
+
+            else:
+                start = 0
+
+            if not event.ShiftDown():
+##                dbg("ctrl-home; move to beginning of field")
+                end = start
+            else:
+##                dbg("shift-ctrl-home; select to beginning of field")
+                end = sel_to
+
+        else:
+        # 5) shift, control, start of selection at beginning of control:
+        #    unselect by moving sel_to backward to beginning of current field;
+        #    if already there, move to start of previous field.
+            start = sel_start
+            if len(self._field_indices) > 1:
+                # find end of previous field:
+                field = self._FindField(sel_to)
+                if sel_to > start and field._index != self._field_indices[0]:
+                    ignore, end = self._FindFieldExtent(field._extent[0]-1)
+                else:
+                    end = start
+                end_of_field = True
+            else:
+                end = start
+                end_of_field = False
+##            dbg("shift-ctrl-home; unselect to beginning of field")
+
+##        dbg('queuing new sel_start, sel_to:', (start, end))
+        wx.CallAfter(self._SetInsertionPoint, start)
+        wx.CallAfter(self._SetSelection, start, end)
+##        dbg(indent=0)
+        return False
+
+
+    def _OnChangeField(self, event):
+        """
+        Primarily handles TAB events, but can be used for any key that
+        designer wants to change fields within a masked edit control.
+        NOTE: at the moment, although coded to handle shift-TAB and
+        control-shift-TAB, these events are not sent to the controls
+        by the framework.
+        """
+##        dbg('MaskedEditMixin::_OnChangeField', indent = 1)
+        # determine end of current field:
+        pos = self._GetInsertionPoint()
+##        dbg('current pos:', pos)
+        sel_start, sel_to = self._GetSelection()
+
+        if self._masklength < 0:   # no fields; process tab normally
+            self._AdjustField(pos)
+            if event.GetKeyCode() == wx.WXK_TAB:
+##                dbg('tab to next ctrl')
+                event.Skip()
+            #else: do nothing
+##            dbg(indent=0)
+            return False
+
+
+        if event.ShiftDown():
+
+            # "Go backward"
+
+            # NOTE: doesn't yet work with SHIFT-tab under wx; the control
+            # never sees this event! (But I've coded for it should it ever work,
+            # and it *does* work for '.' in IpAddrCtrl.)
+            field = self._FindField(pos)
+            index = field._index
+            field_start = field._extent[0]
+            if pos < field_start:
+##                dbg('cursor before 1st field; cannot change to a previous field')
+                if not wx.Validator_IsSilent():
+                    wx.Bell()
+                return False
+
+            if event.ControlDown():
+##                dbg('queuing select to beginning of field:', field_start, pos)
+                wx.CallAfter(self._SetInsertionPoint, field_start)
+                wx.CallAfter(self._SetSelection, field_start, pos)
+##                dbg(indent=0)
+                return False
+
+            elif index == 0:
+                  # We're already in the 1st field; process shift-tab normally:
+                self._AdjustField(pos)
+                if event.GetKeyCode() == wx.WXK_TAB:
+##                    dbg('tab to previous ctrl')
+                    event.Skip()
+                else:
+##                    dbg('position at beginning')
+                    wx.CallAfter(self._SetInsertionPoint, field_start)
+##                dbg(indent=0)
+                return False
+            else:
+                # find beginning of previous field:
+                begin_prev = self._FindField(field_start-1)._extent[0]
+                self._AdjustField(pos)
+##                dbg('repositioning to', begin_prev)
+                wx.CallAfter(self._SetInsertionPoint, begin_prev)
+                if self._FindField(begin_prev)._selectOnFieldEntry:
+                    edit_start, edit_end = self._FindFieldExtent(begin_prev)
+##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end))
+                    wx.CallAfter(self._SetInsertionPoint, edit_start)
+                    wx.CallAfter(self._SetSelection, edit_start, edit_end)
+##                dbg(indent=0)
+                return False
+
+        else:
+            # "Go forward"
+            field = self._FindField(sel_to)
+            field_start, field_end = field._extent
+            if event.ControlDown():
+##                dbg('queuing select to end of field:', pos, field_end)
+                wx.CallAfter(self._SetInsertionPoint, pos)
+                wx.CallAfter(self._SetSelection, pos, field_end)
+##                dbg(indent=0)
+                return False
+            else:
+                if pos < field_start:
+##                    dbg('cursor before 1st field; go to start of field')
+                    wx.CallAfter(self._SetInsertionPoint, field_start)
+                    if field._selectOnFieldEntry:
+                        wx.CallAfter(self._SetSelection, field_start, field_end)
+                    else:
+                        wx.CallAfter(self._SetSelection, field_start, field_start)
+                    return False
+                # else...
+##                dbg('end of current field:', field_end)
+##                dbg('go to next field')
+                if field_end == self._fields[self._field_indices[-1]]._extent[1]:
+                    self._AdjustField(pos)
+                    if event.GetKeyCode() == wx.WXK_TAB:
+##                        dbg('tab to next ctrl')
+                        event.Skip()
+                    else:
+##                        dbg('position at end')
+                        wx.CallAfter(self._SetInsertionPoint, field_end)
+##                    dbg(indent=0)
+                    return False
+                else:
+                    # we have to find the start of the next field
+                    next_pos = self._findNextEntry(field_end)
+                    if next_pos == field_end:
+##                        dbg('already in last field')
+                        self._AdjustField(pos)
+                        if event.GetKeyCode() == wx.WXK_TAB:
+##                            dbg('tab to next ctrl')
+                            event.Skip()
+                        #else: do nothing
+##                        dbg(indent=0)
+                        return False
+                    else:
+                        self._AdjustField( pos )
+
+                        # move cursor to appropriate point in the next field and select as necessary:
+                        field = self._FindField(next_pos)
+                        edit_start, edit_end = field._extent
+                        if field._selectOnFieldEntry:
+##                            dbg('move to ', next_pos)
+                            wx.CallAfter(self._SetInsertionPoint, next_pos)
+                            edit_start, edit_end = self._FindFieldExtent(next_pos)
+##                            dbg('queuing select', edit_start, edit_end)
+                            wx.CallAfter(self._SetSelection, edit_start, edit_end)
+                        else:
+                            if field._insertRight:
+                                next_pos = field._extent[1]
+##                            dbg('move to ', next_pos)
+                            wx.CallAfter(self._SetInsertionPoint, next_pos)
+##                        dbg(indent=0)
+                        return False
+
+
+    def _OnDecimalPoint(self, event):
+##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
+
+        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+
+        if self._isFloat:       ## handle float value, move to decimal place
+##            dbg('key == Decimal tab; decimal pos:', self._decimalpos)
+            value = self._GetValue()
+            if pos < self._decimalpos:
+                clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:]
+##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
+                newstr = self._adjustFloat(clipped_text)
+            else:
+                newstr = self._adjustFloat(value)
+            wx.CallAfter(self._SetValue, newstr)
+            fraction = self._fields[1]
+            start, end = fraction._extent
+            wx.CallAfter(self._SetInsertionPoint, start)
+            if fraction._selectOnFieldEntry:
+##                dbg('queuing selection after decimal point to:', (start, end))
+                wx.CallAfter(self._SetSelection, start, end)
+            keep_processing = False
+
+        if self._isInt:      ## handle integer value, truncate from current position
+##            dbg('key == Integer decimal event')
+            value = self._GetValue()
+            clipped_text = value[0:pos]
+##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
+            newstr = self._adjustInt(clipped_text)
+##            dbg('newstr: "%s"' % newstr)
+            wx.CallAfter(self._SetValue, newstr)
+            newpos = len(newstr.rstrip())
+            if newstr.find(')') != -1:
+                newpos -= 1     # (don't move past right paren)
+            wx.CallAfter(self._SetInsertionPoint, newpos)
+            keep_processing = False
+##        dbg(indent=0)
+
+
+    def _OnChangeSign(self, event):
+##        dbg('MaskedEditMixin::_OnChangeSign', indent=1)
+        key = event.GetKeyCode()
+        pos = self._adjustPos(self._GetInsertionPoint(), key)
+        value = self._eraseSelection()
+        integer = self._fields[0]
+        start, end = integer._extent
+
+####        dbg('adjusted pos:', pos)
+        if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos):
+            cursign = self._isNeg
+##            dbg('cursign:', cursign)
+            if chr(key) in ('-','(', ')'):
+                self._isNeg = (not self._isNeg)   ## flip value
+            else:
+                self._isNeg = False
+##            dbg('isNeg?', self._isNeg)
+
+            text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value)
+##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos)
+            if text is None:
+                text = value
+
+            if self._isNeg and self._signpos is not None and self._signpos != -1:
+                if self._useParens and self._right_signpos is not None:
+                    text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:]
+                else:
+                    text = text[:self._signpos] + '-' + text[self._signpos+1:]
+            else:
+####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text))
+                if self._useParens:
+                    text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:]
+                else:
+                    text = text[:self._signpos] + ' ' + text[self._signpos+1:]
+##                dbg('clearing self._isNeg')
+                self._isNeg = False
+
+            wx.CallAfter(self._SetValue, text)
+            wx.CallAfter(self._applyFormatting)
+##            dbg('pos:', pos, 'signpos:', self._signpos)
+            if pos == self._signpos or integer.IsEmpty(text[start:end]):
+                wx.CallAfter(self._SetInsertionPoint, self._signpos+1)
+            else:
+                wx.CallAfter(self._SetInsertionPoint, pos)
+
+            keep_processing = False
+        else:
+            keep_processing = True
+##        dbg(indent=0)
+        return keep_processing
+
+
+    def _OnGroupChar(self, event):
+        """
+        This handler is only registered if the mask is a numeric mask.
+        It allows the insertion of ',' or '.' if appropriate.
+        """
+##        dbg('MaskedEditMixin::_OnGroupChar', indent=1)
+        keep_processing = True
+        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+        sel_start, sel_to = self._GetSelection()
+        groupchar = self._fields[0]._groupChar
+        if not self._isCharAllowed(groupchar, pos, checkRegex=True):
+            keep_processing = False
+            if not wx.Validator_IsSilent():
+                wx.Bell()
+
+        if keep_processing:
+            newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() )
+##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr)
+            if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
+                keep_processing = False
+                if not wx.Validator_IsSilent():
+                        wx.Bell()
+
+        if keep_processing:
+            wx.CallAfter(self._SetValue, newstr)
+            wx.CallAfter(self._SetInsertionPoint, newpos)
+        keep_processing = False
+##        dbg(indent=0)
+        return keep_processing
+
+
+    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
+            adjustInsert = adjustInsert
+        else:                           # else within a field; flag not relevant
+            adjustInsert = False
+
+        while self._isTemplateChar(pos) and pos < self._masklength:
+            pos += 1
+
+        # if changing fields, and we've been told to adjust insert point,
+        # look at new field; if empty and right-insert field,
+        # adjust to right edge:
+        if adjustInsert and pos < self._masklength:
+            field = self._FindField(pos)
+            start, end = field._extent
+            slice = self._GetValue()[start:end]
+            if field._insertRight and field.IsEmpty(slice):
+                pos = end
+        return pos
+
+
+    def _findNextTemplateChar(self, pos):
+        """ Find the position of the next non-editable character in the mask."""
+        while not self._isTemplateChar(pos) and pos < self._masklength:
+            pos += 1
+        return pos
+
+
+    def _OnAutoCompleteField(self, event):
+##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1)
+        pos = self._GetInsertionPoint()
+        field = self._FindField(pos)
+        edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
+
+        match_index = None
+        keycode = event.GetKeyCode()
+
+        if field._fillChar != ' ':
+            text = slice.replace(field._fillChar, '')
+        else:
+            text = slice
+        text = text.strip()
+        keep_processing = True  # (assume True to start)
+##        dbg('field._hasList?', field._hasList)
+        if field._hasList:
+##            dbg('choices:', field._choices)
+##            dbg('compareChoices:', field._compareChoices)
+            choices, choice_required = field._compareChoices, field._choiceRequired
+            if keycode in (wx.WXK_PRIOR, wx.WXK_UP):
+                direction = -1
+            else:
+                direction = 1
+            match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex)
+            if( match_index is None
+                and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
+                     or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ):
+                # Select the 1st thing from the list:
+                match_index = 0
+
+            if( match_index is not None
+                and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
+                      or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown())
+                      or (keycode == wx.WXK_DOWN and partial_match) ) ):
+
+                # We're allowed to auto-complete:
+##                dbg('match found')
+                value = self._GetValue()
+                newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:]
+##                dbg('setting value to "%s"' % newvalue)
+                self._SetValue(newvalue)
+                self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip())))
+                self._OnAutoSelect(field, match_index)
+                self._CheckValid()  # recolor as appopriate
+
+
+        if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT):
+            # treat as left right arrow if unshifted, tab/shift tab if shifted.
+            if event.ShiftDown():
+                if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT):
+                    # remove "shifting" and treat as (forward) tab:
+                    event.m_shiftDown = False
+                keep_processing = self._OnChangeField(event)
+            else:
+                keep_processing = self._OnArrow(event)
+        # else some other key; keep processing the key
+
+##        dbg('keep processing?', keep_processing, indent=0)
+        return keep_processing
+
+
+    def _OnAutoSelect(self, field, match_index = None):
+        """
+        Function called if autoselect feature is enabled and entire control
+        is selected:
+        """
+##        dbg('MaskedEditMixin::OnAutoSelect', field._index)
+        if match_index is not None:
+            field._autoCompleteIndex = match_index
+
+
+    def _autoComplete(self, direction, choices, value, compareNoCase, current_index):
+        """
+        This function gets called in response to Auto-complete events.
+        It attempts to find a match to the specified value against the
+        list of choices; if exact match, the index of then next
+        appropriate value in the list, based on the given direction.
+        If not an exact match, it will return the index of the 1st value from
+        the choice list for which the partial value can be extended to match.
+        If no match found, it will return None.
+        The function returns a 2-tuple, with the 2nd element being a boolean
+        that indicates if partial match was necessary.
+        """
+##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1)
+        if value is None:
+##            dbg('nothing to match against', indent=0)
+            return (None, False)
+
+        partial_match = False
+
+        if compareNoCase:
+            value = value.lower()
+
+        last_index = len(choices) - 1
+        if value in choices:
+##            dbg('"%s" in', choices)
+            if current_index is not None and choices[current_index] == value:
+                index = current_index
+            else:
+                index = choices.index(value)
+
+##            dbg('matched "%s" (%d)' % (choices[index], index))
+            if direction == -1:
+##                dbg('going to previous')
+                if index == 0: index = len(choices) - 1
+                else: index -= 1
+            else:
+                if index == len(choices) - 1: index = 0
+                else: index += 1
+##            dbg('change value to "%s" (%d)' % (choices[index], index))
+            match = index
+        else:
+            partial_match = True
+            value = value.strip()
+##            dbg('no match; try to auto-complete:')
+            match = None
+##            dbg('searching for "%s"' % value)
+            if current_index is None:
+                indices = range(len(choices))
+                if direction == -1:
+                    indices.reverse()
+            else:
+                if direction == 1:
+                    indices = range(current_index +1, len(choices)) + range(current_index+1)
+##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices)
+                else:
+                    indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1)
+##                    dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices)
+####            dbg('indices:', indices)
+            for index in indices:
+                choice = choices[index]
+                if choice.find(value, 0) == 0:
+##                    dbg('match found:', choice)
+                    match = index
+                    break
+                else: dbg('choice: "%s" - no match' % choice)
+            if match is not None:
+##                dbg('matched', match)
+                pass
+            else:
+##                dbg('no match found')
+                pass
+##        dbg(indent=0)
+        return (match, partial_match)
+
+
+    def _AdjustField(self, pos):
+        """
+        This function gets called by default whenever the cursor leaves a field.
+        The pos argument given is the char position before leaving that field.
+        By default, floating point, integer and date values are adjusted to be
+        legal in this function.  Derived classes may override this function
+        to modify the value of the control in a different way when changing fields.
+
+        NOTE: these change the value immediately, and restore the cursor to
+        the passed location, so that any subsequent code can then move it
+        based on the operation being performed.
+        """
+        newvalue = value = self._GetValue()
+        field = self._FindField(pos)
+        start, end, slice = self._FindFieldExtent(getslice=True)
+        newfield = field._AdjustField(slice)
+        newvalue = value[:start] + newfield + value[end:]
+
+        if self._isFloat and newvalue != self._template:
+            newvalue = self._adjustFloat(newvalue)
+
+        if self._ctrl_constraints._isInt and value != self._template:
+            newvalue = self._adjustInt(value)
+
+        if self._isDate and value != self._template:
+            newvalue = self._adjustDate(value, fixcentury=True)
+            if self._4digityear:
+                year2dig = self._dateExtent - 2
+                if pos == year2dig and value[year2dig] != newvalue[year2dig]:
+                    pos = pos+2
+
+        if newvalue != value:
+            self._SetValue(newvalue)
+            self._SetInsertionPoint(pos)
+
+
+    def _adjustKey(self, pos, key):
+        """ Apply control formatting to the key (e.g. convert to upper etc). """
+        field = self._FindField(pos)
+        if field._forceupper and key in range(97,123):
+            key = ord( chr(key).upper())
+
+        if field._forcelower and key in range(97,123):
+            key = ord( chr(key).lower())
+
+        return key
+
+
+    def _adjustPos(self, pos, key):
+        """
+        Checks the current insertion point position and adjusts it if
+        necessary to skip over non-editable characters.
+        """
+##        dbg('_adjustPos', pos, key, indent=1)
+        sel_start, sel_to = self._GetSelection()
+        # If a numeric or decimal mask, and negatives allowed, reserve the
+        # first space for sign, and last one if using parens.
+        if( self._signOk
+            and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) )
+                 or self._useParens and pos == self._masklength -1)):
+##            dbg('adjusted pos:', pos, indent=0)
+            return pos
+
+        if key not in self._nav:
+            field = self._FindField(pos)
+
+##            dbg('field._insertRight?', field._insertRight)
+            if field._insertRight:              # if allow right-insert
+                start, end = field._extent
+                slice = self._GetValue()[start:end].strip()
+                field_len = end - start
+                if pos == end:                      # if cursor at right edge of field
+                    # if not filled or supposed to stay in field, keep current position
+####                    dbg('pos==end')
+####                    dbg('len (slice):', len(slice))
+####                    dbg('field_len?', field_len)
+####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
+####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
+                    if len(slice) == field_len and field._moveOnFieldFull:
+                        # move cursor to next field:
+                        pos = self._findNextEntry(pos)
+                        self._SetInsertionPoint(pos)
+                        if pos < sel_to:
+                            self._SetSelection(pos, sel_to)     # restore selection
+                        else:
+                            self._SetSelection(pos, pos)        # remove selection
+                    else: # leave cursor alone
+                        pass
+                else:
+                    # if at start of control, move to right edge
+                    if (sel_to == sel_start
+                        and (self._isTemplateChar(pos) or (pos == start and len(slice)+ 1 < field_len))
+                        and pos != end):
+                        pos = end                   # move to right edge
+##                    elif sel_start <= start and sel_to == end:
+##                        # select to right edge of field - 1 (to replace char)
+##                        pos = end - 1
+##                        self._SetInsertionPoint(pos)
+##                        # restore selection
+##                        self._SetSelection(sel_start, pos)
+
+                    elif self._signOk and sel_start == 0:   # if selected to beginning and signed,
+                        # adjust to past reserved sign position:
+                        pos = self._fields[0]._extent[0]
+                        self._SetInsertionPoint(pos)
+                        # restore selection
+                        self._SetSelection(pos, sel_to)
+                    else:
+                        pass    # leave position/selection alone
+
+            # else make sure the user is not trying to type over a template character
+            # If they are, move them to the next valid entry position
+            elif self._isTemplateChar(pos):
+                if( not field._moveOnFieldFull
+                      and (not self._signOk
+                           or (self._signOk
+                               and field._index == 0
+                               and pos > 0) ) ):      # don't move to next field without explicit cursor movement
+                    pass
+                else:
+                    # find next valid position
+                    pos = self._findNextEntry(pos)
+                    self._SetInsertionPoint(pos)
+                    if pos < sel_to:    # restore selection
+                        self._SetSelection(pos, sel_to)
+##        dbg('adjusted pos:', pos, indent=0)
+        return pos
+
+
+    def _adjustFloat(self, candidate=None):
+        """
+        'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
+        """
+##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1)
+        lenInt,lenFraction  = [len(s) for s in self._mask.split('.')]  ## Get integer, fraction lengths
+
+        if candidate is None: value = self._GetValue()
+        else: value = candidate
+##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
+        intStr, fracStr = value.split(self._decimalChar)
+
+        intStr = self._fields[0]._AdjustField(intStr)
+##        dbg('adjusted intStr: "%s"' % intStr)
+        lenInt = len(intStr)
+        fracStr = fracStr + ('0'*(lenFraction-len(fracStr)))  # add trailing spaces to decimal
+
+##        dbg('intStr "%(intStr)s"' % locals())
+##        dbg('lenInt:', lenInt)
+
+        intStr = string.rjust( intStr[-lenInt:], lenInt)
+##        dbg('right-justifed intStr = "%(intStr)s"' % locals())
+        newvalue = intStr + self._decimalChar + fracStr
+
+        if self._signOk:
+            if len(newvalue) < self._masklength:
+                newvalue = ' ' + newvalue
+            signedvalue = self._getSignedValue(newvalue)[0]
+            if signedvalue is not None: newvalue = signedvalue
+
+        # Finally, align string with decimal position, left-padding with
+        # fillChar:
+        newdecpos = newvalue.find(self._decimalChar)
+        if newdecpos < self._decimalpos:
+            padlen = self._decimalpos - newdecpos
+            newvalue = string.join([' ' * padlen] + [newvalue] ,'')
+
+        if self._signOk and self._useParens:
+            if newvalue.find('(') != -1:
+                newvalue = newvalue[:-1] + ')'
+            else:
+                newvalue = newvalue[:-1] + ' '
+
+##        dbg('newvalue = "%s"' % newvalue)
+        if candidate is None:
+            wx.CallAfter(self._SetValue, newvalue)
+##        dbg(indent=0)
+        return newvalue
+
+
+    def _adjustInt(self, candidate=None):
+        """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
+##        dbg("MaskedEditMixin::_adjustInt", candidate)
+        lenInt = self._masklength
+        if candidate is None: value = self._GetValue()
+        else: value = candidate
+
+        intStr = self._fields[0]._AdjustField(value)
+        intStr = intStr.strip() # drop extra spaces
+##        dbg('adjusted field: "%s"' % intStr)
+
+        if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1:
+            if self._useParens:
+                intStr = '(' + intStr + ')'
+            else:
+                intStr = '-' + intStr
+        elif self._isNeg and intStr.find('-') != -1 and self._useParens:
+            intStr = intStr.replace('-', '(')
+
+        if( self._signOk and ((self._useParens and intStr.find('(') == -1)
+                                or (not self._useParens and intStr.find('-') == -1))):
+            intStr = ' ' + intStr
+            if self._useParens:
+                intStr += ' '   # space for right paren position
+
+        elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1:
+            # ensure closing right paren:
+            intStr += ')'
+
+        if self._fields[0]._alignRight:     ## Only if right-alignment is enabled
+            intStr = intStr.rjust( lenInt )
+        else:
+            intStr = intStr.ljust( lenInt )
+
+        if candidate is None:
+            wx.CallAfter(self._SetValue, intStr )
+        return intStr
+
+
+    def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False):
+        """
+        'Fixes' a date control, expanding the year if it can.
+        Applies various self-formatting options.
+        """
+##        dbg("MaskedEditMixin::_adjustDate", indent=1)
+        if candidate is None: text    = self._GetValue()
+        else: text = candidate
+##        dbg('text=', text)
+        if self._datestyle == "YMD":
+            year_field = 0
+        else:
+            year_field = 2
+
+##        dbg('getYear: "%s"' % getYear(text, self._datestyle))
+        year    = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"")  # drop extra fillChars
+        month   = getMonth( text, self._datestyle)
+        day     = getDay( text, self._datestyle)
+##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day)
+
+        yearVal = None
+        yearstart = self._dateExtent - 4
+        if( len(year) < 4
+            and (fixcentury
+                 or force4digit_year
+                 or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ')
+                 or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ):
+            ## user entered less than four digits and changing fields or past point where we could
+            ## enter another digit:
+            try:
+                yearVal = int(year)
+            except:
+##                dbg('bad year=', year)
+                year = text[yearstart:self._dateExtent]
+
+        if len(year) < 4 and yearVal:
+            if len(year) == 2:
+                # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the
+                # years pass...
+                now = wx.DateTime_Now()
+                century = (now.GetYear() /100) * 100        # "this century"
+                twodig_year = now.GetYear() - century       # "this year" (2 digits)
+                # if separation between today's 2-digit year and typed value > 50,
+                #      assume last century,
+                # else assume this century.
+                #
+                # Eg: if 2003 and yearVal == 30, => 2030
+                #     if 2055 and yearVal == 80, => 2080
+                #     if 2010 and yearVal == 96, => 1996
+                #
+                if abs(yearVal - twodig_year) > 50:
+                    yearVal = (century - 100) + yearVal
+                else:
+                    yearVal = century + yearVal
+                year = str( yearVal )
+            else:   # pad with 0's to make a 4-digit year
+                year = "%04d" % yearVal
+            if self._4digityear or force4digit_year:
+                text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:]
+##        dbg('newdate: "%s"' % text, indent=0)
+        return text
+
+
+    def _goEnd(self, getPosOnly=False):
+        """ Moves the insertion point to the end of user-entry """
+##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
+        text = self._GetValue()
+####        dbg('text: "%s"' % text)
+        i = 0
+        if len(text.rstrip()):
+            for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1):
+####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i))
+                if self._isMaskChar(i):
+                    char = text[i]
+####                    dbg("text[%d]: '%s'" % (i, char))
+                    if char != ' ':
+                        i += 1
+                        break
+
+        if i == 0:
+            pos = self._goHome(getPosOnly=True)
+        else:
+            pos = min(i,self._masklength)
+
+        field = self._FindField(pos)
+        start, end = field._extent
+        if field._insertRight and pos < end:
+            pos = end
+##        dbg('next pos:', pos)
+##        dbg(indent=0)
+        if getPosOnly:
+            return pos
+        else:
+            self._SetInsertionPoint(pos)
+
+
+    def _goHome(self, getPosOnly=False):
+        """ Moves the insertion point to the beginning of user-entry """
+##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1)
+        text = self._GetValue()
+        for i in range(self._masklength):
+            if self._isMaskChar(i):
+                break
+        pos = max(i, 0)
+##        dbg(indent=0)
+        if getPosOnly:
+            return pos
+        else:
+            self._SetInsertionPoint(max(i,0))
+
+
+
+    def _getAllowedChars(self, pos):
+        """ Returns a string of all allowed user input characters for the provided
+            mask character plus control options
+        """
+        maskChar = self.maskdict[pos]
+        okchars = self.maskchardict[maskChar]    ## entry, get mask approved characters
+        field = self._FindField(pos)
+        if okchars and field._okSpaces:          ## Allow spaces?
+            okchars += " "
+        if okchars and field._includeChars:      ## any additional included characters?
+            okchars += field._includeChars
+####        dbg('okchars[%d]:' % pos, okchars)
+        return okchars
+
+
+    def _isMaskChar(self, pos):
+        """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
+        """
+        if pos < self._masklength:
+            return self.ismasked[pos]
+        else:
+            return False
+
+
+    def _isTemplateChar(self,Pos):
+        """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#)
+        """
+        if Pos < self._masklength:
+            return not self._isMaskChar(Pos)
+        else:
+            return False
+
+
+    def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False):
+        """ Returns True if character is allowed at the specific position, otherwise False."""
+##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1)
+        field = self._FindField(pos)
+        right_insert = False
+
+        if self.controlInitialized:
+            sel_start, sel_to = self._GetSelection()
+        else:
+            sel_start, sel_to = pos, pos
+
+        if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight:
+            start, end = field._extent
+            field_len = end - start
+            if self.controlInitialized:
+                value = self._GetValue()
+                fstr = value[start:end].strip()
+                if field._padZero:
+                    while fstr and fstr[0] == '0':
+                        fstr = fstr[1:]
+                input_len = len(fstr)
+                if self._signOk and '-' in fstr or '(' in fstr:
+                    input_len -= 1  # sign can move out of field, so don't consider it in length
+            else:
+                value = self._template
+                input_len = 0   # can't get the current "value", so use 0
+
+
+            # if entire field is selected or position is at end and field is not full,
+            # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar:
+            if( (sel_start, sel_to) == field._extent
+                or (pos == end and input_len < field_len)):
+                pos = end - 1
+##                dbg('pos = end - 1 = ', pos, 'right_insert? 1')
+                right_insert = True
+            elif( field._allowInsert and sel_start == sel_to
+                  and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar))
+                  and input_len < field_len ):
+                pos = sel_to - 1    # where character will go
+##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1')
+                right_insert = True
+            # else leave pos alone...
+            else:
+##                dbg('pos stays ', pos, 'right_insert? 0')
+                pass
+
+        if self._isTemplateChar( pos ):  ## if a template character, return empty
+##            dbg('%d is a template character; returning False' % pos, indent=0)
+            return False
+
+        if self._isMaskChar( pos ):
+            okChars  = self._getAllowedChars(pos)
+
+            if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)):
+                okChars += self._fields[0]._groupChar
+
+            if self._signOk:
+                if self._isInt or (self._isFloat and pos < self._decimalpos):
+                    okChars += '-'
+                    if self._useParens:
+                        okChars += '('
+                elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)):
+                    okChars += ')'
+
+####            dbg('%s in %s?' % (char, okChars), char in okChars)
+            approved = char in okChars
+
+            if approved and checkRegex:
+##                dbg("checking appropriate regex's")
+                value = self._eraseSelection(self._GetValue())
+                if right_insert:
+                    at = pos+1
+                else:
+                    at = pos
+                if allowAutoSelect:
+                    newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True)
+                else:
+                    newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value)
+##                dbg('newvalue: "%s"' % newvalue)
+
+                fields = [self._FindField(pos)] + [self._ctrl_constraints]
+                for field in fields:    # includes fields[-1] == "ctrl_constraints"
+                    if field._regexMask and field._filter:
+##                        dbg('checking vs. regex')
+                        start, end = field._extent
+                        slice = newvalue[start:end]
+                        approved = (re.match( field._filter, slice) is not None)
+##                        dbg('approved?', approved)
+                    if not approved: break
+##            dbg(indent=0)
+            return approved
+        else:
+##            dbg('%d is a !???! character; returning False', indent=0)
+            return False
+
+
+    def _applyFormatting(self):
+        """ Apply formatting depending on the control's state.
+            Need to find a way to call this whenever the value changes, in case the control's
+            value has been changed or set programatically.
+        """
+##        dbg(suspend=1)
+##        dbg('MaskedEditMixin::_applyFormatting', indent=1)
+
+        # Handle negative numbers
+        if self._signOk:
+            text, signpos, right_signpos = self._getSignedValue()
+##            dbg('text: "%s", signpos:' % text, signpos)
+            if not text or text[signpos] not in ('-','('):
+                self._isNeg = False
+##                dbg('no valid sign found; new sign:', self._isNeg)
+                if text and signpos != self._signpos:
+                    self._signpos = signpos
+            elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('):
+##                dbg('setting _isNeg to True')
+                self._isNeg = True
+##            dbg('self._isNeg:', self._isNeg)
+
+        if self._signOk and self._isNeg:
+            fc = self._signedForegroundColour
+        else:
+            fc = self._foregroundColour
+
+        if hasattr(fc, '_name'):
+            c =fc._name
+        else:
+            c = fc
+##        dbg('setting foreground to', c)
+        self.SetForegroundColour(fc)
+
+        if self._valid:
+##            dbg('valid')
+            if self.IsEmpty():
+                bc = self._emptyBackgroundColour
+            else:
+                bc = self._validBackgroundColour
+        else:
+##            dbg('invalid')
+            bc = self._invalidBackgroundColour
+        if hasattr(bc, '_name'):
+            c =bc._name
+        else:
+            c = bc
+##        dbg('setting background to', c)
+        self.SetBackgroundColour(bc)
+        self._Refresh()
+##        dbg(indent=0, suspend=0)
+
+
+    def _getAbsValue(self, candidate=None):
+        """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s).
+        """
+##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1)
+        if candidate is None: text = self._GetValue()
+        else: text = candidate
+        right_signpos = text.find(')')
+
+        if self._isInt:
+            if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ':
+                signpos = text.find('-')
+                if signpos == -1:
+##                    dbg('no - found; searching for (')
+                    signpos = text.find('(')
+                elif signpos != -1:
+##                    dbg('- found at', signpos)
+                    pass
+
+                if signpos == -1:
+##                    dbg('signpos still -1')
+##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength)
+                    if len(text) < self._masklength:
+                        text = ' ' + text
+                    if len(text) < self._masklength:
+                        text += ' '
+                    if len(text) > self._masklength and text[-1] in (')', ' '):
+                        text = text[:-1]
+                    else:
+##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength))
+##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1))
+                        signpos = len(text) - (len(text.lstrip()) + 1)
+
+                        if self._useParens and not text.strip():
+                            signpos -= 1    # empty value; use penultimate space
+##                dbg('signpos:', signpos)
+                if signpos >= 0:
+                    text = text[:signpos] + ' ' + text[signpos+1:]
+
+            else:
+                if self._signOk:
+                    signpos = 0
+                    text = self._template[0] + text[1:]
+                else:
+                    signpos = -1
+
+            if right_signpos != -1:
+                if self._signOk:
+                    text = text[:right_signpos] + ' ' + text[right_signpos+1:]
+                elif len(text) > self._masklength:
+                    text = text[:right_signpos] + text[right_signpos+1:]
+                    right_signpos = -1
+
+
+            elif self._useParens and self._signOk:
+                # figure out where it ought to go:
+                right_signpos = self._masklength - 1     # initial guess
+                if not self._ctrl_constraints._alignRight:
+##                    dbg('not right-aligned')
+                    if len(text.strip()) == 0:
+                        right_signpos = signpos + 1
+                    elif len(text.strip()) < self._masklength:
+                        right_signpos = len(text.rstrip())
+##                dbg('right_signpos:', right_signpos)
+
+            groupchar = self._fields[0]._groupChar
+            try:
+                value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', ''))
+            except:
+##                dbg('invalid number', indent=0)
+                return None, signpos, right_signpos
+
+        else:   # float value
+            try:
+                groupchar = self._fields[0]._groupChar
+                value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', ''))
+##                dbg('value:', value)
+            except:
+                value = None
+
+            if value < 0 and value is not None:
+                signpos = text.find('-')
+                if signpos == -1:
+                    signpos = text.find('(')
+
+                text = text[:signpos] + self._template[signpos] + text[signpos+1:]
+            else:
+                # look forwards up to the decimal point for the 1st non-digit
+##                dbg('decimal pos:', self._decimalpos)
+##                dbg('text: "%s"' % text)
+                if self._signOk:
+                    signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1)
+                    # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET
+                    if len(text) >= signpos+1 and  text[signpos+1] in ('-','('):
+                        signpos += 1
+                else:
+                    signpos = -1
+##                dbg('signpos:', signpos)
+
+            if self._useParens:
+                if self._signOk:
+                    right_signpos = self._masklength - 1
+                    text = text[:right_signpos] + ' '
+                    if text[signpos] == '(':
+                        text = text[:signpos] + ' ' + text[signpos+1:]
+                else:
+                    right_signpos = text.find(')')
+                    if right_signpos != -1:
+                        text = text[:-1]
+                        right_signpos = -1
+
+            if value is None:
+##                dbg('invalid number')
+                text = None
+
+##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos)
+##        dbg(indent=0)
+        return text, signpos, right_signpos
+
+
+    def _getSignedValue(self, candidate=None):
+        """ Return a signed value by adding a "-" prefix if the value
+            is set to negative, or a space if positive.
+        """
+##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1)
+        if candidate is None: text = self._GetValue()
+        else: text = candidate
+
+
+        abstext, signpos, right_signpos = self._getAbsValue(text)
+        if self._signOk:
+            if abstext is None:
+##                dbg(indent=0)
+                return abstext, signpos, right_signpos
+
+            if self._isNeg or text[signpos] in ('-', '('):
+                if self._useParens:
+                    sign = '('
+                else:
+                    sign = '-'
+            else:
+                sign = ' '
+            if abstext[signpos] not in string.digits:
+                text = abstext[:signpos] + sign + abstext[signpos+1:]
+            else:
+                # this can happen if value passed is too big; sign assumed to be
+                # in position 0, but if already filled with a digit, prepend sign...
+                text = sign + abstext
+            if self._useParens and text.find('(') != -1:
+                text = text[:right_signpos] + ')' + text[right_signpos+1:]
+        else:
+            text = abstext
+##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos)
+##        dbg(indent=0)
+        return text, signpos, right_signpos
+
+
+    def GetPlainValue(self, candidate=None):
+        """ Returns control's value stripped of the template text.
+            plainvalue = MaskedEditMixin.GetPlainValue()
+        """
+##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1)
+
+        if candidate is None: text = self._GetValue()
+        else: text = candidate
+
+        if self.IsEmpty():
+##            dbg('returned ""', indent=0)
+            return ""
+        else:
+            plain = ""
+            for idx in range( min(len(self._template), len(text)) ):
+                if self._mask[idx] in maskchars:
+                    plain += text[idx]
+
+            if self._isFloat or self._isInt:
+##                dbg('plain so far: "%s"' % plain)
+                plain = plain.replace('(', '-').replace(')', ' ')
+##                dbg('plain after sign regularization: "%s"' % plain)
+
+                if self._signOk and self._isNeg and plain.count('-') == 0:
+                    # must be in reserved position; add to "plain value"
+                    plain = '-' + plain.strip()
+
+                if self._fields[0]._alignRight:
+                    lpad = plain.count(',')
+                    plain = ' ' * lpad + plain.replace(',','')
+                else:
+                    plain = plain.replace(',','')
+##                dbg('plain after pad and group:"%s"' % plain)
+
+##            dbg('returned "%s"' % plain.rstrip(), indent=0)
+            return plain.rstrip()
+
+
+    def IsEmpty(self, value=None):
+        """
+        Returns True if control is equal to an empty value.
+        (Empty means all editable positions in the template == fillChar.)
+        """
+        if value is None: value = self._GetValue()
+        if value == self._template and not self._defaultValue:
+####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)")
+            return True     # (all mask chars == fillChar by defn)
+        elif value == self._template:
+            empty = True
+            for pos in range(len(self._template)):
+####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos))
+####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos])
+                if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]):
+                    empty = False
+####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals())
+            return empty
+        else:
+####            dbg("IsEmpty? 0 (value doesn't match template)")
+            return False
+
+
+    def IsDefault(self, value=None):
+        """
+        Returns True if the value specified (or the value of the control if not specified)
+        is equal to the default value.
+        """
+        if value is None: value = self._GetValue()
+        return value == self._template
+
+
+    def IsValid(self, value=None):
+        """ Indicates whether the value specified (or the current value of the control
+        if not specified) is considered valid."""
+####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1)
+        if value is None: value = self._GetValue()
+        ret = self._CheckValid(value)
+####        dbg(indent=0)
+        return ret
+
+
+    def _eraseSelection(self, value=None, sel_start=None, sel_to=None):
+        """ Used to blank the selection when inserting a new character. """
+##        dbg("MaskedEditMixin::_eraseSelection", indent=1)
+        if value is None: value = self._GetValue()
+        if sel_start is None or sel_to is None:
+            sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
+##        dbg('value: "%s"' % value)
+##        dbg("current sel_start, sel_to:", sel_start, sel_to)
+
+        newvalue = list(value)
+        for i in range(sel_start, sel_to):
+            if self._signOk and newvalue[i] in ('-', '(', ')'):
+##                dbg('found sign (%s) at' % newvalue[i], i)
+
+                # balance parentheses:
+                if newvalue[i] == '(':
+                    right_signpos = value.find(')')
+                    if right_signpos != -1:
+                        newvalue[right_signpos] = ' '
+
+                elif newvalue[i] == ')':
+                    left_signpos = value.find('(')
+                    if left_signpos != -1:
+                        newvalue[left_signpos] = ' '
+
+                newvalue[i] = ' '
+
+            elif self._isMaskChar(i):
+                field = self._FindField(i)
+                if field._padZero:
+                    newvalue[i] = '0'
+                else:
+                    newvalue[i] = self._template[i]
+
+        value = string.join(newvalue,"")
+##        dbg('new value: "%s"' % value)
+##        dbg(indent=0)
+        return value
+
+
+    def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False):
+        """ Handles replacement of the character at the current insertion point."""
+##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
+
+        text = self._eraseSelection(value)
+        field = self._FindField(pos)
+        start, end = field._extent
+        newtext = ""
+        newpos = pos
+
+        if pos != sel_start and sel_start == sel_to:
+            # adjustpos must have moved the position; make selection match:
+            sel_start = sel_to = pos
+
+##        dbg('field._insertRight?', field._insertRight)
+        if( field._insertRight                                  # field allows right insert
+            and ((sel_start, sel_to) == field._extent           # and whole field selected
+                 or (sel_start == sel_to                        # or nothing selected
+                     and (sel_start == end                      # and cursor at right edge
+                          or (field._allowInsert                # or field allows right-insert
+                              and sel_start < end               # next to other char in field:
+                              and text[sel_start] != field._fillChar) ) ) ) ):
+##            dbg('insertRight')
+            fstr = text[start:end]
+            erasable_chars = [field._fillChar, ' ']
+
+            if field._padZero:
+                erasable_chars.append('0')
+
+            erased = ''
+####            dbg("fstr[0]:'%s'" % fstr[0])
+####            dbg('field_index:', field._index)
+####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars)
+####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?",
+##                 self._signOk and field._index == 0 and fstr[0] in ('-','('))
+            if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')):
+                erased = fstr[0]
+####                dbg('value:      "%s"' % text)
+####                dbg('fstr:       "%s"' % fstr)
+####                dbg("erased:     '%s'" % erased)
+                field_sel_start = sel_start - start
+                field_sel_to = sel_to - start
+##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start])
+##                dbg('right fstr: "%s"' % fstr[field_sel_to:end])
+                fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end]
+            if field._alignRight and sel_start != sel_to:
+                field_len = end - start
+##                pos += (field_len - len(fstr))    # move cursor right by deleted amount
+                pos = sel_to
+##                dbg('setting pos to:', pos)
+                if field._padZero:
+                    fstr = '0' * (field_len - len(fstr)) + fstr
+                else:
+                    fstr = fstr.rjust(field_len)   # adjust the field accordingly
+##            dbg('field str: "%s"' % fstr)
+
+            newtext = text[:start] + fstr + text[end:]
+            if erased in ('-', '(') and self._signOk:
+                newtext = erased + newtext[1:]
+##            dbg('newtext: "%s"' % newtext)
+
+            if self._signOk and field._index == 0:
+                start -= 1             # account for sign position
+
+####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
+####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
+            if( field._moveOnFieldFull and pos == end
+                and len(fstr.lstrip()) == end-start):   # if field now full
+                newpos = self._findNextEntry(end)       #   go to next field
+            else:
+                newpos = pos                            # else keep cursor at current position
+
+        if not newtext:
+##            dbg('not newtext')
+            if newpos != pos:
+##                dbg('newpos:', newpos)
+                pass
+            if self._signOk and self._useParens:
+                old_right_signpos = text.find(')')
+
+            if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start:
+                # inserting within a left-insert-capable field
+                field_len = end - start
+                before = text[start:sel_start]
+                after = text[sel_to:end].strip()
+####                dbg("current field:'%s'" % text[start:end])
+####                dbg("before:'%s'" % before, "after:'%s'" % after)
+                new_len = len(before) + len(after) + 1 # (for inserted char)
+####                dbg('new_len:', new_len)
+
+                if new_len < field_len:
+                    retained = after + self._template[end-(field_len-new_len):end]
+                elif new_len > end-start:
+                    retained = after[1:]
+                else:
+                    retained = after
+
+                left = text[0:start] + before
+####                dbg("left:'%s'" % left, "retained:'%s'" % retained)
+                right   = retained + text[end:]
+            else:
+                left  = text[0:pos]
+                right   = text[pos+1:]
+
+            newtext = left + char + right
+
+            if self._signOk and self._useParens:
+                # Balance parentheses:
+                left_signpos = newtext.find('(')
+
+                if left_signpos == -1:     # erased '('; remove ')'
+                    right_signpos = newtext.find(')')
+                    if right_signpos != -1:
+                        newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:]
+
+                elif old_right_signpos != -1:
+                    right_signpos = newtext.find(')')
+
+                    if right_signpos == -1: # just replaced right-paren
+                        if newtext[pos] == ' ': # we just erased '); erase '('
+                            newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:]
+                        else:   # replaced with digit; move ') over
+                            if self._ctrl_constraints._alignRight or self._isFloat:
+                                newtext = newtext[:-1] + ')'
+                            else:
+                                rstripped_text = newtext.rstrip()
+                                right_signpos = len(rstripped_text)
+##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos)
+                                newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:]
+
+            if( field._insertRight                                  # if insert-right field (but we didn't start at right edge)
+                and field._moveOnFieldFull                          # and should move cursor when full
+                and len(newtext[start:end].strip()) == end-start):  # and field now full
+                newpos = self._findNextEntry(end)                   #   go to next field
+##                dbg('newpos = nextentry =', newpos)
+            else:
+##                dbg('pos:', pos, 'newpos:', pos+1)
+                newpos = pos+1
+
+
+        if allowAutoSelect:
+            new_select_to = newpos     # (default return values)
+            match_field = None
+            match_index = None
+
+            if field._autoSelect:
+                match_index, partial_match = self._autoComplete(1,  # (always forward)
+                                                                field._compareChoices,
+                                                                newtext[start:end],
+                                                                compareNoCase=field._compareNoCase,
+                                                                current_index = field._autoCompleteIndex-1)
+                if match_index is not None and partial_match:
+                    matched_str = newtext[start:end]
+                    newtext = newtext[:start] + field._choices[match_index] + newtext[end:]
+                    new_select_to = end
+                    match_field = field
+                    if field._insertRight:
+                        # adjust position to just after partial match in field
+                        newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip()))
+
+            elif self._ctrl_constraints._autoSelect:
+                match_index, partial_match = self._autoComplete(
+                                        1,  # (always forward)
+                                        self._ctrl_constraints._compareChoices,
+                                        newtext,
+                                        self._ctrl_constraints._compareNoCase,
+                                        current_index = self._ctrl_constraints._autoCompleteIndex - 1)
+                if match_index is not None and partial_match:
+                    matched_str = newtext
+                    newtext = self._ctrl_constraints._choices[match_index]
+                    new_select_to = self._ctrl_constraints._extent[1]
+                    match_field = self._ctrl_constraints
+                    if self._ctrl_constraints._insertRight:
+                        # adjust position to just after partial match in control:
+                        newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip()))
+
+##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to)
+##            dbg(indent=0)
+            return newtext, newpos, new_select_to, match_field, match_index
+        else:
+##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos)
+##            dbg(indent=0)
+            return newtext, newpos
+
+
+    def _OnFocus(self,event):
+        """
+        This event handler is currently necessary to work around new default
+        behavior as of wxPython2.3.3;
+        The TAB key auto selects the entire contents of the wxTextCtrl *after*
+        the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
+        *here*, because it hasn't happened yet.  So to prevent this behavior, and
+        preserve the correct selection when the focus event is not due to tab,
+        we need to pull the following trick:
+        """
+##        dbg('MaskedEditMixin::_OnFocus')
+        wx.CallAfter(self._fixSelection)
+        event.Skip()
+        self.Refresh()
+
+
+    def _CheckValid(self, candidate=None):
+        """
+        This is the default validation checking routine; It verifies that the
+        current value of the control is a "valid value," and has the side
+        effect of coloring the control appropriately.
+        """
+##        dbg(suspend=1)
+##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1)
+        oldValid = self._valid
+        if candidate is None: value = self._GetValue()
+        else: value = candidate
+##        dbg('value: "%s"' % value)
+        oldvalue = value
+        valid = True    # assume True
+
+        if not self.IsDefault(value) and self._isDate:                    ## Date type validation
+            valid = self._validateDate(value)
+##            dbg("valid date?", valid)
+
+        elif not self.IsDefault(value) and self._isTime:
+            valid = self._validateTime(value)
+##            dbg("valid time?", valid)
+
+        elif not self.IsDefault(value) and (self._isInt or self._isFloat):  ## Numeric type
+            valid = self._validateNumeric(value)
+##            dbg("valid Number?", valid)
+
+        if valid:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault()
+            ## valid so far; ensure also allowed by any list or regex provided:
+            valid = self._validateGeneric(value)
+##            dbg("valid value?", valid)
+
+##        dbg('valid?', valid)
+
+        if not candidate:
+            self._valid = valid
+            self._applyFormatting()
+            if self._valid != oldValid:
+##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
+##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
+                pass
+##        dbg(indent=0, suspend=0)
+        return valid
+
+
+    def _validateGeneric(self, candidate=None):
+        """ Validate the current value using the provided list or Regex filter (if any).
+        """
+        if candidate is None:
+            text = self._GetValue()
+        else:
+            text = candidate
+
+        valid = True    # assume True
+        for i in [-1] + self._field_indices:   # process global constraints first:
+            field = self._fields[i]
+            start, end = field._extent
+            slice = text[start:end]
+            valid = field.IsValid(slice)
+            if not valid:
+                break
+
+        return valid
+
+
+    def _validateNumeric(self, candidate=None):
+        """ Validate that the value is within the specified range (if specified.)"""
+        if candidate is None: value = self._GetValue()
+        else: value = candidate
+        try:
+            groupchar = self._fields[0]._groupChar
+            if self._isFloat:
+                number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', ''))
+            else:
+                number = long( value.replace(groupchar, '').replace('(', '-').replace(')', ''))
+                if value.strip():
+                    if self._fields[0]._alignRight:
+                        require_digit_at = self._fields[0]._extent[1]-1
+                    else:
+                        require_digit_at = self._fields[0]._extent[0]
+##                    dbg('require_digit_at:', require_digit_at)
+##                    dbg("value[rda]: '%s'" % value[require_digit_at])
+                    if value[require_digit_at] not in list(string.digits):
+                        valid = False
+                        return valid
+                # else...
+##            dbg('number:', number)
+            if self._ctrl_constraints._hasRange:
+                valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh
+            else:
+                valid = True
+            groupcharpos = value.rfind(groupchar)
+            if groupcharpos != -1:  # group char present
+##                dbg('groupchar found at', groupcharpos)
+                if self._isFloat and groupcharpos > self._decimalpos:
+                    # 1st one found on right-hand side is past decimal point
+##                    dbg('groupchar in fraction; illegal')
+                    valid = False
+                elif self._isFloat:
+                    integer = value[:self._decimalpos].strip()
+                else:
+                    integer = value.strip()
+##                dbg("integer:'%s'" % integer)
+                if integer[0] in ('-', '('):
+                    integer = integer[1:]
+                if integer[-1] == ')':
+                    integer = integer[:-1]
+
+                parts = integer.split(groupchar)
+##                dbg('parts:', parts)
+                for i in range(len(parts)):
+                    if i == 0 and abs(int(parts[0])) > 999:
+##                        dbg('group 0 too long; illegal')
+                        valid = False
+                        break
+                    elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]):
+##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i]))
+                        valid = False
+                        break
+        except ValueError:
+##            dbg('value not a valid number')
+            valid = False
+        return valid
+
+
+    def _validateDate(self, candidate=None):
+        """ Validate the current date value using the provided Regex filter.
+            Generally used for character types.BufferType
+        """
+##        dbg('MaskedEditMixin::_validateDate', indent=1)
+        if candidate is None: value = self._GetValue()
+        else: value = candidate
+##        dbg('value = "%s"' % value)
+        text = self._adjustDate(value, force4digit_year=True)     ## Fix the date up before validating it
+##        dbg('text =', text)
+        valid = True   # assume True until proven otherwise
+
+        try:
+            # replace fillChar in each field with space:
+            datestr = text[0:self._dateExtent]
+            for i in range(3):
+                field = self._fields[i]
+                start, end = field._extent
+                fstr = datestr[start:end]
+                fstr.replace(field._fillChar, ' ')
+                datestr = datestr[:start] + fstr + datestr[end:]
+
+            year, month, day = getDateParts( datestr, self._datestyle)
+            year = int(year)
+##            dbg('self._dateExtent:', self._dateExtent)
+            if self._dateExtent == 11:
+                month = charmonths_dict[month.lower()]
+            else:
+                month = int(month)
+            day = int(day)
+##            dbg('year, month, day:', year, month, day)
+
+        except ValueError:
+##            dbg('cannot convert string to integer parts')
+            valid = False
+        except KeyError:
+##            dbg('cannot convert string to integer month')
+            valid = False
+
+        if valid:
+            # use wxDateTime to unambiguously try to parse the date:
+            # ### Note: because wxDateTime is *brain-dead* and expects months 0-11,
+            # rather than 1-12, so handle accordingly:
+            if month > 12:
+                valid = False
+            else:
+                month -= 1
+                try:
+##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year))
+                    dateHandler = wx.DateTimeFromDMY(day,month,year)
+##                    dbg("succeeded")
+                    dateOk = True
+                except:
+##                    dbg('cannot convert string to valid date')
+                    dateOk = False
+                if not dateOk:
+                    valid = False
+
+            if valid:
+                # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
+                # so we eliminate them here:
+                timeStr     = text[self._dateExtent+1:].strip()         ## time portion of the string
+                if timeStr:
+##                    dbg('timeStr: "%s"' % timeStr)
+                    try:
+                        checkTime    = dateHandler.ParseTime(timeStr)
+                        valid = checkTime == len(timeStr)
+                    except:
+                        valid = False
+                    if not valid:
+##                        dbg('cannot convert string to valid time')
+                        pass
+        if valid: dbg('valid date')
+##        dbg(indent=0)
+        return valid
+
+
+    def _validateTime(self, candidate=None):
+        """ Validate the current time value using the provided Regex filter.
+            Generally used for character types.BufferType
+        """
+##        dbg('MaskedEditMixin::_validateTime', indent=1)
+        # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
+        # so we eliminate them here:
+        if candidate is None: value = self._GetValue().strip()
+        else: value = candidate.strip()
+##        dbg('value = "%s"' % value)
+        valid = True   # assume True until proven otherwise
+
+        dateHandler = wx.DateTime_Today()
+        try:
+            checkTime    = dateHandler.ParseTime(value)
+##            dbg('checkTime:', checkTime, 'len(value)', len(value))
+            valid = checkTime == len(value)
+        except:
+            valid = False
+
+        if not valid:
+##            dbg('cannot convert string to valid time')
+            pass
+        if valid: dbg('valid time')
+##        dbg(indent=0)
+        return valid
+
+
+    def _OnKillFocus(self,event):
+        """ Handler for EVT_KILL_FOCUS event.
+        """
+##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1)
+        if self._mask and self._IsEditable():
+            self._AdjustField(self._GetInsertionPoint())
+            self._CheckValid()   ## Call valid handler
+
+        self._LostFocus()    ## Provided for subclass use
+        event.Skip()
+##        dbg(indent=0)
+
+
+    def _fixSelection(self):
+        """
+        This gets called after the TAB traversal selection is made, if the
+        focus event was due to this, but before the EVT_LEFT_* events if
+        the focus shift was due to a mouse event.
+
+        The trouble is that, a priori, there's no explicit notification of
+        why the focus event we received.  However, the whole reason we need to
+        do this is because the default behavior on TAB traveral in a wxTextCtrl is
+        now to select the entire contents of the window, something we don't want.
+        So we can *now* test the selection range, and if it's "the whole text"
+        we can assume the cause, change the insertion point to the start of
+        the control, and deselect.
+        """
+##        dbg('MaskedEditMixin::_fixSelection', indent=1)
+        if not self._mask or not self._IsEditable():
+##            dbg(indent=0)
+            return
+
+        sel_start, sel_to = self._GetSelection()
+##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty())
+
+        if( sel_start == 0 and sel_to >= len( self._mask )   #(can be greater in numeric controls because of reserved space)
+            and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ):
+            # This isn't normally allowed, and so assume we got here by the new
+            # "tab traversal" behavior, so we need to reset the selection
+            # and insertion point:
+##            dbg('entire text selected; resetting selection to start of control')
+            self._goHome()
+            field = self._FindField(self._GetInsertionPoint())
+            edit_start, edit_end = field._extent
+            if field._selectOnFieldEntry:
+                self._SetInsertionPoint(edit_start)
+                self._SetSelection(edit_start, edit_end)
+
+            elif field._insertRight:
+                self._SetInsertionPoint(edit_end)
+                self._SetSelection(edit_end, edit_end)
+
+        elif (self._isFloat or self._isInt):
+
+            text, signpos, right_signpos = self._getAbsValue()
+            if text is None or text == self._template:
+                integer = self._fields[0]
+                edit_start, edit_end = integer._extent
+
+                if integer._selectOnFieldEntry:
+##                    dbg('select on field entry:')
+                    self._SetInsertionPoint(edit_start)
+                    self._SetSelection(edit_start, edit_end)
+
+                elif integer._insertRight:
+##                    dbg('moving insertion point to end')
+                    self._SetInsertionPoint(edit_end)
+                    self._SetSelection(edit_end, edit_end)
+                else:
+##                    dbg('numeric ctrl is empty; start at beginning after sign')
+                    self._SetInsertionPoint(signpos+1)   ## Move past minus sign space if signed
+                    self._SetSelection(signpos+1, signpos+1)
+
+        elif sel_start > self._goEnd(getPosOnly=True):
+##            dbg('cursor beyond the end of the user input; go to end of it')
+            self._goEnd()
+        else:
+##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength)
+            pass
+##        dbg(indent=0)
+
+
+    def _Keypress(self,key):
+        """ Method provided to override OnChar routine. Return False to force
+            a skip of the 'normal' OnChar process. Called before class OnChar.
+        """
+        return True
+
+
+    def _LostFocus(self):
+        """ Method provided for subclasses. _LostFocus() is called after
+            the class processes its EVT_KILL_FOCUS event code.
+        """
+        pass
+
+
+    def _OnDoubleClick(self, event):
+        """ selects field under cursor on dclick."""
+        pos = self._GetInsertionPoint()
+        field = self._FindField(pos)
+        start, end = field._extent
+        self._SetInsertionPoint(start)
+        self._SetSelection(start, end)
+
+
+    def _Change(self):
+        """ Method provided for subclasses. Called by internal EVT_TEXT
+            handler. Return False to override the class handler, True otherwise.
+        """
+        return True
+
+
+    def _Cut(self):
+        """
+        Used to override the default Cut() method in base controls, instead
+        copying the selection to the clipboard and then blanking the selection,
+        leaving only the mask in the selected area behind.
+        Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the
+        derived control because the mixin functions can't override a method of
+        a sibling class.
+        """
+##        dbg("MaskedEditMixin::_Cut", indent=1)
+        value = self._GetValue()
+##        dbg('current value: "%s"' % value)
+        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
+##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
+        do = wxTextDataObject()
+        do.SetText(value[sel_start:sel_to].strip())
+        wxTheClipboard.Open()
+        wxTheClipboard.SetData(do)
+        wxTheClipboard.Close()
+
+        if sel_to - sel_start != 0:
+            self._OnErase()
+##        dbg(indent=0)
+
+
+# WS Note: overriding Copy is no longer necessary given that you
+# can no longer select beyond the last non-empty char in the control.
+#
+##    def _Copy( self ):
+##        """
+##        Override the wxTextCtrl's .Copy function, with our own
+##        that does validation.  Need to strip trailing spaces.
+##        """
+##        sel_start, sel_to = self._GetSelection()
+##        select_len = sel_to - sel_start
+##        textval = wxTextCtrl._GetValue(self)
+##
+##        do = wxTextDataObject()
+##        do.SetText(textval[sel_start:sel_to].strip())
+##        wxTheClipboard.Open()
+##        wxTheClipboard.SetData(do)
+##        wxTheClipboard.Close()
+
+
+    def _getClipboardContents( self ):
+        """ Subroutine for getting the current contents of the clipboard.
+        """
+        do = wxTextDataObject()
+        wxTheClipboard.Open()
+        success = wxTheClipboard.GetData(do)
+        wxTheClipboard.Close()
+
+        if not success:
+            return None
+        else:
+            # Remove leading and trailing spaces before evaluating contents
+            return do.GetText().strip()
+
+
+    def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False):
+        """
+        Used by paste routine and field choice validation to see
+        if a given slice of paste text is legal for the area in question:
+        returns validity, replacement text, and extent of paste in
+        template.
+        """
+##        dbg(suspend=1)
+##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1)
+        select_length = sel_to - sel_start
+        maxlength = select_length
+##        dbg('sel_to - sel_start:', maxlength)
+        if maxlength == 0:
+            maxlength = self._masklength - sel_start
+            item = 'control'
+        else:
+            item = 'selection'
+##        dbg('maxlength:', maxlength)
+        length_considered = len(paste_text)
+        if length_considered > maxlength:
+##            dbg('paste text will not fit into the %s:' % item, indent=0)
+            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))
+                else:
+                    raise ValueError('"%s" will not fit into the selection' % paste_text)
+            else:
+##                dbg(indent=0, suspend=0)
+                return False, None, None
+
+        text = self._template
+##        dbg('length_considered:', length_considered)
+
+        valid_paste = True
+        replacement_text = ""
+        replace_to = sel_start
+        i = 0
+        while valid_paste and i < length_considered and replace_to < self._masklength:
+            if paste_text[i:] == self._template[replace_to:length_considered]:
+                # remainder of paste matches template; skip char-by-char analysis
+##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered))
+                replacement_text += paste_text[i:]
+                replace_to = i = length_considered
+                continue
+            # else:
+            char = paste_text[i]
+            field = self._FindField(replace_to)
+            if not field._compareNoCase:
+                if field._forceupper:   char = char.upper()
+                elif field._forcelower: char = char.lower()
+
+##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to)
+##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to))
+            if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True):
+                replacement_text += char
+##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals())
+##                dbg("replacement_text:", '"'+replacement_text+'"')
+                i += 1
+                replace_to += 1
+            elif( char == self._template[replace_to]
+                  or (self._signOk and
+                          ( (i == 0 and (char == '-' or (self._useParens and char == '(')))
+                            or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ):
+                replacement_text += char
+##                dbg("'%(char)s' == template(%(replace_to)d)" % locals())
+##                dbg("replacement_text:", '"'+replacement_text+'"')
+                i += 1
+                replace_to += 1
+            else:
+                next_entry = self._findNextEntry(replace_to, adjustInsert=False)
+                if next_entry == replace_to:
+                    valid_paste = False
+                else:
+                    replacement_text += self._template[replace_to:next_entry]
+##                    dbg("skipping template; next_entry =", next_entry)
+##                    dbg("replacement_text:", '"'+replacement_text+'"')
+                    replace_to = next_entry  # so next_entry will be considered on next loop
+
+        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))
+
+        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))
+
+##        dbg('valid_paste?', valid_paste)
+        if valid_paste:
+##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
+            pass
+##        dbg(indent=0, suspend=0)
+        return valid_paste, replacement_text, replace_to
+
+
+    def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
+        """
+        Used to override the base control's .Paste() function,
+        with our own that does validation.
+        Note: _Paste must be called from a Paste() override in the
+        derived control because the mixin functions can't override a
+        method of a sibling class.
+        """
+##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
+        if value is None:
+            paste_text = self._getClipboardContents()
+        else:
+            paste_text = value
+
+        if paste_text is not None:
+##            dbg('paste text: "%s"' % paste_text)
+            # (conversion will raise ValueError if paste isn't legal)
+            sel_start, sel_to = self._GetSelection()
+##            dbg('selection:', (sel_start, sel_to))
+
+            # special case: handle allowInsert fields properly
+            field = self._FindField(sel_start)
+            edit_start, edit_end = field._extent
+            new_pos = None
+            if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end:
+                new_pos = sel_start + len(paste_text)   # store for subsequent positioning
+                paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
+##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end)
+                sel_to = sel_start + len(paste_text)
+
+            # Another special case: paste won't fit, but it's a right-insert field where entire
+            # non-empty value is selected, and there's room if the selection is expanded leftward:
+            if( len(paste_text) > sel_to - sel_start
+                and field._insertRight
+                and sel_start > edit_start
+                and sel_to >= edit_end
+                and not self._GetValue()[edit_start:sel_start].strip() ):
+                # text won't fit within selection, but left of selection is empty;
+                # check to see if we can expand selection to accomodate the value:
+                empty_space = sel_start - edit_start
+                amount_needed = len(paste_text) - (sel_to - sel_start)
+                if amount_needed <= empty_space:
+                    sel_start -= amount_needed
+##                    dbg('expanded selection to:', (sel_start, sel_to))
+
+
+            # another special case: deal with signed values properly:
+            if self._signOk:
+                signedvalue, signpos, right_signpos = self._getSignedValue()
+                paste_signpos = paste_text.find('-')
+                if paste_signpos == -1:
+                    paste_signpos = paste_text.find('(')
+
+                # if paste text will result in signed value:
+####                dbg('paste_signpos != -1?', paste_signpos != -1)
+####                dbg('sel_start:', sel_start, 'signpos:', signpos)
+####                dbg('field._insertRight?', field._insertRight)
+####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos)
+                if paste_signpos != -1 and (sel_start <= signpos
+                                            or (field._insertRight and sel_start - len(paste_text) <= signpos)):
+                    signed = True
+                else:
+                    signed = False
+                # remove "sign" from paste text, so we can auto-adjust for sign type after paste:
+                paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','')
+##                dbg('unsigned paste text: "%s"' % paste_text)
+            else:
+                signed = False
+
+            # another special case: deal with insert-right fields when selection is empty and
+            # cursor is at end of field:
+####            dbg('field._insertRight?', field._insertRight)
+####            dbg('sel_start == edit_end?', sel_start == edit_end)
+####            dbg('sel_start', sel_start, 'sel_to', sel_to)
+            if field._insertRight and sel_start == edit_end and sel_start == sel_to:
+                sel_start -= len(paste_text)
+                if sel_start < 0:
+                    sel_start = 0
+##                dbg('adjusted selection:', (sel_start, sel_to))
+
+            try:
+                valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
+            except:
+##                dbg('exception thrown', indent=0)
+                raise
+
+            if not valid_paste:
+##                dbg('paste text not legal for the selection or portion of the control following the cursor;')
+                if not wx.Validator_IsSilent():
+                    wx.Bell()
+##                dbg(indent=0)
+                return False
+            # else...
+            text = self._eraseSelection()
+
+            new_text = text[:sel_start] + replacement_text + text[replace_to:]
+            if new_text:
+                new_text = string.ljust(new_text,self._masklength)
+            if signed:
+                new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text)
+                if new_text:
+                    if self._useParens:
+                        new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:]
+                    else:
+                        new_text = new_text[:signpos] + '-' + new_text[signpos+1:]
+                    if not self._isNeg:
+                        self._isNeg = 1
+
+##            dbg("new_text:", '"'+new_text+'"')
+
+            if not just_return_value:
+                if new_text != self._GetValue():
+                    self.modified = True
+                if new_text == '':
+                    self.ClearValue()
+                else:
+                    wx.CallAfter(self._SetValue, new_text)
+                    if new_pos is None:
+                        new_pos = sel_start + len(replacement_text)
+                    wx.CallAfter(self._SetInsertionPoint, new_pos)
+            else:
+##                dbg(indent=0)
+                return new_text
+        elif just_return_value:
+##            dbg(indent=0)
+            return self._GetValue()
+##        dbg(indent=0)
+
+    def _Undo(self):
+        """ Provides an Undo() method in base controls. """
+##        dbg("MaskedEditMixin::_Undo", indent=1)
+        value = self._GetValue()
+        prev = self._prevValue
+##        dbg('current value:  "%s"' % value)
+##        dbg('previous value: "%s"' % prev)
+        if prev is None:
+##            dbg('no previous value', indent=0)
+            return
+
+        elif value != prev:
+            # Determine what to select: (relies on fixed-length strings)
+            # (This is a lot harder than it would first appear, because
+            # of mask chars that stay fixed, and so break up the "diff"...)
+
+            # Determine where they start to differ:
+            i = 0
+            length = len(value)     # (both are same length in masked control)
+
+            while( value[:i] == prev[:i] ):
+                    i += 1
+            sel_start = i - 1
+
+
+            # handle signed values carefully, so undo from signed to unsigned or vice-versa
+            # works properly:
+            if self._signOk:
+                text, signpos, right_signpos = self._getSignedValue(candidate=prev)
+                if self._useParens:
+                    if prev[signpos] == '(' and prev[right_signpos] == ')':
+                        self._isNeg = True
+                    else:
+                        self._isNeg = False
+                    # eliminate source of "far-end" undo difference if using balanced parens:
+                    value = value.replace(')', ' ')
+                    prev = prev.replace(')', ' ')
+                elif prev[signpos] == '-':
+                    self._isNeg = True
+                else:
+                    self._isNeg = False
+
+            # Determine where they stop differing in "undo" result:
+            sm = difflib.SequenceMatcher(None, a=value, b=prev)
+            i, j, k = sm.find_longest_match(sel_start, length, sel_start, length)
+##            dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
+
+            if k == 0:                              # no match found; select to end
+                sel_to = length
+            else:
+                code_5tuples = sm.get_opcodes()
+                for op, i1, i2, j1, j2 in code_5tuples:
+##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2]))
+                    pass
+
+                diff_found = False
+                # look backward through operations needed to produce "previous" value;
+                # first change wins:
+                for next_op in range(len(code_5tuples)-1, -1, -1):
+                    op, i1, i2, j1, j2 = code_5tuples[next_op]
+##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2])
+                    if op == 'insert' and prev[j1:j2] != self._template[j1:j2]:
+##                        dbg('insert found: selection =>', (j1, j2))
+                        sel_start = j1
+                        sel_to = j2
+                        diff_found = True
+                        break
+                    elif op == 'delete' and value[i1:i2] != self._template[i1:i2]:
+                        field = self._FindField(i2)
+                        edit_start, edit_end = field._extent
+                        if field._insertRight and i2 == edit_end:
+                            sel_start = i2
+                            sel_to = i2
+                        else:
+                            sel_start = i1
+                            sel_to = j1
+##                        dbg('delete found: selection =>', (sel_start, sel_to))
+                        diff_found = True
+                        break
+                    elif op == 'replace':
+##                        dbg('replace found: selection =>', (j1, j2))
+                        sel_start = j1
+                        sel_to = j2
+                        diff_found = True
+                        break
+
+
+                if diff_found:
+                    # now go forwards, looking for earlier changes:
+                    for next_op in range(len(code_5tuples)):
+                        op, i1, i2, j1, j2 = code_5tuples[next_op]
+                        field = self._FindField(i1)
+                        if op == 'equal':
+                            continue
+                        elif op == 'replace':
+##                            dbg('setting sel_start to', i1)
+                            sel_start = i1
+                            break
+                        elif op == 'insert' and not value[i1:i2]:
+##                            dbg('forward %s found' % op)
+                            if prev[j1:j2].strip():
+##                                dbg('item to insert non-empty; setting sel_start to', j1)
+                                sel_start = j1
+                                break
+                            elif not field._insertRight:
+##                                dbg('setting sel_start to inserted space:', j1)
+                                sel_start = j1
+                                break
+                        elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip():
+                            continue
+                        else:
+                            # we've got what we need
+                            break
+
+
+                if not diff_found:
+##                    dbg('no insert,delete or replace found (!)')
+                    # do "left-insert"-centric processing of difference based on l.c.s.:
+                    if i == j and j != sel_start:         # match starts after start of selection
+                        sel_to = sel_start + (j-sel_start)  # select to start of match
+                    else:
+                        sel_to = j                          # (change ends at j)
+
+
+            # There are several situations where the calculated difference is
+            # not what we want to select.  If changing sign, or just adding
+            # group characters, we really don't want to highlight the characters
+            # changed, but instead leave the cursor where it is.
+            # Also, there a situations in which the difference can be ambiguous;
+            # Consider:
+            #
+            # current value:    11234
+            # previous value:   1111234
+            #
+            # Where did the cursor actually lie and which 1s were selected on the delete
+            # operation?
+            #
+            # Also, difflib can "get it wrong;" Consider:
+            #
+            # current value:    "       128.66"
+            # previous value:   "       121.86"
+            #
+            # difflib produces the following opcodes, which are sub-optimal:
+            #    equal value[0:9] (       12) prev[0:9] (       12)
+            #   insert value[9:9] () prev[9:11] (1.)
+            #    equal value[9:10] (8) prev[11:12] (8)
+            #   delete value[10:11] (.) prev[12:12] ()
+            #    equal value[11:12] (6) prev[12:13] (6)
+            #   delete value[12:13] (6) prev[13:13] ()
+            #
+            # This should have been:
+            #    equal value[0:9] (       12) prev[0:9] (       12)
+            #  replace value[9:11] (8.6) prev[9:11] (1.8)
+            #    equal value[12:13] (6) prev[12:13] (6)
+            #
+            # But it didn't figure this out!
+            #
+            # To get all this right, we use the previous selection recorded to help us...
+
+            if (sel_start, sel_to) != self._prevSelection:
+##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection)
+
+                prev_sel_start, prev_sel_to = self._prevSelection
+                field = self._FindField(sel_start)
+
+                if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')')
+                                     or self._curValue[sel_start] in ('-', '(', ')')):
+                    # change of sign; leave cursor alone...
+                    sel_start, sel_to = self._prevSelection
+
+                elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar
+                                             or self._prevValue[sel_start:sel_to] == field._groupChar):
+                    # do not highlight grouping changes
+                    sel_start, sel_to = self._prevSelection
+
+                else:
+                    calc_select_len = sel_to - sel_start
+                    prev_select_len = prev_sel_to - prev_sel_start
+
+##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start)
+##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to)
+
+                    if prev_select_len >= calc_select_len:
+                        # old selection was bigger; trust it:
+                        sel_start, sel_to = self._prevSelection
+
+                    elif( sel_to > prev_sel_to                  # calculated select past last selection
+                          and prev_sel_to < len(self._template) # and prev_sel_to not at end of control
+                          and sel_to == len(self._template) ):  # and calculated selection goes to end of control
+
+                        i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length)
+##                        dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
+                        if k > 0:
+                            # difflib must not have optimized opcodes properly;
+                            sel_to = j
+
+                    else:
+                        # look for possible ambiguous diff:
+
+                        # if last change resulted in no selection, test from resulting cursor position:
+                        if prev_sel_start == prev_sel_to:
+                            calc_select_len = sel_to - sel_start
+                            field = self._FindField(prev_sel_start)
+
+                            # determine which way to search from last cursor position for ambiguous change:
+                            if field._insertRight:
+                                test_sel_start = prev_sel_start
+                                test_sel_to = prev_sel_start + calc_select_len
+                            else:
+                                test_sel_start = prev_sel_start - calc_select_len
+                                test_sel_to = prev_sel_start
+                        else:
+                            test_sel_start, test_sel_to = prev_sel_start, prev_sel_to
+
+##                        dbg('test selection:', (test_sel_start, test_sel_to))
+##                        dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to])
+##                        dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to])
+
+                        # if calculated selection spans characters, and same characters
+                        # "before" the previous insertion point are present there as well,
+                        # select the ones related to the last known selection instead.
+                        if( sel_start != sel_to
+                            and test_sel_to < len(self._template)
+                            and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ):
+
+                            sel_start, sel_to = test_sel_start, test_sel_to
+
+##            dbg('sel_start, sel_to:', sel_start, sel_to)
+##            dbg('previous value: "%s"' % self._prevValue)
+            self._SetValue(self._prevValue)
+            self._SetInsertionPoint(sel_start)
+            self._SetSelection(sel_start, sel_to)
+        else:
+##            dbg('no difference between previous value')
+            pass
+##        dbg(indent=0)
+
+
+    def _OnClear(self, event):
+        """ Provides an action for context menu delete operation """
+        self.ClearValue()
+
+
+    def _OnContextMenu(self, event):
+##        dbg('MaskedEditMixin::OnContextMenu()', indent=1)
+        menu = wxMenu()
+        menu.Append(wxID_UNDO, "Undo", "")
+        menu.AppendSeparator()
+        menu.Append(wxID_CUT, "Cut", "")
+        menu.Append(wxID_COPY, "Copy", "")
+        menu.Append(wxID_PASTE, "Paste", "")
+        menu.Append(wxID_CLEAR, "Delete", "")
+        menu.AppendSeparator()
+        menu.Append(wxID_SELECTALL, "Select All", "")
+
+        EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z)
+        EVT_MENU(menu, wxID_CUT, self._OnCtrl_X)
+        EVT_MENU(menu, wxID_COPY, self._OnCtrl_C)
+        EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V)
+        EVT_MENU(menu, wxID_CLEAR, self._OnClear)
+        EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A)
+
+        # ## WSS: The base control apparently handles
+        # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE
+        # and wxID_CLEAR menu items even if the menu is one
+        # we created.  However, it doesn't do undo properly,
+        # so we're keeping track of previous values ourselves.
+        # Therefore, we have to override the default update for
+        # that item on the menu:
+        EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI)
+        self._contextMenu = menu
+
+        self.PopupMenu(menu, event.GetPosition())
+        menu.Destroy()
+        self._contextMenu = None
+##        dbg(indent=0)
+
+    def _UndoUpdateUI(self, event):
+        if self._prevValue is None or self._prevValue == self._curValue:
+            self._contextMenu.Enable(wxID_UNDO, False)
+        else:
+            self._contextMenu.Enable(wxID_UNDO, True)
+
+
+    def _OnCtrlParametersChanged(self):
+        """
+        Overridable function to allow derived classes to take action as a
+        result of parameter changes prior to possibly changing the value
+        of the control.
+        """
+        pass
+
+ ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+# ## TRICKY BIT: to avoid a ton of boiler-plate, and to
+# ## automate the getter/setter generation for each valid
+# ## control parameter so we never forget to add the
+# ## functions when adding parameters, this loop
+# ## programmatically adds them to the class:
+# ## (This makes it easier for Designers like Boa to
+# ## deal with masked controls.)
+#
+# ## To further complicate matters, this is done with an
+# ## extra level of inheritance, so that "general" classes like
+# ## MaskedTextCtrl can have all possible attributes,
+# ## while derived classes, like TimeCtrl and MaskedNumCtrl
+# ## can prevent exposure of those optional attributes of their base
+# ## class that do not make sense for their derivation.  Therefore,
+# ## we define
+# ##    BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin)
+# ## and
+# ##    MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin).
+# ##
+# ## This allows us to then derive:
+# ##    MaskedNumCtrl( BaseMaskedTextCtrl )
+# ##
+# ## and not have to expose all the same accessor functions for the
+# ## derived control when they don't all make sense for it.
+# ##
+class MaskedEditAccessorsMixin:
+
+    # Define the default set of attributes exposed by the most generic masked controls:
+    exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys()
+    exposed_basectrl_params.remove('index')
+    exposed_basectrl_params.remove('extent')
+    exposed_basectrl_params.remove('foregroundColour')   # (base class already has this)
+
+    for param in exposed_basectrl_params:
+        propname = param[0].upper() + param[1:]
+        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+        if param.find('Colour') != -1:
+            # add non-british spellings, for backward-compatibility
+            propname.replace('Colour', 'Color')
+
+            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+## these are helper subroutines:
+
+def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '):
+    """ addseparators = add separator character every three numerals if True
+    """
+    fmt0 = fmtstring.split('.')
+    fmt1 = fmt0[0]
+    fmt2 = fmt0[1]
+    val  = origvalue.split('.')[0].strip()
+    ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2)
+    if neg:
+        ret = '-' + ret[1:]
+    return (ret,len(fmt1))
+
+
+def isDateType( fmtstring ):
+    """ Checks the mask and returns True if it fits an allowed
+        date or datetime format.
+    """
+    dateMasks = ("^##/##/####",
+                 "^##-##-####",
+                 "^##.##.####",
+                 "^####/##/##",
+                 "^####-##-##",
+                 "^####.##.##",
+                 "^##/CCC/####",
+                 "^##.CCC.####",
+                 "^##/##/##$",
+                 "^##/##/## ",
+                 "^##/CCC/##$",
+                 "^##.CCC.## ",)
+    reString  = "|".join(dateMasks)
+    filter = re.compile( reString)
+    if re.match(filter,fmtstring): return True
+    return False
+
+def isTimeType( fmtstring ):
+    """ Checks the mask and returns True if it fits an allowed
+        time format.
+    """
+    reTimeMask = "^##:##(:##)?( (AM|PM))?"
+    filter = re.compile( reTimeMask )
+    if re.match(filter,fmtstring): return True
+    return False
+
+
+def isFloatingPoint( fmtstring):
+    filter = re.compile("[ ]?[#]+\.[#]+\n")
+    if re.match(filter,fmtstring+"\n"): return True
+    return False
+
+
+def isInteger( fmtstring ):
+    filter = re.compile("[#]+\n")
+    if re.match(filter,fmtstring+"\n"): return True
+    return False
+
+
+def getDateParts( dateStr, dateFmt ):
+    if len(dateStr) > 11: clip = dateStr[0:11]
+    else:                 clip = dateStr
+    if clip[-2] not in string.digits:
+        clip = clip[:-1]    # (got part of time; drop it)
+
+    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
+    slices  = clip.split(dateSep)
+    if dateFmt == "MDY":
+        y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts
+    elif dateFmt == "DMY":
+        y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts
+    elif dateFmt == "YMD":
+        y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts
+    else:
+        y,m,d = None, None, None
+    if not y:
+        return None
+    else:
+        return y,m,d
+
+
+def getDateSepChar(dateStr):
+    clip   = dateStr[0:10]
+    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
+    return dateSep
+
+
+def makeDate( year, month, day, dateFmt, dateStr):
+    sep    = getDateSepChar( dateStr)
+    if dateFmt == "MDY":
+        return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts
+    elif dateFmt == "DMY":
+        return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts
+    elif dateFmt == "YMD":
+        return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts
+    else:
+        return none
+
+
+def getYear(dateStr,dateFmt):
+    parts = getDateParts( dateStr, dateFmt)
+    return parts[0]
+
+def getMonth(dateStr,dateFmt):
+    parts = getDateParts( dateStr, dateFmt)
+    return parts[1]
+
+def getDay(dateStr,dateFmt):
+    parts = getDateParts( dateStr, dateFmt)
+    return parts[2]
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+class test(wx.PySimpleApp):
+        def OnInit(self):
+            from wx.lib.rcsizer import RowColSizer
+            self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
+            self.panel = wx.Panel( self.frame, -1)
+            self.sizer = RowColSizer()
+            self.labels = []
+            self.editList  = []
+            rowcount    = 4
+
+            id, id1 = wx.NewId(), wx.NewId()
+            self.command1  = wx.Button( self.panel, id, "&Close" )
+            self.command2  = wx.Button( self.panel, id1, "&AutoFormats" )
+            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
+            self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5)
+            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 )
+##            self.panel.SetDefaultItem(self.command1 )
+            self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2)
+
+            self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" )
+            self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" )
+            self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 )
+            self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 )
+            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 )
+            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 )
+
+
+            label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field.
+Note that all controls have been auto-sized by including F in the format code.
+Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status)."""
+            label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)."
+
+            self.label1 = wx.StaticText( self.panel, -1, label)
+            self.label2 = wx.StaticText( self.panel, -1, "Description")
+            self.label3 = wx.StaticText( self.panel, -1, "Mask Value")
+            self.label4 = wx.StaticText( self.panel, -1, "Format")
+            self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)")
+            self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl")
+            self.label7 = wx.StaticText( self.panel, -1, label2)
+            self.label7.SetForegroundColour("Blue")
+            self.label1.SetForegroundColour("Blue")
+            self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+
+            self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5)
+
+            # The following list is of the controls for the demo. Feel free to play around with
+            # the options!
+            controls = [
+            #description        mask                    excl format     regexp                              range,list,initial
+           ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''),
+           ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''),
+           ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''),
+           ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''),
+           ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''),
+           ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'),
+           ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''),
+           ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'),
+           ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''),
+           ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '),
+           ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '),
+           ("Month selector",   "XXX",                  "", 'F',        "",                                 (),
+                ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""),
+           ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (),
+                ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "")
+           ]
+
+            for control in controls:
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL)
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL)
+
+                if control in controls[:]:#-2]:
+                    newControl  = MaskedTextCtrl( self.panel, -1, "",
+                                                    mask         = control[1],
+                                                    excludeChars = control[2],
+                                                    formatcodes  = control[3],
+                                                    includeChars = "",
+                                                    validRegex   = control[4],
+                                                    validRange   = control[5],
+                                                    choices      = control[6],
+                                                    defaultValue = control[7],
+                                                    demo         = True)
+                    if control[6]: newControl.SetCtrlParameters(choiceRequired = True)
+                else:
+                    newControl = MaskedComboBox(  self.panel, -1, "",
+                                                    choices = control[7],
+                                                    choiceRequired  = True,
+                                                    mask         = control[1],
+                                                    formatcodes  = control[3],
+                                                    excludeChars = control[2],
+                                                    includeChars = "",
+                                                    validRegex   = control[4],
+                                                    validRange   = control[5],
+                                                    demo         = True)
+                self.editList.append( newControl )
+
+                self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5)
+                rowcount += 1
+
+            self.sizer.AddGrowableCol(4)
+
+            self.panel.SetSizer(self.sizer)
+            self.panel.SetAutoLayout(1)
+
+            self.frame.Show(1)
+            self.MainLoop()
+
+            return True
+
+        def onClick(self, event):
+            self.frame.Close()
+
+        def onClickPage(self, event):
+            self.page2 = test2(self.frame,-1,"")
+            self.page2.Show(True)
+
+        def _onCheck1(self,event):
+            """ Set required value on/off """
+            value = event.IsChecked()
+            if value:
+                for control in self.editList:
+                    control.SetCtrlParameters(emptyInvalid=True)
+                    control.Refresh()
+            else:
+                for control in self.editList:
+                    control.SetCtrlParameters(emptyInvalid=False)
+                    control.Refresh()
+            self.panel.Refresh()
+
+        def _onCheck2(self,event):
+            """ Highlight empty values"""
+            value = event.IsChecked()
+            if value:
+                for control in self.editList:
+                    control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine')
+                    control.Refresh()
+            else:
+                for control in self.editList:
+                    control.SetCtrlParameters( emptyBackgroundColour = 'White')
+                    control.Refresh()
+            self.panel.Refresh()
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class test2(wx.Frame):
+        def __init__(self, parent, id, caption):
+            wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
+            from wx.lib.rcsizer import RowColSizer
+            self.panel = wx.Panel( self, -1)
+            self.sizer = RowColSizer()
+            self.labels = []
+            self.texts  = []
+            rowcount    = 4
+
+            label = """\
+All these controls have been created by passing a single parameter, the AutoFormat code.
+The class contains an internal dictionary of types and formats (autoformats).
+To see a great example of validations in action, try entering a bad email address, then tab out."""
+
+            self.label1 = wx.StaticText( self.panel, -1, label)
+            self.label2 = wx.StaticText( self.panel, -1, "Description")
+            self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code")
+            self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control")
+            self.label1.SetForegroundColour("Blue")
+            self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+            self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
+
+            self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
+            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
+
+            id, id1 = wx.NewId(), wx.NewId()
+            self.command1  = wx.Button( self.panel, id, "&Close")
+            self.command2  = wx.Button( self.panel, id1, "&Print Formats")
+            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1)
+            self.panel.SetDefaultItem(self.command1)
+            self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2)
+
+            # The following list is of the controls for the demo. Feel free to play around with
+            # the options!
+            controls = [
+           ("Phone No","USPHONEFULLEXT"),
+           ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"),
+           ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
+           ("Time (with seconds)","TIMEHHMMSS"),
+           ("Military Time\n(without seconds)","24HRTIMEHHMM"),
+           ("Social Sec#","USSOCIALSEC"),
+           ("Credit Card","CREDITCARD"),
+           ("Expiration MM/YY","EXPDATEMMYY"),
+           ("Percentage","PERCENT"),
+           ("Person's Age","AGE"),
+           ("US Zip Code","USZIP"),
+           ("US Zip+4","USZIPPLUS4"),
+           ("Email Address","EMAIL"),
+           ("IP Address", "(derived control IpAddrCtrl)")
+           ]
+
+            for control in controls:
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
+                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
+                if control in controls[:-1]:
+                    self.sizer.Add( MaskedTextCtrl( self.panel, -1, "",
+                                                      autoformat  = control[1],
+                                                      demo        = True),
+                                row=rowcount,col=2,flag=wx.ALL,border=5)
+                else:
+                    self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ),
+                                    row=rowcount,col=2,flag=wx.ALL,border=5)
+                rowcount += 1
+
+            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
+            self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5)
+            self.sizer.AddGrowableCol(3)
+
+            self.panel.SetSizer(self.sizer)
+            self.panel.SetAutoLayout(1)
+
+        def onClick(self, event):
+            self.Close()
+
+        def onClickPrint(self, event):
+            for format in masktags.keys():
+                sep = "+------------------------+"
+                print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex'])
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+if __name__ == "__main__":
+    app = test(False)
+
+i=1
+##
+## Current Issues:
+## ===================================
+##
+## 1. WS: For some reason I don't understand, the control is generating two (2)
+##      EVT_TEXT events for every one (1) .SetValue() of the underlying control.
+##      I've been unsuccessful in determining why or in my efforts to make just one
+##      occur.  So, I've added a hack to save the last seen value from the
+##      control in the EVT_TEXT handler, and if *different*, call event.Skip()
+##      to propagate it down the event chain, and let the application see it.
+##
+## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the
+##      behavior of the underlying control that I can't fix.  The problems are:
+##      a) The background coloring doesn't work in the text field of the control;
+##         instead, there's a only border around it that assumes the correct color.
+##      b) The control will not pass WXK_TAB to the event handler, no matter what
+##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to
+##         indicate that we want these events.  As a result, MaskedComboBox
+##         doesn't do the nice field-tabbing that MaskedTextCtrl does.
+##      c) Auto-complete had to be reimplemented for the control because programmatic
+##         setting of the value of the text field does not set up the auto complete
+##         the way that the control processing keystrokes does.  (But I think I've
+##         implemented a fairly decent approximation.)  Because of this the control
+##         also won't auto-complete on dropdown, and there's no event I can catch
+##         to work around this problem.
+##      d) There is no method provided for getting the selection; the hack I've
+##         implemented has its flaws, not the least of which is that due to the
+##         strategy that I'm using, the paste buffer is always replaced by the
+##         contents of the control's selection when in focus, on each keystroke;
+##         this makes it impossible to paste anything into a MaskedComboBox
+##         at the moment... :-(
+##      e) The other deficient behavior, likely induced by the workaround for (d),
+##         is that you can can't shift-left to select more than one character
+##         at a time.
+##
+##
+## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their
+##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in
+##      wxWindows, shift-tab won't take you backwards through the fields of
+##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will
+##      work like shift-tab and tab ought to.
+##
+
+## To-Do's:
+## =============================##
+##  1. Add Popup list for auto-completable fields that simulates combobox on individual
+##     fields.  Example: City validates against list of cities, or zip vs zip code list.
+##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal"
+##     control.
+##  3. Fix shift-left selection for MaskedComboBox.
+##  5. Transform notion of "decimal control" to be less "entire control"-centric,
+##     so that monetary symbols can be included and still have the appropriate
+##     semantics.  (Big job, as currently written, but would make control even
+##     more useful for business applications.)
+
+
+## CHANGELOG:
+## ====================
+##  Version 1.6
+##  1. Reorganized masked controls into separate package, renamed things accordingly
+##  2. Split actual controls out of this file into their own files.
+##  Version 1.5
+##  (Reported) bugs fixed:
+##   1. Crash ensues if you attempt to change the mask of a read-only
+##      MaskedComboBox after initial construction.
+##   2. Changed strategy of defining Get/Set property functions so that
+##      these are now generated dynamically at runtime, rather than as
+##      part of the class definition.  (This makes it possible to have
+##      more general base classes that have many more options for configuration
+##      without requiring that derivations support the same options.)
+##   3. Fixed IsModified for _Paste() and _OnErase().
+##
+##   Enhancements:
+##   1. Fixed "attribute function inheritance," since base control is more
+##      generic than subsequent derivations, not all property functions of a
+##      generic control should be exposed in those derivations.  New strategy
+##      uses base control classes (eg. BaseMaskedTextCtrl) that should be
+##      used to derive new class types, and mixed with their own mixins to
+##      only expose those attributes from the generic masked controls that
+##      make sense for the derivation.  (This makes Boa happier.)
+##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less
+##      "parochial."
+##
+##  Version 1.4
+##  (Reported) bugs fixed:
+##   1. Right-click menu allowed "cut" operation that destroyed mask
+##      (was implemented by base control)
+##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all
+##      got converted to lower case.
+##   3. MaskedComboBox selection didn't deal with spaces in values
+##      properly when autocompleting, and didn't have a concept of "next"
+##      match for handling choice list duplicates.
+##   4. Size of MaskedComboBox was always default.
+##   5. Email address regexp allowed some "non-standard" things, and wasn't
+##      general enough.
+##   6. Couldn't easily reset MaskedComboBox contents programmatically.
+##   7. Couldn't set emptyInvalid during construction.
+##   8. Under some versions of wxPython, readonly comboboxes can apparently
+##      return a GetInsertionPoint() result (655535), causing masked control
+##      to fail.
+##   9. Specifying an empty mask caused the controls to traceback.
+##  10. Can't specify float ranges for validRange.
+##  11. '.' from within a the static portion of a restricted IP address
+##      destroyed the mask from that point rightward; tab when cursor is
+##      before 1st field takes cursor past that field.
+##
+##  Enhancements:
+##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.)
+##  13. Added auto-select option on char input for masked controls with
+##      choice lists.
+##  14. Added '>' formatcode, allowing insert within a given or each field
+##      as appropriate, rather than requiring "overwrite".  This makes single
+##      field controls that just have validation rules (eg. EMAIL) much more
+##      friendly.  The same flag controls left shift when deleting vs just
+##      blanking the value, and for right-insert fields, allows right-insert
+##      at any non-blank (non-sign) position in the field.
+##  15. Added option to use to indicate negative values for numeric controls.
+##  16. Improved OnFocus handling of numeric controls.
+##  17. Enhanced Home/End processing to allow operation on a field level,
+##      using ctrl key.
+##  18. Added individual Get/Set functions for control parameters, for
+##      simplified integration with Boa Constructor.
+##  19. Standardized "Colour" parameter names to match wxPython, with
+##      non-british spellings still supported for backward-compatibility.
+##  20. Added '&' mask specification character for punctuation only (no letters
+##      or digits).
+##  21. Added (in a separate file) wxMaskedCtrl() factory function to provide
+##      unified interface to the masked edit subclasses.
+##
+##
+##  Version 1.3
+##   1. Made it possible to configure grouping, decimal and shift-decimal characters,
+##      to make controls more usable internationally.
+##   2. Added code to smart "adjust" value strings presented to .SetValue()
+##      for right-aligned numeric format controls if they are shorter than
+##      than the control width,  prepending the missing portion, prepending control
+##      template left substring for the missing characters, so that setting
+##      numeric values is easier.
+##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved
+##      for b-c), as this makes more sense.
+##
+##  Version 1.2
+##   1. Fixed .SetValue() to replace the current value, rather than the current
+##      selection. Also changed it to generate ValueError if presented with
+##      either a value which doesn't follow the format or won't fit.  Also made
+##      set value adjust numeric and date controls as if user entered the value.
+##      Expanded doc explaining how SetValue() works.
+##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
+##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
+##   3. Made all date autoformats automatically pick implied "datestyle".
+##   4. Added IsModified override, since base wxTextCtrl never reports modified if
+##      .SetValue used to change the value, which is what the masked edit controls
+##      use internally.
+##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when
+##      using tab to "leave field" and auto-adjust.
+##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes,
+##      and bug in ._Paste() that didn't account for signs in signed masks either.
+##   7. Fixed issues with _adjustPos for right-insert fields causing improper
+##      selection/replacement of values
+##   8. Fixed _OnHome handler to properly handle extending current selection to
+##      beginning of control.
+##   9. Exposed all (valid) autoformats to demo, binding descriptions to
+##      autoformats.
+##  10. Fixed a couple of bugs in email regexp.
+##  11. Made maskchardict an instance var, to make mask chars to be more
+##      amenable to international use.
+##  12. Clarified meaning of '-' formatcode in doc.
+##  13. Fixed a couple of coding bugs being flagged by Python2.1.
+##  14. Fixed several issues with sign positioning, erasure and validity
+##      checking for "numeric" masked controls.
+##  15. Added validation to IpAddrCtrl.SetValue().
+##
+##  Version 1.1
+##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default)
+##      vs. literal font facename, and use wxTELETYPE as the font family
+##      if so specified.
+##   2. Switched to use of dbg module vs. locally defined version.
+##   3. Revamped entire control structure to use Field classes to hold constraint
+##      and formatting data, to make code more hierarchical, allow for more
+##      sophisticated masked edit construction.
+##   4. Better strategy for managing options, and better validation on keywords.
+##   5. Added 'V' format code, which requires that in order for a character
+##      to be accepted, it must result in a string that passes the validRegex.
+##   6. Added 'S' format code which means "select entire field when navigating
+##      to new field."
+##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment)
+##   8. Added '<' format code to allow fields to require explicit cursor movement
+##      to leave field.
+##   9. Added validFunc option to other validation mechanisms, that allows derived
+##      classes to add dynamic validation constraints to the control.
+##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also
+##      fixed failure to obey case conversion codes when pasting.
+##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere...
+##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.'
+##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal,
+##      individual field validation to prevent field values > 255, and require explicit
+##      tab/. to change fields.
+##  14. Added handler for left double-click to select field under cursor.
+##  15. Fixed handling for "Read-only" styles.
+##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor
+##      attribute, for more consistent and controllable coloring.
+##  17. Added retainFieldValidation parameter, allowing top-level constraints
+##      such as "validRequired" to be set independently of field-level equivalent.
+##      (needed in TimeCtrl for bounds constraints.)
+##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed
+##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue,
+##      etc.
+##  19. Fixed maskchar setting for upper/lowercase, to work in all locales.
+##
+##
+##  Version 1.0
+##   1. Decimal point behavior restored for decimal and integer type controls:
+##      decimal point now trucates the portion > 0.
+##   2. Return key now works like the tab character and moves to the next field,
+##      provided no default button is set for the form panel on which the control
+##      resides.
+##   3. Support added in _FindField() for subclasses controls (like timecontrol)
+##      to determine where the current insertion point is within the mask (i.e.
+##      which sub-'field'). See method documentation for more info and examples.
+##   4. Added Field class and support for all constraints to be field-specific
+##      in addition to being globally settable for the control.
+##      Choices for each field are validated for length and pastability into
+##      the field in question, raising ValueError if not appropriate for the control.
+##      Also added selective additional validation based on individual field constraints.
+##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all
+##      auto-complete fields with choice lists, supplying the 1st entry in
+##      the choice list if the field is empty, and cycling through the list in
+##      the appropriate direction if already a match.  WXK_DOWN will also auto-
+##      complete if the field is partially completed and a match can be made.
+##      SHIFT-WXK_UP/DOWN will also take you to the next field after any
+##      auto-completion performed.
+##   5. Added autoCompleteKeycodes=[] parameters for allowing further
+##      customization of the control.  Any keycode supplied as a member
+##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If
+##      requireFieldChoice is set, then a valid value from each non-empty
+##      choice list will be required for the value of the control to validate.
+##   6. Fixed "auto-sizing" to be relative to the font actually used, rather
+##      than making assumptions about character width.
+##   7. Fixed GetMaskParameter(), which was non-functional in previous version.
+##   8. Fixed exceptions raised to provide info on which control had the error.
+##   9. Fixed bug in choice management of MaskedComboBox.
+##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of
+##     the form '# #'.  Modified control code for IpAddrCtrl so that '.'
+##     in the middle of a field clips the rest of that field, similar to
+##     decimal and integer controls.
+##
+##
+##  Version 0.0.7
+##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive.
+##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333).
+##   3. New support for selecting text within the control.(thanks Will Sadkin!)
+##      Shift-End and Shift-Home now select text as you would expect
+##      Control-Shift-End selects to the end of the mask string, even if value not entered.
+##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control.
+##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed-
+##      for some reason I couldn't find the original email but thanks!!!)
+##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome,
+##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc.
+##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!).
+##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...)
+##   7. New mechanism for replacing default behavior for any given key, using
+##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available
+##      for easier subclassing of the control.
+##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs
+##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut"
+##      semantics, erasing the selection, rather than erasing the entire control.
+##   9. Added option for an "default value" (ie. the template) for use when a single fillChar
+##      is not desired in every position.  Added IsDefault() function to mean "does the value
+##      equal the template?" and modified .IsEmpty() to mean "do all of the editable
+##      positions in the template == the fillChar?"
+##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox,
+##      now included.
+##  11. MaskedComboBox now adds the capability to validate from list of valid values.
+##      Example: City validates against list of cities, or zip vs zip code list.
+##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being
+##      passed to the next handler in the event chain, causing updates to the
+##      control to be invisible to the parent code.
+##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within
+##      the control, that auto-reformats as you move between cells.
+##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'.
+##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14}
+##  16. Fixed major bugs in date validation, due to the fact that
+##      wxDateTime.ParseDate is too liberal, and will accept any form that
+##      makes any kind of sense, regardless of the datestyle you specified
+##      for the control.  Unfortunately, the strategy used to fix it only
+##      works for versions of wxPython post 2.3.3.1, as a C++ assert box
+##      seems to show up on an invalid date otherwise, instead of a catchable
+##      exception.
+##  17. Enhanced date adjustment to automatically adjust heuristic based on
+##      current year, making last century/this century determination on
+##      2-digit year based on distance between today's year and value;
+##      if > 50 year separation, assume last century (and don't assume last
+##      century is 20th.)
+##  18. Added autoformats and support for including HHMMSS as well as HHMM for
+##      date times, and added similar time, and militaray time autoformats.
+##  19. Enhanced tabbing logic so that tab takes you to the next field if the
+##      control is a multi-field control.
+##  20. Added stub method called whenever the control "changes fields", that
+##      can be overridden by subclasses (eg. IpAddrCtrl.)
+##  21. Changed a lot of code to be more functionally-oriented so side-effects
+##      aren't as problematic when maintaining code and/or adding features.
+##      Eg: IsValid() now does not have side-effects; it merely reflects the
+##      validity of the value of the control; to determine validity AND recolor
+##      the control, _CheckValid() should be used with a value argument of None.
+##      Similarly, made most reformatting function take an optional candidate value
+##      rather than just using the current value of the control, and only
+##      have them change the value of the control if a candidate is not specified.
+##      In this way, you can do validation *before* changing the control.
+##  22. Changed validRequired to mean "disallow chars that result in invalid
+##      value."  (Old meaning now represented by emptyInvalid.)  (This was
+##      possible once I'd made the changes in (19) above.)
+##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they
+##      can be set/modified/retrieved after construction.  Removed individual
+##      parameter setting functions, in favor of this mechanism, so that
+##      all adjustment of the control based on changing parameter values can
+##      be handled in one place with unified mechanism.
+##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability
+##      to type "grouping char" (ie. ',') and validate as appropriate.
+##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9.
+##  26. Fixed assumption about "decimal or integer" masks so that they're only
+##      made iff there's no validRegex associated with the field.  (This
+##      is so things like zipcodes which look like integers can have more
+##      restrictive validation (ie. must be 5 digits.)
+##  27. Added a ton more doc strings to explain use and derivation requirements
+##      and did regularization of the naming conventions.
+##  28. Fixed a range bug in _adjustKey preventing z from being handled properly.
+##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to
+##      reformat the value and move the next field as appropriate. (shift-'.',
+##      ie. '>' moves to the previous field.
+
+##  Version 0.0.6
+##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending
+##      in '0'.
+##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the
+##      year position, the control will expand the value to four digits, using numerals below
+##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn).
+##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM}
+##   3. revalid parameter renamed validRegex to conform to standard for all validation
+##      parameters (see 2 new ones below).
+##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types.
+##      Allows the developer to specify a valid low/high range of values.
+##   5. New optional init parameter = validList. Used for character types. Allows developer
+##      to send a list of values to the control to be used for specific validation.
+##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams.
+##   6. Date type fields now use wxDateTime's parser to validate the date and time.
+##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing
+##      me toward this solution!
+##   7. Date fields now automatically expand 2-digit years when it can. For example,
+##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year
+##      date is entered it will be expanded in any case when the user tabs out of the
+##      field.
+##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor,
+##      SetSignedForeColor allow accessto override default class coloring behavior.
+##   9. Documentation updated and improved.
+##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better.
+##      Two new options (checkboxes) - test highlight empty and disallow empty.
+##  11. Home and End now work more intuitively, moving to the first and last user-entry
+##      value, respectively.
+##  12. New class function: SetRequired(bool). Sets the control's entry required flag
+##      (i.e. disallow empty values if True).
+##
+##  Version 0.0.5
+##   1. get_plainValue method renamed to GetPlainValue following the wxWindows
+##      StudlyCaps(tm) standard (thanks Paul Moore).  ;)
+##   2. New format code 'F' causes the control to auto-fit (auto-size) itself
+##      based on the length of the mask template.
+##   3. Class now supports "autoformat" codes. These can be passed to the class
+##      on instantiation using the parameter autoformat="code". If the code is in
+##      the dictionary, it will self set the mask, formatting, and validation string.
+##      I have included a number of samples, but I am hoping that someone out there
+##      can help me to define a whole bunch more.
+##   4. I have added a second page to the demo (as well as a second demo class, test2)
+##      to showcase how autoformats work. The way they self-format and self-size is,
+##      I must say, pretty cool.
+##   5. Comments added and some internal cosmetic revisions re: matching the code
+##      standards for class submission.
+##   6. Regex validation is now done in real time - field turns yellow immediately
+##      and stays yellow until the entered value is valid
+##   7. Cursor now skips over template characters in a more intuitive way (before the
+##      next keypress).
+##   8. Change, Keypress and LostFocus methods added for convenience of subclasses.
+##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR,
+##      and EVT_KILL_FOCUS, respectively.
+##   9. Decimal and numeric handlers have been rewritten and now work more intuitively.
+##
+##  Version 0.0.4
+##   1. New .IsEmpty() method returns True if the control's value is equal to the
+##      blank template string
+##   2. Control now supports a new init parameter: revalid. Pass a regular expression
+##      that the value will have to match when the control loses focus. If invalid,
+##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next).
+##   3. Demo now shows revalid functionality. Try entering a partial value, such as a
+##      partial social security number.
+##   4. New .IsValid() value returns True if the control is empty, or if the value matches
+##      the revalid expression. If not, .IsValid() returns False.
+##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never
+##      presses the decimal point.
+##   6. Cursor now goes to the beginning of the field if the user clicks in an
+##      "empty" field intead of leaving the insertion point in the middle of the
+##      field.
+##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9.
+##   8. New formatcodes init parameter replaces other init params and adds functions.
+##      String passed to control on init controls:
+##        _ Allow spaces
+##        ! Force upper
+##        ^ Force lower
+##        R Show negative #s in red
+##        , Group digits
+##        - Signed numerals
+##        0 Numeric fields get leading zeros
+##   9. Ctrl-X in any field clears the current value.
+##   10. Code refactored and made more modular (esp in OnChar method). Should be more
+##       easy to read and understand.
+##   11. Demo enhanced.
+##   12. Now has _doc_.
+##
+##  Version 0.0.3
+##   1. GetPlainValue() now returns the value without the template characters;
+##      so, for example, a social security number (123-33-1212) would return as
+##      123331212; also removes white spaces from numeric/decimal values, so
+##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value.
+##   2. Press '.' in an integer style masked control and truncate any trailing digits.
+##   3. Code moderately refactored. Internal names improved for clarity. Additional
+##      internal documentation.
+##   4. Home and End keys now supported to move cursor to beginning or end of field.
+##   5. Un-signed integers and decimals now supported.
+##   6. Cosmetic improvements to the demo.
+##   7. Class renamed to MaskedTextCtrl.
+##   8. Can now specify include characters that will override the basic
+##      controls: for example, includeChars = "@." for email addresses
+##   9. Added mask character 'C' -> allow any upper or lowercase character
+##   10. .SetSignColor(str:color) sets the foreground color for negative values
+##       in signed controls (defaults to red)
+##   11. Overview documentation written.
+##
+##  Version 0.0.2
+##   1. Tab now works properly when pressed in last position
+##   2. Decimal types now work (e.g. #####.##)
+##   3. Signed decimal or numeric values supported (i.e. negative numbers)
+##   4. Negative decimal or numeric values now can show in red.
+##   5. Can now specify an "exclude list" with the excludeChars parameter.
+##      See date/time formatted example - you can only enter A or P in the
+##      character mask space (i.e. AM/PM).
+##   6. Backspace now works properly, including clearing data from a selected
+##      region but leaving template characters intact. Also delete key.
+##   7. Left/right arrows now work properly.
+##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3
+##
diff --git a/wxPython/wx/lib/masked/numctrl.py b/wxPython/wx/lib/masked/numctrl.py
new file mode 100644 (file)
index 0000000..17c9588
--- /dev/null
@@ -0,0 +1,1616 @@
+#----------------------------------------------------------------------------
+# Name:         wxPython.lib.masked.numctrl.py
+# Author:       Will Sadkin
+# Created:      09/06/2003
+# Copyright:   (c) 2003 by Will Sadkin
+# RCS-ID:      $Id$
+# License:     wxWidgets license
+#----------------------------------------------------------------------------
+# NOTE:
+#   This was written to provide a numeric edit control for wxPython that
+#   does things like right-insert (like a calculator), and does grouping, etc.
+#   (ie. the features of masked.TextCtrl), but allows Get/Set of numeric
+#   values, rather than text.
+#
+#   Masked.NumCtrl permits integer, and floating point values to be set
+#   retrieved or set via .GetValue() and .SetValue() (type chosen based on
+#   fraction width, and provides an masked.EVT_NUM() event function for trapping
+#   changes to the control.
+#
+#   It supports negative numbers as well as the naturals, and has the option
+#   of not permitting leading zeros or an empty control; if an empty value is
+#   not allowed, attempting to delete the contents of the control will result
+#   in a (selected) value of zero, thus preserving a legitimate numeric value.
+#   Similarly, replacing the contents of the control with '-' will result in
+#   a selected (absolute) value of -1.
+#
+#   masked.NumCtrl also supports range limits, with the option of either
+#   enforcing them or simply coloring the text of the control if the limits
+#   are exceeded.
+#
+#   masked.NumCtrl is intended to support fixed-point numeric entry, and
+#   is derived from BaseMaskedTextCtrl.  As such, it supports a limited range
+#   of values to comply with a fixed-width entry mask.
+#----------------------------------------------------------------------------
+# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Updated for wx namespace
+#
+# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o wxMaskedEditMixin -> MaskedEditMixin
+# o wxMaskedTextCtrl -> masked.TextCtrl
+# o wxMaskedNumNumberUpdatedEvent -> masked.NumberUpdatedEvent
+# o wxMaskedNumCtrl -> masked.NumCtrl
+#
+
+"""<html><body>
+<P>
+<B>masked.NumCtrl:</B>
+<UL>
+<LI>allows you to get and set integer or floating point numbers as value,</LI>
+<LI>provides bounds support and optional value limiting,</LI>
+<LI>has the right-insert input style that MaskedTextCtrl supports,</LI>
+<LI>provides optional automatic grouping, sign control and format, grouping and decimal
+character selection, etc. etc.</LI>
+</UL>
+<P>
+Being derived from MaskedTextCtrl, the control only allows
+fixed-point  notation.  That is, it has a fixed (though reconfigurable)
+maximum width for the integer portion and optional fixed width
+fractional portion.
+<P>
+Here's the API:
+<DL><PRE>
+    <B>masked.NumCtrl</B>(
+         parent, id = -1,
+         <B>value</B> = 0,
+         pos = wx.DefaultPosition,
+         size = wx.DefaultSize,
+         style = 0,
+         validator = wx.DefaultValidator,
+         name = "masked.number",
+         <B>integerWidth</B> = 10,
+         <B>fractionWidth</B> = 0,
+         <B>allowNone</B> = False,
+         <B>allowNegative</B> = True,
+         <B>useParensForNegatives</B> = False,
+         <B>groupDigits</B> = False,
+         <B>groupChar</B> = ',',
+         <B>decimalChar</B> = '.',
+         <B>min</B> = None,
+         <B>max</B> = None,
+         <B>limited</B> = False,
+         <B>selectOnEntry</b> = True,
+         <B>foregroundColour</B> = "Black",
+         <B>signedForegroundColour</B> = "Red",
+         <B>emptyBackgroundColour</B> = "White",
+         <B>validBackgroundColour</B> = "White",
+         <B>invalidBackgroundColour</B> = "Yellow",
+         <B>autoSize</B> = True
+         )
+</PRE>
+<UL>
+    <DT><B>value</B>
+    <DD>If no initial value is set, the default will be zero, or
+        the minimum value, if specified.  If an illegal string is specified,
+        a ValueError will result. (You can always later set the initial
+        value with SetValue() after instantiation of the control.)
+    <BR>
+    <DL><B>integerWidth</B>
+    <DD>Indicates how many places to the right of any decimal point
+        should be allowed in the control.  This will, perforce, limit
+        the size of the values that can be entered. This number need
+        not include space for grouping characters or the sign, if either
+        of these options are enabled, as the resulting underlying
+        mask is automatically by the control.  The default of 10
+        will allow any 32 bit integer value.  The minimum value
+        for integerWidth is 1.
+    <BR>
+    <DL><B>fractionWidth</B>
+    <DD>Indicates how many decimal places to show for numeric value.
+        If default (0), then the control will display and return only
+        integer or long values.
+    <BR>
+    <DT><B>allowNone</B>
+    <DD>Boolean indicating whether or not the control is allowed to be
+        empty, representing a value of None for the control.
+    <BR>
+    <DT><B>allowNegative</B>
+    <DD>Boolean indicating whether or not control is allowed to hold
+        negative numbers.
+    <BR>
+    <DT><B>useParensForNegatives</B>
+    <DD>If true, this will cause negative numbers to be displayed with ()s
+        rather than -, (although '-' will still trigger a negative number.)
+    <BR>
+    <DT><B>groupDigits</B>
+    <DD>Indicates whether or not grouping characters should be allowed and/or
+        inserted when leaving the control or the decimal character is entered.
+    <BR>
+    <DT><B>groupChar</B>
+    <DD>What grouping character will be used if allowed. (By default ',')
+    <BR>
+    <DT><B>decimalChar</B>
+    <DD>If fractionWidth is > 0, what character will be used to represent
+        the decimal point.  (By default '.')
+    <BR>
+    <DL><B>min</B>
+    <DD>The minimum value that the control should allow.  This can be also be
+        adjusted with SetMin().  If the control is not limited, any value
+        below this bound will result in a background colored with the current
+        invalidBackgroundColour.  If the min specified will not fit into the
+        control, the min setting will be ignored.
+    <BR>
+    <DT><B>max</B>
+    <DD>The maximum value that the control should allow.  This can be
+        adjusted with SetMax().  If the control is not limited, any value
+        above this bound will result in a background colored with the current
+        invalidBackgroundColour.  If the max specified will not fit into the
+        control, the max setting will be ignored.
+    <BR>
+    <DT><B>limited</B>
+    <DD>Boolean indicating whether the control prevents values from
+        exceeding the currently set minimum and maximum values (bounds).
+        If False and bounds are set, out-of-bounds values will
+        result in a background colored with the current invalidBackgroundColour.
+    <BR>
+    <DT><B>selectOnEntry</B>
+    <DD>Boolean indicating whether or not the value in each field of the
+        control should be automatically selected (for replacement) when
+        that field is entered, either by cursor movement or tabbing.
+        This can be desirable when using these controls for rapid data entry.
+    <BR>
+    <DT><B>foregroundColour</B>
+    <DD>Color value used for positive values of the control.
+    <BR>
+    <DT><B>signedForegroundColour</B>
+    <DD>Color value used for negative values of the control.
+    <BR>
+    <DT><B>emptyBackgroundColour</B>
+    <DD>What background color to use when the control is considered
+        "empty." (allow_none must be set to trigger this behavior.)
+    <BR>
+    <DT><B>validBackgroundColour</B>
+    <DD>What background color to use when the control value is
+        considered valid.
+    <BR>
+    <DT><B>invalidBackgroundColour</B>
+    <DD>Color value used for illegal values or values out-of-bounds of the
+        control when the bounds are set but the control is not limited.
+    <BR>
+    <DT><B>autoSize</B>
+    <DD>Boolean indicating whether or not the control should set its own
+        width based on the integer and fraction widths.  True by default.
+        <B><I>Note:</I></B> Setting this to False will produce seemingly odd
+        behavior unless the control is large enough to hold the maximum
+        specified value given the widths and the sign positions; if not,
+        the control will appear to "jump around" as the contents scroll.
+        (ie. autoSize is highly recommended.)
+</UL>
+<BR>
+<BR>
+<DT><B>masked.EVT_NUM(win, id, func)</B>
+<DD>Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when
+the value changes. Notice that this event will always be sent when the
+control's contents changes - whether this is due to user input or
+comes from the program itself (for example, if SetValue() is called.)
+<BR>
+<BR>
+<DT><B>SetValue(int|long|float|string)</B>
+<DD>Sets the value of the control to the value specified, if
+possible.  The resulting actual value of the control may be
+altered to conform to the format of the control, changed
+to conform with the bounds set on the control if limited,
+or colored if not limited but the value is out-of-bounds.
+A ValueError exception will be raised if an invalid value
+is specified.
+<BR>
+<DT><B>GetValue()</B>
+<DD>Retrieves the numeric value from the control.  The value
+retrieved will be either be returned as a long if the
+fractionWidth is 0, or a float otherwise.
+<BR>
+<BR>
+<DT><B>SetParameters(**kwargs)</B>
+<DD>Allows simultaneous setting of various attributes
+of the control after construction.  Keyword arguments
+allowed are the same parameters as supported in the constructor.
+<BR>
+<BR>
+<DT><B>SetIntegerWidth(value)</B>
+<DD>Resets the width of the integer portion of the control.  The
+value must be >= 1, or an AttributeError exception will result.
+This value should account for any grouping characters that might
+be inserted (if grouping is enabled), but does not need to account
+for the sign, as that is handled separately by the control.
+<DT><B>GetIntegerWidth()</B>
+<DD>Returns the current width of the integer portion of the control,
+not including any reserved sign position.
+<BR>
+<BR>
+<DT><B>SetFractionWidth(value)</B>
+<DD>Resets the width of the fractional portion of the control.  The
+value must be >= 0, or an AttributeError exception will result.  If
+0, the current value of the control will be truncated to an integer
+value.
+<DT><B>GetFractionWidth()</B>
+<DD>Returns the current width of the fractional portion of the control.
+<BR>
+<BR>
+<DT><B>SetMin(min=None)</B>
+<DD>Resets the minimum value of the control.  If a value of <I>None</I>
+is provided, then the control will have no explicit minimum value.
+If the value specified is greater than the current maximum value,
+then the function returns False and the minimum will not change from
+its current setting.  On success, the function returns True.
+<DT><DD>
+If successful and the current value is lower than the new lower
+bound, if the control is limited, the value will be automatically
+adjusted to the new minimum value; if not limited, the value in the
+control will be colored as invalid.
+<DT><DD>
+If min > the max value allowed by the width of the control,
+the function will return False, and the min will not be set.
+<BR>
+<DT><B>GetMin()</B>
+<DD>Gets the current lower bound value for the control.
+It will return None if no lower bound is currently specified.
+<BR>
+<BR>
+<DT><B>SetMax(max=None)</B>
+<DD>Resets the maximum value of the control. If a value of <I>None</I>
+is provided, then the control will have no explicit maximum value.
+If the value specified is less than the current minimum value, then
+the function returns False and the maximum will not change from its
+current setting. On success, the function returns True.
+<DT><DD>
+If successful and the current value is greater than the new upper
+bound, if the control is limited the value will be automatically
+adjusted to this maximum value; if not limited, the value in the
+control will be colored as invalid.
+<DT><DD>
+If max > the max value allowed by the width of the control,
+the function will return False, and the max will not be set.
+<BR>
+<DT><B>GetMax()</B>
+<DD>Gets the current upper bound value for the control.
+It will return None if no upper bound is currently specified.
+<BR>
+<BR>
+<DT><B>SetBounds(min=None,max=None)</B>
+<DD>This function is a convenience function for setting the min and max
+values at the same time.  The function only applies the maximum bound
+if setting the minimum bound is successful, and returns True
+only if both operations succeed.  <B><I>Note:</I></B> leaving out an argument
+will remove the corresponding bound.
+<DT><B>GetBounds()</B>
+<DD>This function returns a two-tuple (min,max), indicating the
+current bounds of the control.  Each value can be None if
+that bound is not set.
+<BR>
+<BR>
+<DT><B>IsInBounds(value=None)</B>
+<DD>Returns <I>True</I> if no value is specified and the current value
+of the control falls within the current bounds.  This function can also
+be called with a value to see if that value would fall within the current
+bounds of the given control.
+<BR>
+<BR>
+<DT><B>SetLimited(bool)</B>
+<DD>If called with a value of True, this function will cause the control
+to limit the value to fall within the bounds currently specified.
+If the control's value currently exceeds the bounds, it will then
+be limited accordingly.
+If called with a value of False, this function will disable value
+limiting, but coloring of out-of-bounds values will still take
+place if bounds have been set for the control.
+<DT><B>GetLimited()</B>
+<DT><B>IsLimited()</B>
+<DD>Returns <I>True</I> if the control is currently limiting the
+value to fall within the current bounds.
+<BR>
+<BR>
+<DT><B>SetAllowNone(bool)</B>
+<DD>If called with a value of True, this function will cause the control
+to allow the value to be empty, representing a value of None.
+If called with a value of False, this function will prevent the value
+from being None.  If the value of the control is currently None,
+ie. the control is empty, then the value will be changed to that
+of the lower bound of the control, or 0 if no lower bound is set.
+<DT><B>GetAllowNone()</B>
+<DT><B>IsNoneAllowed()</B>
+<DD>Returns <I>True</I> if the control currently allows its
+value to be None.
+<BR>
+<BR>
+<DT><B>SetAllowNegative(bool)</B>
+<DD>If called with a value of True, this function will cause the
+control to allow the value to be negative (and reserve space for
+displaying the sign. If called with a value of False, and the
+value of the control is currently negative, the value of the
+control will be converted to the absolute value, and then
+limited appropriately based on the existing bounds of the control
+(if any).
+<DT><B>GetAllowNegative()</B>
+<DT><B>IsNegativeAllowed()</B>
+<DD>Returns <I>True</I> if the control currently permits values
+to be negative.
+<BR>
+<BR>
+<DT><B>SetGroupDigits(bool)</B>
+<DD>If called with a value of True, this will make the control
+automatically add and manage grouping characters to the presented
+value in integer portion of the control.
+<DT><B>GetGroupDigits()</B>
+<DT><B>IsGroupingAllowed()</B>
+<DD>Returns <I>True</I> if the control is currently set to group digits.
+<BR>
+<BR>
+<DT><B>SetGroupChar()</B>
+<DD>Sets the grouping character for the integer portion of the
+control.  (The default grouping character this is ','.
+<DT><B>GetGroupChar()</B>
+<DD>Returns the current grouping character for the control.
+<BR>
+<BR>
+<DT><B>SetSelectOnEntry()</B>
+<DD>If called with a value of <I>True</I>, this will make the control
+automatically select the contents of each field as it is entered
+within the control.  (The default is True.)
+<DT><B>GetSelectOnEntry()</B>
+<DD>Returns <I>True</I> if the control currently auto selects
+the field values on entry.
+<BR>
+<BR>
+<DT><B>SetAutoSize(bool)</B>
+<DD>Resets the autoSize attribute of the control.
+<DT><B>GetAutoSize()</B>
+<DD>Returns the current state of the autoSize attribute for the control.
+<BR>
+<BR>
+</DL>
+</body></html>
+"""
+
+import  copy
+import  string
+import  types
+
+import  wx
+
+from sys import maxint
+MAXINT = maxint     # (constants should be in upper case)
+MININT = -maxint-1
+
+from wx.tools.dbg import Logger
+from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl
+dbg = Logger()
+##dbg(enable=0)
+
+#----------------------------------------------------------------------------
+
+wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wx.NewEventType()
+EVT_NUM = wx.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED, 1)
+
+#----------------------------------------------------------------------------
+
+class NumberUpdatedEvent(wx.PyCommandEvent):
+    def __init__(self, id, value = 0, object=None):
+        wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id)
+
+        self.__value = value
+        self.SetEventObject(object)
+
+    def GetValue(self):
+        """Retrieve the value of the control at the time
+        this event was generated."""
+        return self.__value
+
+
+#----------------------------------------------------------------------------
+class NumCtrlAccessorsMixin:
+    # Define masked.NumCtrl's list of attributes having their own
+    # Get/Set functions, ignoring those that make no sense for
+    # an numeric control.
+    exposed_basectrl_params = (
+         'decimalChar',
+         'shiftDecimalChar',
+         'groupChar',
+         'useParensForNegatives',
+         'defaultValue',
+         'description',
+
+         'useFixedWidthFont',
+         'autoSize',
+         'signedForegroundColour',
+         'emptyBackgroundColour',
+         'validBackgroundColour',
+         'invalidBackgroundColour',
+
+         'emptyInvalid',
+         'validFunc',
+         'validRequired',
+        )
+    for param in exposed_basectrl_params:
+        propname = param[0].upper() + param[1:]
+        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+        if param.find('Colour') != -1:
+            # add non-british spellings, for backward-compatibility
+            propname.replace('Colour', 'Color')
+
+            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+
+
+#----------------------------------------------------------------------------
+
+class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin):
+
+
+    valid_ctrl_params = {
+        'integerWidth': 10,                 # by default allow all 32-bit integers
+        'fractionWidth': 0,                 # by default, use integers
+        'decimalChar': '.',                 # by default, use '.' for decimal point
+        'allowNegative': True,              # by default, allow negative numbers
+        'useParensForNegatives': False,     # by default, use '-' to indicate negatives
+        'groupDigits': True,                # by default, don't insert grouping
+        'groupChar': ',',                   # by default, use ',' for grouping
+        'min': None,                        # by default, no bounds set
+        'max': None,
+        'limited': False,                   # by default, no limiting even if bounds set
+        'allowNone': False,                 # by default, don't allow empty value
+        'selectOnEntry': True,              # by default, select the value of each field on entry
+        'foregroundColour': "Black",
+        'signedForegroundColour': "Red",
+        'emptyBackgroundColour': "White",
+        'validBackgroundColour': "White",
+        'invalidBackgroundColour': "Yellow",
+        'useFixedWidthFont': True,          # by default, use a fixed-width font
+        'autoSize': True,                   # by default, set the width of the control based on the mask
+        }
+
+
+    def __init__ (
+                self, parent, id=-1, value = 0,
+                pos = wx.DefaultPosition, size = wx.DefaultSize,
+                style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator,
+                name = "masked.num",
+                **kwargs ):
+
+##        dbg('masked.NumCtrl::__init__', indent=1)
+
+        # Set defaults for control:
+##        dbg('setting defaults:')
+        for key, param_value in NumCtrl.valid_ctrl_params.items():
+            # This is done this way to make setattr behave consistently with
+            # "private attribute" name mangling
+            setattr(self, '_' + key, copy.copy(param_value))
+
+        # Assign defaults for all attributes:
+        init_args = copy.deepcopy(NumCtrl.valid_ctrl_params)
+##        dbg('kwargs:', kwargs)
+        for key, param_value in kwargs.items():
+            key = key.replace('Color', 'Colour')
+            if key not in NumCtrl.valid_ctrl_params.keys():
+                raise AttributeError('invalid keyword argument "%s"' % key)
+            else:
+                init_args[key] = param_value
+##        dbg('init_args:', indent=1)
+        for key, param_value in init_args.items():
+##            dbg('%s:' % key, param_value)
+            pass
+##        dbg(indent=0)
+
+        # Process initial fields for the control, as part of construction:
+        if type(init_args['integerWidth']) != types.IntType:
+            raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth']))
+        elif init_args['integerWidth'] < 1:
+            raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth']))
+
+        fields = {}
+
+        if init_args.has_key('fractionWidth'):
+            if type(init_args['fractionWidth']) != types.IntType:
+                raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth))
+            elif init_args['fractionWidth'] < 0:
+                raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth']))
+            self._fractionWidth = init_args['fractionWidth']
+
+        if self._fractionWidth:
+            fracmask = '.' + '#{%d}' % self._fractionWidth
+##            dbg('fracmask:', fracmask)
+            fields[1] = Field(defaultValue='0'*self._fractionWidth)
+        else:
+            fracmask = ''
+
+        self._integerWidth = init_args['integerWidth']
+        if init_args['groupDigits']:
+            self._groupSpace = (self._integerWidth - 1) / 3
+        else:
+            self._groupSpace = 0
+        intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
+        if self._fractionWidth:
+            emptyInvalid = False
+        else:
+            emptyInvalid = True
+        fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
+##        dbg('intmask:', intmask)
+
+        # don't bother to reprocess these arguments:
+        del init_args['integerWidth']
+        del init_args['fractionWidth']
+
+        self._autoSize = init_args['autoSize']
+        if self._autoSize:
+            formatcodes = 'FR<'
+        else:
+            formatcodes = 'R<'
+
+
+        mask = intmask+fracmask
+
+        # initial value of state vars
+        self._oldvalue = 0
+        self._integerEnd = 0
+        self._typedSign = False
+
+        # Construct the base control:
+        BaseMaskedTextCtrl.__init__(
+                self, parent, id, '',
+                pos, size, style, validator, name,
+                mask = mask,
+                formatcodes = formatcodes,
+                fields = fields,
+                validFunc=self.IsInBounds,
+                setupEventHandling = False)
+
+        self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )        ## defeat automatic full selection
+        self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )   ## run internal validator
+        self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
+        self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
+        self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab.
+        self.Bind(wx.EVT_CHAR, self._OnChar )              ## handle each keypress
+        self.Bind(wx.EVT_TEXT, self.OnTextChange )      ## color control appropriately & keep
+                                                           ## track of previous value for undo
+
+        # Establish any additional parameters, with appropriate error checking
+        self.SetParameters(**init_args)
+
+        # Set the value requested (if possible)
+##        wxCallAfter(self.SetValue, value)
+        self.SetValue(value)
+
+        # Ensure proper coloring:
+        self.Refresh()
+##        dbg('finished NumCtrl::__init__', indent=0)
+
+
+    def SetParameters(self, **kwargs):
+        """
+        This routine is used to initialize and reconfigure the control:
+        """
+##        dbg('NumCtrl::SetParameters', indent=1)
+        maskededit_kwargs = {}
+        reset_fraction_width = False
+
+
+        if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth)
+            or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth)
+            or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits)
+            or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ):
+
+            fields = {}
+
+            if kwargs.has_key('fractionWidth'):
+                if type(kwargs['fractionWidth']) != types.IntType:
+                    raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth']))
+                elif kwargs['fractionWidth'] < 0:
+                    raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth']))
+                else:
+                    if self._fractionWidth != kwargs['fractionWidth']:
+                        self._fractionWidth = kwargs['fractionWidth']
+
+            if self._fractionWidth:
+                fracmask = '.' + '#{%d}' % self._fractionWidth
+                fields[1] = Field(defaultValue='0'*self._fractionWidth)
+                emptyInvalid = False
+            else:
+                emptyInvalid = True
+                fracmask = ''
+##            dbg('fracmask:', fracmask)
+
+            if kwargs.has_key('integerWidth'):
+                if type(kwargs['integerWidth']) != types.IntType:
+##                    dbg(indent=0)
+                    raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth']))
+                elif kwargs['integerWidth'] < 0:
+##                    dbg(indent=0)
+                    raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth']))
+                else:
+                    self._integerWidth = kwargs['integerWidth']
+
+            if kwargs.has_key('groupDigits'):
+                self._groupDigits = kwargs['groupDigits']
+
+            if self._groupDigits:
+                self._groupSpace = (self._integerWidth - 1) / 3
+            else:
+                self._groupSpace = 0
+
+            intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
+##            dbg('intmask:', intmask)
+            fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
+            maskededit_kwargs['fields'] = fields
+
+            # don't bother to reprocess these arguments:
+            if kwargs.has_key('integerWidth'):
+                del kwargs['integerWidth']
+            if kwargs.has_key('fractionWidth'):
+                del kwargs['fractionWidth']
+
+            maskededit_kwargs['mask'] = intmask+fracmask
+
+        if kwargs.has_key('groupChar'):
+            old_groupchar = self._groupChar     # save so we can reformat properly
+##            dbg("old_groupchar: '%s'" % old_groupchar)
+            maskededit_kwargs['groupChar'] = kwargs['groupChar']
+        if kwargs.has_key('decimalChar'):
+            old_decimalchar = self._decimalChar
+##            dbg("old_decimalchar: '%s'" % old_decimalchar)
+            maskededit_kwargs['decimalChar'] = kwargs['decimalChar']
+
+        # for all other parameters, assign keyword args as appropriate:
+        for key, param_value in kwargs.items():
+            key = key.replace('Color', 'Colour')
+            if key not in NumCtrl.valid_ctrl_params.keys():
+                raise AttributeError('invalid keyword argument "%s"' % key)
+            elif key not in MaskedEditMixin.valid_ctrl_params.keys():
+                setattr(self, '_' + key, param_value)
+            elif key in ('mask', 'autoformat'): # disallow explicit setting of mask
+                raise AttributeError('invalid keyword argument "%s"' % key)
+            else:
+                maskededit_kwargs[key] = param_value
+##        dbg('kwargs:', kwargs)
+
+        # reprocess existing format codes to ensure proper resulting format:
+        formatcodes = self.GetCtrlParameter('formatcodes')
+        if kwargs.has_key('allowNegative'):
+            if kwargs['allowNegative'] and '-' not in formatcodes:
+                formatcodes += '-'
+                maskededit_kwargs['formatcodes'] = formatcodes
+            elif not kwargs['allowNegative'] and '-' in formatcodes:
+                formatcodes = formatcodes.replace('-','')
+                maskededit_kwargs['formatcodes'] = formatcodes
+
+        if kwargs.has_key('groupDigits'):
+            if kwargs['groupDigits'] and ',' not in formatcodes:
+                formatcodes += ','
+                maskededit_kwargs['formatcodes'] = formatcodes
+            elif not kwargs['groupDigits'] and ',' in formatcodes:
+                formatcodes = formatcodes.replace(',','')
+                maskededit_kwargs['formatcodes'] = formatcodes
+
+        if kwargs.has_key('selectOnEntry'):
+            self._selectOnEntry = kwargs['selectOnEntry']
+##            dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes)
+            if kwargs['selectOnEntry'] and 'S' not in formatcodes:
+                formatcodes += 'S'
+                maskededit_kwargs['formatcodes'] = formatcodes
+            elif not kwargs['selectOnEntry'] and 'S' in formatcodes:
+                formatcodes = formatcodes.replace('S','')
+                maskededit_kwargs['formatcodes'] = formatcodes
+
+        if kwargs.has_key('autoSize'):
+            self._autoSize = kwargs['autoSize']
+            if kwargs['autoSize'] and 'F' not in formatcodes:
+                formatcodes += 'F'
+                maskededit_kwargs['formatcodes'] = formatcodes
+            elif not kwargs['autoSize'] and 'F' in formatcodes:
+                formatcodes = formatcodes.replace('F', '')
+                maskededit_kwargs['formatcodes'] = formatcodes
+
+
+        if 'r' in formatcodes and self._fractionWidth:
+            # top-level mask should only be right insert if no fractional
+            # part will be shown; ie. if reconfiguring control, remove
+            # previous "global" setting.
+            formatcodes = formatcodes.replace('r', '')
+            maskededit_kwargs['formatcodes'] = formatcodes
+
+
+        if kwargs.has_key('limited'):
+            if kwargs['limited'] and not self._limited:
+                maskededit_kwargs['validRequired'] = True
+            elif not kwargs['limited'] and self._limited:
+                maskededit_kwargs['validRequired'] = False
+            self._limited = kwargs['limited']
+
+##        dbg('maskededit_kwargs:', maskededit_kwargs)
+        if maskededit_kwargs.keys():
+            self.SetCtrlParameters(**maskededit_kwargs)
+
+        # Record end of integer and place cursor there:
+        integerEnd = self._fields[0]._extent[1]
+        self.SetInsertionPoint(0)
+        self.SetInsertionPoint(integerEnd)
+        self.SetSelection(integerEnd, integerEnd)
+
+        # Go ensure all the format codes necessary are present:
+        orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes')
+        if 'r' not in intformat:
+            intformat += 'r'
+        if '>' not in intformat:
+            intformat += '>'
+        if intformat != orig_intformat:
+            if self._fractionWidth:
+                self.SetFieldParameters(0, formatcodes=intformat)
+            else:
+                self.SetCtrlParameters(formatcodes=intformat)
+
+        # Set min and max as appropriate:
+        if kwargs.has_key('min'):
+            min = kwargs['min']
+            if( self._max is None
+                or min is None
+                or (self._max is not None and self._max >= min) ):
+##                dbg('examining min')
+                if min is not None:
+                    try:
+                        textmin = self._toGUI(min, apply_limits = False)
+                    except ValueError:
+##                        dbg('min will not fit into control; ignoring', indent=0)
+                        raise
+##                dbg('accepted min')
+                self._min = min
+            else:
+##                dbg('ignoring min')
+                pass
+
+
+        if kwargs.has_key('max'):
+            max = kwargs['max']
+            if( self._min is None
+                or max is None
+                or (self._min is not None and self._min <= max) ):
+##                dbg('examining max')
+                if max is not None:
+                    try:
+                        textmax = self._toGUI(max, apply_limits = False)
+                    except ValueError:
+##                        dbg('max will not fit into control; ignoring', indent=0)
+                        raise
+##                dbg('accepted max')
+                self._max = max
+            else:
+##                dbg('ignoring max')
+                pass
+
+        if kwargs.has_key('allowNegative'):
+            self._allowNegative = kwargs['allowNegative']
+
+        # Ensure current value of control obeys any new restrictions imposed:
+        text = self._GetValue()
+##        dbg('text value: "%s"' % text)
+        if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1:
+            text = text.replace(old_groupchar, self._groupChar)
+        if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1:
+            text = text.replace(old_decimalchar, self._decimalChar)
+        if text != self._GetValue():
+            wx.TextCtrl.SetValue(self, text)
+
+        value = self.GetValue()
+
+##        dbg('self._allowNegative?', self._allowNegative)
+        if not self._allowNegative and self._isNeg:
+            value = abs(value)
+##            dbg('abs(value):', value)
+            self._isNeg = False
+
+        elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '':
+            if self._min > 0:
+                value = self._min
+            else:
+                value = 0
+
+        sel_start, sel_to = self.GetSelection()
+        if self.IsLimited() and self._min is not None and value < self._min:
+##            dbg('Set to min value:', self._min)
+            self._SetValue(self._toGUI(self._min))
+
+        elif self.IsLimited() and self._max is not None and value > self._max:
+##            dbg('Setting to max value:', self._max)
+            self._SetValue(self._toGUI(self._max))
+        else:
+            # reformat current value as appropriate to possibly new conditions
+##            dbg('Reformatting value:', value)
+            sel_start, sel_to = self.GetSelection()
+            self._SetValue(self._toGUI(value))
+        self.Refresh() # recolor as appropriate
+##        dbg('finished NumCtrl::SetParameters', indent=0)
+
+
+
+    def _GetNumValue(self, value):
+        """
+        This function attempts to "clean up" a text value, providing a regularized
+        convertable string, via atol() or atof(), for any well-formed numeric text value.
+        """
+        return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip()
+
+
+    def GetFraction(self, candidate=None):
+        """
+        Returns the fractional portion of the value as a float.  If there is no
+        fractional portion, the value returned will be 0.0.
+        """
+        if not self._fractionWidth:
+            return 0.0
+        else:
+            fracstart, fracend = self._fields[1]._extent
+            if candidate is None:
+                value = self._toGUI(BaseMaskedTextCtrl.GetValue(self))
+            else:
+                value = self._toGUI(candidate)
+            fracstring = value[fracstart:fracend].strip()
+            if not value:
+                return 0.0
+            else:
+                return string.atof(fracstring)
+
+    def _OnChangeSign(self, event):
+##        dbg('NumCtrl::_OnChangeSign', indent=1)
+        self._typedSign = True
+        MaskedEditMixin._OnChangeSign(self, event)
+##        dbg(indent=0)
+
+
+    def _disallowValue(self):
+##        dbg('NumCtrl::_disallowValue')
+        # limited and -1 is out of bounds
+        if self._typedSign:
+            self._isNeg = False
+        if not wx.Validator_IsSilent():
+            wx.Bell()
+        sel_start, sel_to = self._GetSelection()
+##        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
+        wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position
+        wx.CallAfter(self.SetSelection, sel_start, sel_to)
+
+    def _SetValue(self, value):
+        """
+        This routine supersedes the base masked control _SetValue().  It is
+        needed to ensure that the value of the control is always representable/convertable
+        to a numeric return value (via GetValue().)  This routine also handles
+        automatic adjustment and grouping of the value without explicit intervention
+        by the user.
+        """
+
+##        dbg('NumCtrl::_SetValue("%s")' % value, indent=1)
+
+        if( (self._fractionWidth and value.find(self._decimalChar) == -1) or
+            (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) :
+            value = self._toGUI(value)
+
+        numvalue = self._GetNumValue(value)
+##        dbg('cleansed value: "%s"' % numvalue)
+        replacement = None
+
+        if numvalue == "":
+            if self._allowNone:
+##                dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value)
+                BaseMaskedTextCtrl._SetValue(self, value)
+                self.Refresh()
+                return
+            elif self._min > 0 and self.IsLimited():
+                replacement = self._min
+            else:
+                replacement = 0
+##            dbg('empty value; setting replacement:', replacement)
+
+        if replacement is None:
+            # Go get the integer portion about to be set and verify its validity
+            intstart, intend = self._fields[0]._extent
+##            dbg('intstart, intend:', intstart, intend)
+##            dbg('raw integer:"%s"' % value[intstart:intend])
+            int = self._GetNumValue(value[intstart:intend])
+            numval = self._fromGUI(value)
+
+##            dbg('integer: "%s"' % int)
+            try:
+                fracval = self.GetFraction(value)
+            except ValueError, e:
+##                dbg('Exception:', e, 'must be out of bounds; disallow value')
+                self._disallowValue()
+##                dbg(indent=0)
+                return
+
+            if fracval == 0.0:
+##                dbg('self._isNeg?', self._isNeg)
+                if int == '-' and self._oldvalue < 0 and not self._typedSign:
+##                    dbg('just a negative sign; old value < 0; setting replacement of 0')
+                    replacement = 0
+                    self._isNeg = False
+                elif int[:2] == '-0' and self._fractionWidth == 0:
+                    if self._oldvalue < 0:
+##                        dbg('-0; setting replacement of 0')
+                        replacement = 0
+                        self._isNeg = False
+                    elif not self._limited or (self._min < -1 and self._max >= -1):
+##                        dbg('-0; setting replacement of -1')
+                        replacement = -1
+                        self._isNeg = True
+                    else:
+                        # limited and -1 is out of bounds
+                        self._disallowValue()
+##                        dbg(indent=0)
+                        return
+
+                elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0:
+                    if not self._limited or (self._min < -1 and self._max >= -1):
+##                        dbg('just a negative sign; setting replacement of -1')
+                        replacement = -1
+                    else:
+                        # limited and -1 is out of bounds
+                        self._disallowValue()
+##                        dbg(indent=0)
+                        return
+
+                elif( self._typedSign
+                      and int.find('-') != -1
+                      and self._limited
+                      and not self._min <= numval <= self._max):
+                    # changed sign resulting in value that's now out-of-bounds;
+                    # disallow
+                    self._disallowValue()
+##                    dbg(indent=0)
+                    return
+
+            if replacement is None:
+                if int and int != '-':
+                    try:
+                        string.atol(int)
+                    except ValueError:
+                        # integer requested is not legal.  This can happen if the user
+                        # is attempting to insert a digit in the middle of the control
+                        # resulting in something like "   3   45". Disallow such actions:
+##                        dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int)
+                        if not wx.Validator_IsSilent():
+                            wx.Bell()
+                        sel_start, sel_to = self._GetSelection()
+##                        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
+                        wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position
+                        wx.CallAfter(self.SetSelection, sel_start, sel_to)
+##                        dbg(indent=0)
+                        return
+
+                    if int[0] == '0' and len(int) > 1:
+##                        dbg('numvalue: "%s"' % numvalue.replace(' ', ''))
+                        if self._fractionWidth:
+                            value = self._toGUI(string.atof(numvalue))
+                        else:
+                            value = self._toGUI(string.atol(numvalue))
+##                        dbg('modified value: "%s"' % value)
+
+        self._typedSign = False     # reset state var
+
+        if replacement is not None:
+            # Value presented wasn't a legal number, but control should do something
+            # reasonable instead:
+##            dbg('setting replacement value:', replacement)
+            self._SetValue(self._toGUI(replacement))
+            sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement)))   # find where it put the 1, so we can select it
+            sel_to = sel_start + len(str(abs(replacement)))
+##            dbg('queuing selection of (%d, %d)' %(sel_start, sel_to))
+            wx.CallAfter(self.SetInsertionPoint, sel_start)
+            wx.CallAfter(self.SetSelection, sel_start, sel_to)
+##            dbg(indent=0)
+            return
+
+        # Otherwise, apply appropriate formatting to value:
+
+        # Because we're intercepting the value and adjusting it
+        # before a sign change is detected, we need to do this here:
+        if '-' in value or '(' in value:
+            self._isNeg = True
+        else:
+            self._isNeg = False
+
+##        dbg('value:"%s"' % value, 'self._useParens:', self._useParens)
+        if self._fractionWidth:
+            adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar))
+        else:
+            adjvalue = self._adjustInt(self._GetNumValue(value))
+##        dbg('adjusted value: "%s"' % adjvalue)
+
+
+        sel_start, sel_to = self._GetSelection()     # record current insertion point
+##        dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue)
+        BaseMaskedTextCtrl._SetValue(self, adjvalue)
+        # After all actions so far scheduled, check that resulting cursor
+        # position is appropriate, and move if not:
+        wx.CallAfter(self._CheckInsertionPoint)
+
+##        dbg('finished NumCtrl::_SetValue', indent=0)
+
+    def _CheckInsertionPoint(self):
+        # If current insertion point is before the end of the integer and
+        # its before the 1st digit, place it just after the sign position:
+##        dbg('NumCtrl::CheckInsertionPoint', indent=1)
+        sel_start, sel_to = self._GetSelection()
+        text = self._GetValue()
+        if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('):
+            text, signpos, right_signpos = self._getSignedValue()
+##            dbg('setting selection(%d, %d)' % (signpos+1, signpos+1))
+            self.SetInsertionPoint(signpos+1)
+            self.SetSelection(signpos+1, signpos+1)
+##        dbg(indent=0)
+
+
+    def _OnErase( self, event ):
+        """
+        This overrides the base control _OnErase, so that erasing around
+        grouping characters auto selects the digit before or after the
+        grouping character, so that the erasure does the right thing.
+        """
+##        dbg('NumCtrl::_OnErase', indent=1)
+
+        #if grouping digits, make sure deletes next to group char always
+        # delete next digit to appropriate side:
+        if self._groupDigits:
+            key = event.GetKeyCode()
+            value = BaseMaskedTextCtrl.GetValue(self)
+            sel_start, sel_to = self._GetSelection()
+
+            if key == wx.WXK_BACK:
+                # if 1st selected char is group char, select to previous digit
+                if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar:
+                    self.SetInsertionPoint(sel_start-1)
+                    self.SetSelection(sel_start-1, sel_to)
+
+                # elif previous char is group char, select to previous digit
+                elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar:
+                    self.SetInsertionPoint(sel_start-2)
+                    self.SetSelection(sel_start-2, sel_to)
+
+            elif key == wx.WXK_DELETE:
+                if( sel_to < len(self._mask) - 2 + (1 *self._useParens)
+                    and sel_start == sel_to
+                    and value[sel_to] == self._groupChar ):
+                    self.SetInsertionPoint(sel_start)
+                    self.SetSelection(sel_start, sel_to+2)
+
+                elif( sel_to < len(self._mask) - 2 + (1 *self._useParens)
+                           and value[sel_start:sel_to] == self._groupChar ):
+                    self.SetInsertionPoint(sel_start)
+                    self.SetSelection(sel_start, sel_to+1)
+
+        BaseMaskedTextCtrl._OnErase(self, event)
+##        dbg(indent=0)
+
+
+    def OnTextChange( self, event ):
+        """
+        Handles an event indicating that the text control's value
+        has changed, and issue EVT_NUM event.
+        NOTE: using wxTextCtrl.SetValue() to change the control's
+        contents from within a EVT_CHAR handler can cause double
+        text events.  So we check for actual changes to the text
+        before passing the events on.
+        """
+##        dbg('NumCtrl::OnTextChange', indent=1)
+        if not BaseMaskedTextCtrl._OnTextChange(self, event):
+##            dbg(indent=0)
+            return
+
+        # else... legal value
+
+        value = self.GetValue()
+        if value != self._oldvalue:
+            try:
+                self.GetEventHandler().ProcessEvent(
+                    NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) )
+            except ValueError:
+##                dbg(indent=0)
+                return
+            # let normal processing of the text continue
+            event.Skip()
+        self._oldvalue = value # record for next event
+##        dbg(indent=0)
+
+    def _GetValue(self):
+        """
+        Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the
+        control with this function.
+        """
+        return wx.TextCtrl.GetValue(self)
+
+
+    def GetValue(self):
+        """
+        Returns the current numeric value of the control.
+        """
+        return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) )
+
+    def SetValue(self, value):
+        """
+        Sets the value of the control to the value specified.
+        The resulting actual value of the control may be altered to
+        conform with the bounds set on the control if limited,
+        or colored if not limited but the value is out-of-bounds.
+        A ValueError exception will be raised if an invalid value
+        is specified.
+        """
+        BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) )
+
+
+    def SetIntegerWidth(self, value):
+        self.SetParameters(integerWidth=value)
+    def GetIntegerWidth(self):
+        return self._integerWidth
+
+    def SetFractionWidth(self, value):
+        self.SetParameters(fractionWidth=value)
+    def GetFractionWidth(self):
+        return self._fractionWidth
+
+
+
+    def SetMin(self, min=None):
+        """
+        Sets the minimum value of the control.  If a value of None
+        is provided, then the control will have no explicit minimum value.
+        If the value specified is greater than the current maximum value,
+        then the function returns False and the minimum will not change from
+        its current setting.  On success, the function returns True.
+
+        If successful and the current value is lower than the new lower
+        bound, if the control is limited, the value will be automatically
+        adjusted to the new minimum value; if not limited, the value in the
+        control will be colored as invalid.
+
+        If min > the max value allowed by the width of the control,
+        the function will return False, and the min will not be set.
+        """
+##        dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1)
+        if( self._max is None
+            or min is None
+            or (self._max is not None and self._max >= min) ):
+            try:
+                self.SetParameters(min=min)
+                bRet = True
+            except ValueError:
+                bRet = False
+        else:
+            bRet = False
+##        dbg(indent=0)
+        return bRet
+
+    def GetMin(self):
+        """
+        Gets the lower bound value of the control.  It will return
+        None if not specified.
+        """
+        return self._min
+
+
+    def SetMax(self, max=None):
+        """
+        Sets the maximum value of the control. If a value of None
+        is provided, then the control will have no explicit maximum value.
+        If the value specified is less than the current minimum value, then
+        the function returns False and the maximum will not change from its
+        current setting. On success, the function returns True.
+
+        If successful and the current value is greater than the new upper
+        bound, if the control is limited the value will be automatically
+        adjusted to this maximum value; if not limited, the value in the
+        control will be colored as invalid.
+
+        If max > the max value allowed by the width of the control,
+        the function will return False, and the max will not be set.
+        """
+        if( self._min is None
+            or max is None
+            or (self._min is not None and self._min <= max) ):
+            try:
+                self.SetParameters(max=max)
+                bRet = True
+            except ValueError:
+                bRet = False
+        else:
+            bRet = False
+
+        return bRet
+
+
+    def GetMax(self):
+        """
+        Gets the maximum value of the control.  It will return the current
+        maximum integer, or None if not specified.
+        """
+        return self._max
+
+
+    def SetBounds(self, min=None, max=None):
+        """
+        This function is a convenience function for setting the min and max
+        values at the same time.  The function only applies the maximum bound
+        if setting the minimum bound is successful, and returns True
+        only if both operations succeed.
+        NOTE: leaving out an argument will remove the corresponding bound.
+        """
+        ret = self.SetMin(min)
+        return ret and self.SetMax(max)
+
+
+    def GetBounds(self):
+        """
+        This function returns a two-tuple (min,max), indicating the
+        current bounds of the control.  Each value can be None if
+        that bound is not set.
+        """
+        return (self._min, self._max)
+
+
+    def SetLimited(self, limited):
+        """
+        If called with a value of True, this function will cause the control
+        to limit the value to fall within the bounds currently specified.
+        If the control's value currently exceeds the bounds, it will then
+        be limited accordingly.
+
+        If called with a value of False, this function will disable value
+        limiting, but coloring of out-of-bounds values will still take
+        place if bounds have been set for the control.
+        """
+        self.SetParameters(limited = limited)
+
+
+    def IsLimited(self):
+        """
+        Returns True if the control is currently limiting the
+        value to fall within the current bounds.
+        """
+        return self._limited
+
+    def GetLimited(self):
+        """ (For regularization of property accessors) """
+        return self.IsLimited
+
+
+    def IsInBounds(self, value=None):
+        """
+        Returns True if no value is specified and the current value
+        of the control falls within the current bounds.  This function can
+        also be called with a value to see if that value would fall within
+        the current bounds of the given control.
+        """
+##        dbg('IsInBounds(%s)' % repr(value), indent=1)
+        if value is None:
+            value = self.GetValue()
+        else:
+            try:
+                value = self._GetNumValue(self._toGUI(value))
+            except ValueError, e:
+##                dbg('error getting NumValue(self._toGUI(value)):', e, indent=0)
+                return False
+            if value.strip() == '':
+                value = None
+            elif self._fractionWidth:
+                value = float(value)
+            else:
+                value = long(value)
+
+        min = self.GetMin()
+        max = self.GetMax()
+        if min is None: min = value
+        if max is None: max = value
+
+        # if bounds set, and value is None, return False
+        if value == None and (min is not None or max is not None):
+##            dbg('finished IsInBounds', indent=0)
+            return 0
+        else:
+##            dbg('finished IsInBounds', indent=0)
+            return min <= value <= max
+
+
+    def SetAllowNone(self, allow_none):
+        """
+        Change the behavior of the validation code, allowing control
+        to have a value of None or not, as appropriate.  If the value
+        of the control is currently None, and allow_none is False, the
+        value of the control will be set to the minimum value of the
+        control, or 0 if no lower bound is set.
+        """
+        self._allowNone = allow_none
+        if not allow_none and self.GetValue() is None:
+            min = self.GetMin()
+            if min is not None: self.SetValue(min)
+            else:               self.SetValue(0)
+
+
+    def IsNoneAllowed(self):
+        return self._allowNone
+    def GetAllowNone(self):
+        """ (For regularization of property accessors) """
+        return self.IsNoneAllowed()
+
+    def SetAllowNegative(self, value):
+        self.SetParameters(allowNegative=value)
+    def IsNegativeAllowed(self):
+        return self._allowNegative
+    def GetAllowNegative(self):
+        """ (For regularization of property accessors) """
+        return self.IsNegativeAllowed()
+
+    def SetGroupDigits(self, value):
+        self.SetParameters(groupDigits=value)
+    def IsGroupingAllowed(self):
+        return self._groupDigits
+    def GetGroupDigits(self):
+        """ (For regularization of property accessors) """
+        return self.IsGroupingAllowed()
+
+    def SetGroupChar(self, value):
+        self.SetParameters(groupChar=value)
+    def GetGroupChar(self):
+        return self._groupChar
+
+    def SetDecimalChar(self, value):
+        self.SetParameters(decimalChar=value)
+    def GetDecimalChar(self):
+        return self._decimalChar
+
+    def SetSelectOnEntry(self, value):
+        self.SetParameters(selectOnEntry=value)
+    def GetSelectOnEntry(self):
+        return self._selectOnEntry
+
+    def SetAutoSize(self, value):
+        self.SetParameters(autoSize=value)
+    def GetAutoSize(self):
+        return self._autoSize
+
+
+    # (Other parameter accessors are inherited from base class)
+
+
+    def _toGUI( self, value, apply_limits = True ):
+        """
+        Conversion function used to set the value of the control; does
+        type and bounds checking and raises ValueError if argument is
+        not a valid value.
+        """
+##        dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1)
+        if value is None and self.IsNoneAllowed():
+##            dbg(indent=0)
+            return self._template
+
+        elif type(value) in (types.StringType, types.UnicodeType):
+            value = self._GetNumValue(value)
+##            dbg('cleansed num value: "%s"' % value)
+            if value == "":
+                if self.IsNoneAllowed():
+##                    dbg(indent=0)
+                    return self._template
+                else:
+##                    dbg('exception raised:', e, indent=0)
+                    raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) )
+            # else...
+            try:
+                if self._fractionWidth or value.find('.') != -1:
+                    value = float(value)
+                else:
+                    value = long(value)
+            except Exception, e:
+##                dbg('exception raised:', e, indent=0)
+                raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) )
+
+        elif type(value) not in (types.IntType, types.LongType, types.FloatType):
+##            dbg(indent=0)
+            raise ValueError (
+                'NumCtrl requires numeric value, passed %s'% repr(value) )
+
+        if not self._allowNegative and value < 0:
+            raise ValueError (
+                'control configured to disallow negative values, passed %s'% repr(value) )
+
+        if self.IsLimited() and apply_limits:
+            min = self.GetMin()
+            max = self.GetMax()
+            if not min is None and value < min:
+##                dbg(indent=0)
+                raise ValueError (
+                    'value %d is below minimum value of control'% value )
+            if not max is None and value > max:
+##                dbg(indent=0)
+                raise ValueError (
+                    'value %d exceeds value of control'% value )
+
+        adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk)
+##        dbg('len(%s):' % self._mask, len(self._mask))
+##        dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace)
+##        dbg('adjustwidth:', adjustwidth)
+        if self._fractionWidth == 0:
+            s = str(long(value)).rjust(self._integerWidth)
+        else:
+            format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth)
+            s = format % float(value)
+##        dbg('s:"%s"' % s, 'len(s):', len(s))
+        if len(s) > (adjustwidth - self._groupSpace):
+##            dbg(indent=0)
+            raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
+        elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace):
+##            dbg(indent=0)
+            raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
+
+        s = s.rjust(adjustwidth).replace('.', self._decimalChar)
+        if self._signOk and self._useParens:
+            if s.find('-') != -1:
+                s = s.replace('-', '(') + ')'
+            else:
+                s += ' '
+##        dbg('returned: "%s"' % s, indent=0)
+        return s
+
+
+    def _fromGUI( self, value ):
+        """
+        Conversion function used in getting the value of the control.
+        """
+##        dbg(suspend=0)
+##        dbg('NumCtrl::_fromGUI(%s)' % value, indent=1)
+        # One or more of the underlying text control implementations
+        # issue an intermediate EVT_TEXT when replacing the control's
+        # value, where the intermediate value is an empty string.
+        # So, to ensure consistency and to prevent spurious ValueErrors,
+        # we make the following test, and react accordingly:
+        #
+        if value.strip() == '':
+            if not self.IsNoneAllowed():
+##                dbg('empty value; not allowed,returning 0', indent = 0)
+                if self._fractionWidth:
+                    return 0.0
+                else:
+                    return 0
+            else:
+##                dbg('empty value; returning None', indent = 0)
+                return None
+        else:
+            value = self._GetNumValue(value)
+##            dbg('Num value: "%s"' % value)
+            if self._fractionWidth:
+                try:
+##                    dbg(indent=0)
+                    return float( value )
+                except ValueError:
+##                    dbg("couldn't convert to float; returning None")
+                    return None
+                else:
+                    raise
+            else:
+                try:
+##                    dbg(indent=0)
+                    return int( value )
+                except ValueError:
+                    try:
+##                       dbg(indent=0)
+                       return long( value )
+                    except ValueError:
+##                       dbg("couldn't convert to long; returning None")
+                       return None
+
+                    else:
+                       raise
+                else:
+##                    dbg('exception occurred; returning None')
+                    return None
+
+
+    def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
+        """
+        Preprocessor for base control paste; if value needs to be right-justified
+        to fit in control, do so prior to paste:
+        """
+##        dbg('NumCtrl::_Paste (value = "%s")' % value)
+        if value is None:
+            paste_text = self._getClipboardContents()
+        else:
+            paste_text = value
+
+        # treat paste as "replace number", if appropriate:
+        sel_start, sel_to = self._GetSelection()
+        if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent:
+            paste_text = self._toGUI(paste_text)
+            self._SetSelection(0, len(self._mask))
+
+        return MaskedEditMixin._Paste(self,
+                                        paste_text,
+                                        raise_on_invalid=raise_on_invalid,
+                                        just_return_value=just_return_value)
+
+
+
+#===========================================================================
+
+if __name__ == '__main__':
+
+    import traceback
+
+    class myDialog(wx.Dialog):
+        def __init__(self, parent, id, title,
+            pos = wx.DefaultPosition, size = wx.DefaultSize,
+            style = wx.DEFAULT_DIALOG_STYLE ):
+            wx.Dialog.__init__(self, parent, id, title, pos, size, style)
+
+            self.int_ctrl = NumCtrl(self, wx.NewId(), size=(55,20))
+            self.OK = wx.Button( self, wx.ID_OK, "OK")
+            self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel")
+
+            vs = wx.BoxSizer( wx.VERTICAL )
+            vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+            hs = wx.BoxSizer( wx.HORIZONTAL )
+            hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+            hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+            vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+
+            self.SetAutoLayout( True )
+            self.SetSizer( vs )
+            vs.Fit( self )
+            vs.SetSizeHints( self )
+            self.Bind(EVT_NUM, self.OnChange, self.int_ctrl)
+
+        def OnChange(self, event):
+            print 'value now', event.GetValue()
+
+    class TestApp(wx.App):
+        def OnInit(self):
+            try:
+                self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100)  )
+                self.panel = wx.Panel(self.frame, -1)
+                button = wx.Button(self.panel, -1, "Push Me", (20, 20))
+                self.Bind(wx.EVT_BUTTON, self.OnClick, button)
+            except:
+                traceback.print_exc()
+                return False
+            return True
+
+        def OnClick(self, event):
+            dlg = myDialog(self.panel, -1, "test NumCtrl")
+            dlg.int_ctrl.SetValue(501)
+            dlg.int_ctrl.SetInsertionPoint(1)
+            dlg.int_ctrl.SetSelection(1,2)
+            rc = dlg.ShowModal()
+            print 'final value', dlg.int_ctrl.GetValue()
+            del dlg
+            self.frame.Destroy()
+
+        def Show(self):
+            self.frame.Show(True)
+
+    try:
+        app = TestApp(0)
+        app.Show()
+        app.MainLoop()
+    except:
+        traceback.print_exc()
+
+i=0
+## To-Do's:
+## =============================##
+##   1. Add support for printf-style format specification.
+##   2. Add option for repositioning on 'illegal' insertion point.
+##
+## Version 1.1
+##   1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions.
+##   2. Added autoSize parameter, to allow manual sizing of the control.
+##   3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of
+##      nonsensical parameter methods from the control, so it will work
+##      properly with Boa.
+##   4. Fixed allowNone bug found by user sameerc1@grandecom.net
+
diff --git a/wxPython/wx/lib/masked/textctrl.py b/wxPython/wx/lib/masked/textctrl.py
new file mode 100644 (file)
index 0000000..d73fbe4
--- /dev/null
@@ -0,0 +1,325 @@
+#----------------------------------------------------------------------------
+# Name:         masked.textctrl.py
+# Authors:      Jeff Childers, Will Sadkin
+# Email:        jchilders_98@yahoo.com, wsadkin@nameconnector.com
+# Created:      02/11/2003
+# Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
+# Portions:     (c) 2002 by Will Sadkin, 2002-2003
+# RCS-ID:       $Id$
+# License:      wxWidgets license
+#----------------------------------------------------------------------------
+#
+# This file contains the most typically used generic masked control,
+# masked.TextCtrl.  It also defines the BaseMaskedTextCtrl, which can
+# be used to derive other "semantics-specific" classes, like masked.NumCtrl,
+# masked.TimeCtrl, and masked.IpAddrCtrl.
+#
+#----------------------------------------------------------------------------
+import  wx
+from wx.lib.masked import *
+
+# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
+# be a good place to implement the 2.3 logger class
+from wx.tools.dbg import Logger
+dbg = Logger()
+##dbg(enable=0)
+
+# ## TRICKY BIT: to avoid a ton of boiler-plate, and to
+# ## automate the getter/setter generation for each valid
+# ## control parameter so we never forget to add the
+# ## functions when adding parameters, this loop
+# ## programmatically adds them to the class:
+# ## (This makes it easier for Designers like Boa to
+# ## deal with masked controls.)
+#
+# ## To further complicate matters, this is done with an
+# ## extra level of inheritance, so that "general" classes like
+# ## MaskedTextCtrl can have all possible attributes,
+# ## while derived classes, like TimeCtrl and MaskedNumCtrl
+# ## can prevent exposure of those optional attributes of their base
+# ## class that do not make sense for their derivation.  Therefore,
+# ## we define
+# ##    BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin)
+# ## and
+# ##    MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin).
+# ##
+# ## This allows us to then derive:
+# ##    MaskedNumCtrl( BaseMaskedTextCtrl )
+# ##
+# ## and not have to expose all the same accessor functions for the
+# ## derived control when they don't all make sense for it.
+# ##
+
+class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
+    """
+    This is the primary derivation from MaskedEditMixin.  It provides
+    a general masked text control that can be configured with different
+    masks.  It's actually a "base masked textCtrl", so that the
+    MaskedTextCtrl class can be derived from it, and add those
+    accessor functions to it that are appropriate to the general class,
+    whilst other classes can derive from BaseMaskedTextCtrl, and
+    only define those accessor functions that are appropriate for
+    those derivations.
+    """
+
+    def __init__( self, parent, id=-1, value = '',
+                  pos = wx.DefaultPosition,
+                  size = wx.DefaultSize,
+                  style = wx.TE_PROCESS_TAB,
+                  validator=wx.DefaultValidator,     ## placeholder provided for data-transfer logic
+                  name = 'maskedTextCtrl',
+                  setupEventHandling = True,        ## setup event handling by default
+                  **kwargs):
+
+        wx.TextCtrl.__init__(self, parent, id, value='',
+                            pos=pos, size = size,
+                            style=style, validator=validator,
+                            name=name)
+
+        self.controlInitialized = True
+        MaskedEditMixin.__init__( self, name, **kwargs )
+
+        self._SetInitialValue(value)
+
+        if setupEventHandling:
+            ## Setup event handlers
+            self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
+            self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
+            self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
+            self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
+            self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab.
+            self.Bind(wx.EVT_CHAR, self._OnChar )               ## handle each keypress
+            self.Bind(wx.EVT_TEXT, self._OnTextChange )         ## color control appropriately & keep
+                                                                ## track of previous value for undo
+
+
+    def __repr__(self):
+        return "<BaseMaskedTextCtrl: %s>" % self.GetValue()
+
+
+    def _GetSelection(self):
+        """
+        Allow mixin to get the text selection of this control.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return self.GetSelection()
+
+    def _SetSelection(self, sel_start, sel_to):
+        """
+        Allow mixin to set the text selection of this control.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+####        dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
+        return self.SetSelection( sel_start, sel_to )
+
+    def SetSelection(self, sel_start, sel_to):
+        """
+        This is just for debugging...
+        """
+##        dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
+        wx.TextCtrl.SetSelection(self, sel_start, sel_to)
+
+
+    def _GetInsertionPoint(self):
+        return self.GetInsertionPoint()
+
+    def _SetInsertionPoint(self, pos):
+####        dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals())
+        self.SetInsertionPoint(pos)
+
+    def SetInsertionPoint(self, pos):
+        """
+        This is just for debugging...
+        """
+##        dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals())
+        wx.TextCtrl.SetInsertionPoint(self, pos)
+
+
+    def _GetValue(self):
+        """
+        Allow mixin to get the raw value of the control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return self.GetValue()
+
+    def _SetValue(self, value):
+        """
+        Allow mixin to set the raw value of the control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+##        dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
+        # Record current selection and insertion point, for undo
+        self._prevSelection = self._GetSelection()
+        self._prevInsertionPoint = self._GetInsertionPoint()
+        wx.TextCtrl.SetValue(self, value)
+##        dbg(indent=0)
+
+    def SetValue(self, value):
+        """
+        This function redefines the externally accessible .SetValue to be
+        a smart "paste" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+##        dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1)
+
+        if not self._mask:
+            wx.TextCtrl.SetValue(self, value)    # revert to base control behavior
+            return
+
+        # empty previous contents, replacing entire value:
+        self._SetInsertionPoint(0)
+        self._SetSelection(0, self._masklength)
+        if self._signOk and self._useParens:
+            signpos = value.find('-')
+            if signpos != -1:
+                value = value[:signpos] + '(' + value[signpos+1:].strip() + ')'
+            elif value.find(')') == -1 and len(value) < self._masklength:
+                value += ' '    # add place holder for reserved space for right paren
+
+        if( len(value) < self._masklength                # value shorter than control
+            and (self._isFloat or self._isInt)            # and it's a numeric control
+            and self._ctrl_constraints._alignRight ):   # and it's a right-aligned control
+
+##            dbg('len(value)', len(value), ' < self._masklength', self._masklength)
+            # try to intelligently "pad out" the value to the right size:
+            value = self._template[0:self._masklength - len(value)] + value
+            if self._isFloat and value.find('.') == -1:
+                value = value[1:]
+##            dbg('padded value = "%s"' % value)
+
+        # make SetValue behave the same as if you had typed the value in:
+        try:
+            value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
+            if self._isFloat:
+                self._isNeg = False     # (clear current assumptions)
+                value = self._adjustFloat(value)
+            elif self._isInt:
+                self._isNeg = False     # (clear current assumptions)
+                value = self._adjustInt(value)
+            elif self._isDate and not self.IsValid(value) and self._4digityear:
+                value = self._adjustDate(value, fixcentury=True)
+        except ValueError:
+            # If date, year might be 2 digits vs. 4; try adjusting it:
+            if self._isDate and self._4digityear:
+                dateparts = value.split(' ')
+                dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
+                value = string.join(dateparts, ' ')
+##                dbg('adjusted value: "%s"' % value)
+                value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
+            else:
+##                dbg('exception thrown', indent=0)
+                raise
+
+        self._SetValue(value)   # note: to preserve similar capability, .SetValue()
+                                # does not change IsModified()
+####        dbg('queuing insertion after .SetValue', self._masklength)
+        wx.CallAfter(self._SetInsertionPoint, self._masklength)
+        wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
+##        dbg(indent=0)
+
+
+    def Clear(self):
+        """ Blanks the current control value by replacing it with the default value."""
+##        dbg("MaskedTextCtrl::Clear - value reset to default value (template)")
+        if self._mask:
+            self.ClearValue()
+        else:
+            wx.TextCtrl.Clear(self)    # else revert to base control behavior
+
+
+    def _Refresh(self):
+        """
+        Allow mixin to refresh the base control with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+##        dbg('MaskedTextCtrl::_Refresh', indent=1)
+        wx.TextCtrl.Refresh(self)
+##        dbg(indent=0)
+
+
+    def Refresh(self):
+        """
+        This function redefines the externally accessible .Refresh() to
+        validate the contents of the masked control as it refreshes.
+        NOTE: this must be done in the class derived from the base wx control.
+        """
+##        dbg('MaskedTextCtrl::Refresh', indent=1)
+        self._CheckValid()
+        self._Refresh()
+##        dbg(indent=0)
+
+
+    def _IsEditable(self):
+        """
+        Allow mixin to determine if the base control is editable with this function.
+        REQUIRED by any class derived from MaskedEditMixin.
+        """
+        return wx.TextCtrl.IsEditable(self)
+
+
+    def Cut(self):
+        """
+        This function redefines the externally accessible .Cut to be
+        a smart "erase" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+        if self._mask:
+            self._Cut()             # call the mixin's Cut method
+        else:
+            wx.TextCtrl.Cut(self)    # else revert to base control behavior
+
+
+    def Paste(self):
+        """
+        This function redefines the externally accessible .Paste to be
+        a smart "paste" of the text in question, so as not to corrupt the
+        masked control.  NOTE: this must be done in the class derived
+        from the base wx control.
+        """
+        if self._mask:
+            self._Paste()                   # call the mixin's Paste method
+        else:
+            wx.TextCtrl.Paste(self, value)   # else revert to base control behavior
+
+
+    def Undo(self):
+        """
+        This function defines the undo operation for the control. (The default
+        undo is 1-deep.)
+        """
+        if self._mask:
+            self._Undo()
+        else:
+            wx.TextCtrl.Undo(self)   # else revert to base control behavior
+
+
+    def IsModified(self):
+        """
+        This function overrides the raw wxTextCtrl method, because the
+        masked edit mixin uses SetValue to change the value, which doesn't
+        modify the state of this attribute.  So, we keep track on each
+        keystroke to see if the value changes, and if so, it's been
+        modified.
+        """
+        return wx.TextCtrl.IsModified(self) or self.modified
+
+
+    def _CalcSize(self, size=None):
+        """
+        Calculate automatic size if allowed; use base mixin function.
+        """
+        return self._calcSize(size)
+
+
+class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
+    """
+    This extra level of inheritance allows us to add the generic set of
+    masked edit parameters only to this class while allowing other
+    classes to derive from the "base" masked text control, and provide
+    a smaller set of valid accessor functions.
+    """
+    pass
+
+
diff --git a/wxPython/wx/lib/masked/timectrl.py b/wxPython/wx/lib/masked/timectrl.py
new file mode 100644 (file)
index 0000000..36fbbee
--- /dev/null
@@ -0,0 +1,1317 @@
+#----------------------------------------------------------------------------
+# Name:         timectrl.py
+# Author:       Will Sadkin
+# Created:      09/19/2002
+# Copyright:    (c) 2002 by Will Sadkin, 2002
+# RCS-ID:       $Id$
+# License:      wxWindows license
+#----------------------------------------------------------------------------
+# NOTE:
+#   This was written way it is because of the lack of masked edit controls
+#   in wxWindows/wxPython.  I would also have preferred to derive this
+#   control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl
+#   component of that control is inaccessible through the interface exposed in
+#   wxPython.
+#
+#   TimeCtrl does not use validators, because it does careful manipulation
+#   of the cursor in the text window on each keystroke, and validation is
+#   cursor-position specific, so the control intercepts the key codes before the
+#   validator would fire.
+#
+#   TimeCtrl now also supports .SetValue() with either strings or wxDateTime
+#   values, as well as range limits, with the option of either enforcing them
+#   or simply coloring the text of the control if the limits are exceeded.
+#
+#   Note: this class now makes heavy use of wxDateTime for parsing and
+#   regularization, but it always does so with ephemeral instances of
+#   wxDateTime, as the C++/Python validity of these instances seems to not
+#   persist.  Because "today" can be a day for which an hour can "not exist"
+#   or be counted twice (1 day each per year, for DST adjustments), the date
+#   portion of all wxDateTimes used/returned have their date portion set to
+#   Jan 1, 1970 (the "epoch.")
+#----------------------------------------------------------------------------
+# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o Updated for V2.5 compatability
+# o wx.SpinCtl has some issues that cause the control to
+#   lock up. Noted in other places using it too, it's not this module
+#   that's at fault.
+#
+# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o wxMaskedTextCtrl -> masked.TextCtrl
+# o wxTimeCtrl -> masked.TimeCtrl
+#
+
+"""<html><body>
+<P>
+<B>TimeCtrl</B> provides a multi-cell control that allows manipulation of a time
+value.  It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime
+to get/set values from the control.
+<P>
+Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act
+like a spin control.  TimeCtrl also allows for an actual spin button to be attached
+to the control, so that it acts like the up/down arrow keys.
+<P>
+The <B>!</B> or <B>c</B> key sets the value of the control to the current time.
+<P>
+Here's the API for TimeCtrl:
+<DL><PRE>
+    <B>TimeCtrl</B>(
+         parent, id = -1,
+         <B>value</B> = '12:00:00 AM',
+         pos = wx.DefaultPosition,
+         size = wx.DefaultSize,
+         <B>style</B> = wxTE_PROCESS_TAB,
+         <B>validator</B> = wx.DefaultValidator,
+         name = "time",
+         <B>format</B> = 'HHMMSS',
+         <B>fmt24hr</B> = False,
+         <B>displaySeconds</B> = True,
+         <B>spinButton</B> = None,
+         <B>min</B> = None,
+         <B>max</B> = None,
+         <B>limited</B> = None,
+         <B>oob_color</B> = "Yellow"
+)
+</PRE>
+<UL>
+    <DT><B>value</B>
+    <DD>If no initial value is set, the default will be midnight; if an illegal string
+    is specified, a ValueError will result.  (You can always later set the initial time
+    with SetValue() after instantiation of the control.)
+    <DL><B>size</B>
+    <DD>The size of the control will be automatically adjusted for 12/24 hour format
+    if wx.DefaultSize is specified.
+    <DT><B>style</B>
+    <DD>By default, TimeCtrl will process TAB events, by allowing tab to the
+    different cells within the control.
+    <DT><B>validator</B>
+    <DD>By default, TimeCtrl just uses the default (empty) validator, as all
+    of its validation for entry control is handled internally.  However, a validator
+    can be supplied to provide data transfer capability to the control.
+    <BR>
+    <DT><B>format</B>
+    <DD>This parameter can be used instead of the fmt24hr and displaySeconds
+    parameters, respectively; it provides a shorthand way to specify the time
+    format you want.  Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and
+    '24HHMM'.  If the format is specified, the other two  arguments will be ignored.
+    <BR>
+    <DT><B>fmt24hr</B>
+    <DD>If True, control will display time in 24 hour time format; if False, it will
+    use 12 hour AM/PM format.  SetValue() will adjust values accordingly for the
+    control, based on the format specified.  (This value is ignored if the <i>format</i>
+    parameter is specified.)
+    <BR>
+    <DT><B>displaySeconds</B>
+    <DD>If True, control will include a seconds field; if False, it will
+    just show hours and minutes. (This value is ignored if the <i>format</i>
+    parameter is specified.)
+    <BR>
+    <DT><B>spinButton</B>
+    <DD>If specified, this button's events will be bound to the behavior of the
+    TimeCtrl, working like up/down cursor key events.  (See BindSpinButton.)
+    <BR>
+    <DT><B>min</B>
+    <DD>Defines the lower bound for "valid" selections in the control.
+    By default, TimeCtrl doesn't have bounds.  You must set both upper and lower
+    bounds to make the control pay attention to them, (as only one bound makes no sense
+    with times.) "Valid" times will fall between the min and max "pie wedge" of the
+    clock.
+    <DT><B>max</B>
+    <DD>Defines the upper bound for "valid" selections in the control.
+    "Valid" times will fall between the min and max "pie wedge" of the
+    clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
+    means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
+    <DT><B>limited</B>
+    <DD>If True, the control will not permit entry of values that fall outside the
+    set bounds.
+    <BR>
+    <DT><B>oob_color</B>
+    <DD>Sets the background color used to indicate out-of-bounds values for the control
+    when the control is not limited.  This is set to "Yellow" by default.
+    </DL>
+</UL>
+<BR>
+<BR>
+<BR>
+<DT><B>EVT_TIMEUPDATE(win, id, func)</B>
+<DD>func is fired whenever the value of the control changes.
+<BR>
+<BR>
+<DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
+<DD>Sets the value of the control to a particular time, given a valid
+value; raises ValueError on invalid value.
+<EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
+was successfully imported by the class module.
+<BR>
+<DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
+<DD>Retrieves the value of the time from the control.  By default this is
+returned as a string, unless one of the other arguments is set; args are
+searched in the order listed; only one value will be returned.
+<BR>
+<DT><B>GetWxDateTime(value=None)</B>
+<DD>When called without arguments, retrieves the value of the control, and applies
+it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
+The date portion will always be set to Jan 1, 1970. This form is the same
+as GetValue(as_wxDateTime=True).  GetWxDateTime can also be called with any of the
+other valid time formats settable with SetValue, to regularize it to a single
+wxDateTime form.  The function will raise ValueError on an unconvertable argument.
+<BR>
+<DT><B>GetMxDateTime()</B>
+<DD>Retrieves the value of the control and applies it to the DateTime.Time()
+constructor,and returns the resulting value.  (The date portion will always be
+set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
+compatibility with previous release.)
+<BR>
+<BR>
+<DT><B>BindSpinButton(SpinBtton)</B>
+<DD>Binds an externally created spin button to the control, so that up/down spin
+events change the active cell or selection in the control (in addition to the
+up/down cursor keys.)  (This is primarily to allow you to create a "standard"
+interface to time controls, as seen in Windows.)
+<BR>
+<BR>
+<DT><B>SetMin(min=None)</B>
+<DD>Sets the expected minimum value, or lower bound, of the control.
+(The lower bound will only be enforced if the control is
+configured to limit its values to the set bounds.)
+If a value of <I>None</I> is provided, then the control will have
+explicit lower bound.  If the value specified is greater than
+the current lower bound, then the function returns False and the
+lower bound will not change from its current setting.  On success,
+the function returns True.  Even if set, if there is no corresponding
+upper bound, the control will behave as if it is unbounded.
+<DT><DD>If successful and the current value is outside the
+new bounds, if the control is limited the value will be
+automatically adjusted to the nearest bound; if not limited,
+the background of the control will be colored with the current
+out-of-bounds color.
+<BR>
+<DT><B>GetMin(as_string=False)</B>
+<DD>Gets the current lower bound value for the control, returning
+None, if not set, or a wxDateTime, unless the as_string parameter
+is set to True, at which point it will return the string
+representation of the lower bound.
+<BR>
+<BR>
+<DT><B>SetMax(max=None)</B>
+<DD>Sets the expected maximum value, or upper bound, of the control.
+(The upper bound will only be enforced if the control is
+configured to limit its values to the set bounds.)
+If a value of <I>None</I> is provided, then the control will
+have no explicit upper bound.  If the value specified is less
+than the current lower bound, then the function returns False and
+the maximum will not change from its current setting. On success,
+the function returns True.  Even if set, if there is no corresponding
+lower bound, the control will behave as if it is unbounded.
+<DT><DD>If successful and the current value is outside the
+new bounds, if the control is limited the value will be
+automatically adjusted to the nearest bound; if not limited,
+the background of the control will be colored with the current
+out-of-bounds color.
+<BR>
+<DT><B>GetMax(as_string = False)</B>
+<DD>Gets the current upper bound value for the control, returning
+None, if not set, or a wxDateTime, unless the as_string parameter
+is set to True, at which point it will return the string
+representation of the lower bound.
+
+<BR>
+<BR>
+<DT><B>SetBounds(min=None,max=None)</B>
+<DD>This function is a convenience function for setting the min and max
+values at the same time.  The function only applies the maximum bound
+if setting the minimum bound is successful, and returns True
+only if both operations succeed.  <B><I>Note: leaving out an argument
+will remove the corresponding bound, and result in the behavior of
+an unbounded control.</I></B>
+<BR>
+<DT><B>GetBounds(as_string = False)</B>
+<DD>This function returns a two-tuple (min,max), indicating the
+current bounds of the control.  Each value can be None if
+that bound is not set.  The values will otherwise be wxDateTimes
+unless the as_string argument is set to True, at which point they
+will be returned as string representations of the bounds.
+<BR>
+<BR>
+<DT><B>IsInBounds(value=None)</B>
+<DD>Returns <I>True</I> if no value is specified and the current value
+of the control falls within the current bounds.  This function can also
+be called with a value to see if that value would fall within the current
+bounds of the given control.  It will raise ValueError if the value
+specified is not a wxDateTime, mxDateTime (if available) or parsable string.
+<BR>
+<BR>
+<DT><B>IsValid(value)</B>
+<DD>Returns <I>True</I>if specified value is a legal time value and
+falls within the current bounds of the given control.
+<BR>
+<BR>
+<DT><B>SetLimited(bool)</B>
+<DD>If called with a value of True, this function will cause the control
+to limit the value to fall within the bounds currently specified.
+(Provided both bounds have been set.)
+If the control's value currently exceeds the bounds, it will then
+be set to the nearest bound.
+If called with a value of False, this function will disable value
+limiting, but coloring of out-of-bounds values will still take
+place if bounds have been set for the control.
+<DT><B>IsLimited()</B>
+<DD>Returns <I>True</I> if the control is currently limiting the
+value to fall within the current bounds.
+<BR>
+</DL>
+</body></html>
+"""
+
+import  copy
+import  string
+import  types
+
+import  wx
+
+from wx.tools.dbg import Logger
+from wx.lib.masked import Field, BaseMaskedTextCtrl
+
+dbg = Logger()
+##dbg(enable=0)
+
+try:
+    from mx import DateTime
+    accept_mx = True
+except ImportError:
+    accept_mx = False
+
+# This class of event fires whenever the value of the time changes in the control:
+wxEVT_TIMEVAL_UPDATED = wx.NewEventType()
+EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1)
+
+class TimeUpdatedEvent(wx.PyCommandEvent):
+    def __init__(self, id, value ='12:00:00 AM'):
+        wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id)
+        self.value = value
+    def GetValue(self):
+        """Retrieve the value of the time control at the time this event was generated"""
+        return self.value
+
+class TimeCtrlAccessorsMixin:
+    # Define TimeCtrl's list of attributes having their own
+    # Get/Set functions, ignoring those that make no sense for
+    # an numeric control.
+    exposed_basectrl_params = (
+         'defaultValue',
+         'description',
+
+         'useFixedWidthFont',
+         'emptyBackgroundColour',
+         'validBackgroundColour',
+         'invalidBackgroundColour',
+
+         'validFunc',
+         'validRequired',
+        )
+    for param in exposed_basectrl_params:
+        propname = param[0].upper() + param[1:]
+        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+        if param.find('Colour') != -1:
+            # add non-british spellings, for backward-compatibility
+            propname.replace('Colour', 'Color')
+
+            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
+            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
+
+
+class TimeCtrl(BaseMaskedTextCtrl):
+
+    valid_ctrl_params = {
+        'format' :  'HHMMSS',       # default format code
+        'displaySeconds' : True,    # by default, shows seconds
+        'min': None,                # by default, no bounds set
+        'max': None,
+        'limited': False,           # by default, no limiting even if bounds set
+        'useFixedWidthFont': True,  # by default, use a fixed-width font
+        'oob_color': "Yellow"       # by default, the default masked.TextCtrl "invalid" color
+        }
+
+    def __init__ (
+                self, parent, id=-1, value = '12:00:00 AM',
+                pos = wx.DefaultPosition, size = wx.DefaultSize,
+                fmt24hr=False,
+                spinButton = None,
+                style = wx.TE_PROCESS_TAB,
+                validator = wx.DefaultValidator,
+                name = "time",
+                **kwargs ):
+
+        # set defaults for control:
+##        dbg('setting defaults:')
+        for key, param_value in TimeCtrl.valid_ctrl_params.items():
+            # This is done this way to make setattr behave consistently with
+            # "private attribute" name mangling
+            setattr(self, "_TimeCtrl__" + key, copy.copy(param_value))
+
+        # create locals from current defaults, so we can override if
+        # specified in kwargs, and handle uniformly:
+        min = self.__min
+        max = self.__max
+        limited = self.__limited
+        self.__posCurrent = 0
+        # handle deprecated keword argument name:
+        if kwargs.has_key('display_seconds'):
+            kwargs['displaySeconds'] = kwargs['display_seconds']
+            del kwargs['display_seconds']
+        if not kwargs.has_key('displaySeconds'):
+            kwargs['displaySeconds'] = True
+
+        # (handle positional arg (from original release) differently from rest of kwargs:)
+        self.__fmt24hr = False
+        if not kwargs.has_key('format'):
+            if fmt24hr:
+                if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
+                    kwargs['format'] = '24HHMMSS'
+                    del kwargs['displaySeconds']
+                else:
+                    kwargs['format'] = '24HHMM'
+            else:
+                if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
+                    kwargs['format'] = 'HHMMSS'
+                    del kwargs['displaySeconds']
+                else:
+                    kwargs['format'] = 'HHMM'
+
+        if not kwargs.has_key('useFixedWidthFont'):
+            # allow control over font selection:
+            kwargs['useFixedWidthFont'] = self.__useFixedWidthFont
+
+        maskededit_kwargs = self.SetParameters(**kwargs)
+
+        # allow for explicit size specification:
+        if size != wx.DefaultSize:
+            # override (and remove) "autofit" autoformat code in standard time formats:
+            maskededit_kwargs['formatcodes'] = 'T!'
+
+        # This allows range validation if set
+        maskededit_kwargs['validFunc'] = self.IsInBounds
+
+        # This allows range limits to affect insertion into control or not
+        # dynamically without affecting individual field constraint validation
+        maskededit_kwargs['retainFieldValidation'] = True
+
+        # Now we can initialize the base control:
+        BaseMaskedTextCtrl.__init__(
+                self, parent, id=id,
+                pos=pos, size=size,
+                style = style,
+                validator = validator,
+                name = name,
+                setupEventHandling = False,
+                **maskededit_kwargs)
+
+
+        # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
+        self._SetKeyHandler(':', self._OnChangeField)
+
+
+        # This makes the up/down keys act like spin button controls:
+        self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp)
+        self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown)
+
+
+        # This allows ! and c/C to set the control to the current time:
+        self._SetKeyHandler('!', self.__OnSetToNow)
+        self._SetKeyHandler('c', self.__OnSetToNow)
+        self._SetKeyHandler('C', self.__OnSetToNow)
+
+
+        # Set up event handling ourselves, so we can insert special
+        # processing on the ":' key to remove the "shift" attribute
+        # *before* the default handlers have been installed, so
+        # that : takes you forward, not back, and so we can issue
+        # EVT_TIMEUPDATE events on changes:
+
+        self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
+        self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
+        self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field
+        self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick
+        self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab.
+        self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event,
+                                                            ## then call BaseMaskedTextCtrl._OnChar with
+                                                            ## the possibly modified event.
+        self.Bind(wx.EVT_TEXT, self.__OnTextChange, self )  ## color control appropriately and EVT_TIMEUPDATE events
+
+
+        # Validate initial value and set if appropriate
+        try:
+            self.SetBounds(min, max)
+            self.SetLimited(limited)
+            self.SetValue(value)
+        except:
+            self.SetValue('12:00:00 AM')
+
+        if spinButton:
+            self.BindSpinButton(spinButton)     # bind spin button up/down events to this control
+
+
+    def SetParameters(self, **kwargs):
+##        dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1)
+        maskededit_kwargs = {}
+        reset_format = False
+
+        if kwargs.has_key('display_seconds'):
+            kwargs['displaySeconds'] = kwargs['display_seconds']
+            del kwargs['display_seconds']
+        if kwargs.has_key('format') and kwargs.has_key('displaySeconds'):
+            del kwargs['displaySeconds']    # always apply format if specified
+
+        # assign keyword args as appropriate:
+        for key, param_value in kwargs.items():
+            if key not in TimeCtrl.valid_ctrl_params.keys():
+                raise AttributeError('invalid keyword argument "%s"' % key)
+
+            if key == 'format':
+                # handle both local or generic 'maskededit' autoformat codes:
+                if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS':
+                    self.__displaySeconds = True
+                    self.__fmt24hr = False
+                elif param_value == 'HHMM' or param_value == 'TIMEHHMM':
+                    self.__displaySeconds = False
+                    self.__fmt24hr = False
+                elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS':
+                    self.__displaySeconds = True
+                    self.__fmt24hr = True
+                elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM':
+                    self.__displaySeconds = False
+                    self.__fmt24hr = True
+                else:
+                    raise AttributeError('"%s" is not a valid format' % param_value)
+                reset_format = True
+
+            elif key in ("displaySeconds",  "display_seconds") and not kwargs.has_key('format'):
+                self.__displaySeconds = param_value
+                reset_format = True
+
+            elif key == "min":      min = param_value
+            elif key == "max":      max = param_value
+            elif key == "limited":  limited = param_value
+
+            elif key == "useFixedWidthFont":
+                maskededit_kwargs[key] = param_value
+
+            elif key == "oob_color":
+                maskededit_kwargs['invalidBackgroundColor'] = param_value
+
+        if reset_format:
+            if self.__fmt24hr:
+                if self.__displaySeconds:  maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS'
+                else:                      maskededit_kwargs['autoformat'] = '24HRTIMEHHMM'
+
+                # Set hour field to zero-pad, right-insert, require explicit field change,
+                # select entire field on entry, and require a resultant valid entry
+                # to allow character entry:
+                hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True)
+            else:
+                if self.__displaySeconds:  maskededit_kwargs['autoformat'] = 'TIMEHHMMSS'
+                else:                      maskededit_kwargs['autoformat'] = 'TIMEHHMM'
+
+                # Set hour field to allow spaces (at start), right-insert,
+                # require explicit field change, select entire field on entry,
+                # and require a resultant valid entry to allow character entry:
+                hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True)
+                ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True)
+
+            # Field 1 is always a zero-padded right-insert minute field,
+            # similarly configured as above:
+            minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True)
+
+            fields = [ hourfield, minutefield ]
+            if self.__displaySeconds:
+                fields.append(copy.copy(minutefield))    # second field has same constraints as field 1
+
+            if not self.__fmt24hr:
+                fields.append(ampmfield)
+
+            # set fields argument:
+            maskededit_kwargs['fields'] = fields
+
+            # This allows range validation if set
+            maskededit_kwargs['validFunc'] = self.IsInBounds
+
+            # This allows range limits to affect insertion into control or not
+            # dynamically without affecting individual field constraint validation
+            maskededit_kwargs['retainFieldValidation'] = True
+
+        if hasattr(self, 'controlInitialized') and self.controlInitialized:
+            self.SetCtrlParameters(**maskededit_kwargs)   # set appropriate parameters
+
+            # Validate initial value and set if appropriate
+            try:
+                self.SetBounds(min, max)
+                self.SetLimited(limited)
+                self.SetValue(value)
+            except:
+                self.SetValue('12:00:00 AM')
+##            dbg(indent=0)
+            return {}   # no arguments to return
+        else:
+##            dbg(indent=0)
+            return maskededit_kwargs
+
+
+    def BindSpinButton(self, sb):
+        """
+        This function binds an externally created spin button to the control, so that
+        up/down events from the button automatically change the control.
+        """
+##        dbg('TimeCtrl::BindSpinButton')
+        self.__spinButton = sb
+        if self.__spinButton:
+            # bind event handlers to spin ctrl
+            self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton)
+            self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton)
+
+
+    def __repr__(self):
+        return "<TimeCtrl: %s>" % self.GetValue()
+
+
+    def SetValue(self, value):
+        """
+        Validating SetValue function for time values:
+        This function will do dynamic type checking on the value argument,
+        and convert wxDateTime, mxDateTime, or 12/24 format time string
+        into the appropriate format string for the control.
+        """
+##        dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1)
+        try:
+            strtime = self._toGUI(self.__validateValue(value))
+        except:
+##            dbg('validation failed', indent=0)
+            raise
+
+##        dbg('strtime:', strtime)
+        self._SetValue(strtime)
+##        dbg(indent=0)
+
+    def GetValue(self,
+                 as_wxDateTime = False,
+                 as_mxDateTime = False,
+                 as_wxTimeSpan = False,
+                 as_mxDateTimeDelta = False):
+
+
+        if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta:
+            value = self.GetWxDateTime()
+            if as_wxDateTime:
+                pass
+            elif as_mxDateTime:
+                value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond())
+            elif as_wxTimeSpan:
+                value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
+            elif as_mxDateTimeDelta:
+                value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
+        else:
+            value = BaseMaskedTextCtrl.GetValue(self)
+        return value
+
+
+    def SetWxDateTime(self, wxdt):
+        """
+        Because SetValue can take a wxDateTime, this is now just an alias.
+        """
+        self.SetValue(wxdt)
+
+
+    def GetWxDateTime(self, value=None):
+        """
+        This function is the conversion engine for TimeCtrl; it takes
+        one of the following types:
+            time string
+            wxDateTime
+            wxTimeSpan
+            mxDateTime
+            mxDateTimeDelta
+        and converts it to a wxDateTime that always has Jan 1, 1970 as its date
+        portion, so that range comparisons around values can work using
+        wxDateTime's built-in comparison function.  If a value is not
+        provided to convert, the string value of the control will be used.
+        If the value is not one of the accepted types, a ValueError will be
+        raised.
+        """
+        global accept_mx
+##        dbg(suspend=1)
+##        dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
+        if value is None:
+##            dbg('getting control value')
+            value = self.GetValue()
+##            dbg('value = "%s"' % value)
+
+        if type(value) == types.UnicodeType:
+            value = str(value)  # convert to regular string
+
+        valid = True    # assume true
+        if type(value) == types.StringType:
+
+            # Construct constant wxDateTime, then try to parse the string:
+            wxdt = wx.DateTimeFromDMY(1, 0, 1970)
+##            dbg('attempting conversion')
+            value = value.strip()    # (parser doesn't like leading spaces)
+            checkTime    = wxdt.ParseTime(value)
+            valid = checkTime == len(value)     # entire string parsed?
+##            dbg('checkTime == len(value)?', valid)
+
+            if not valid:
+##                dbg(indent=0, suspend=0)
+                raise ValueError('cannot convert string "%s" to valid time' % value)
+
+        else:
+            if isinstance(value, wx.DateTime):
+                hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
+            elif isinstance(value, wx.TimeSpan):
+                totalseconds = value.GetSeconds()
+                hour = totalseconds / 3600
+                minute = totalseconds / 60 - (hour * 60)
+                second = totalseconds - ((hour * 3600) + (minute * 60))
+
+            elif accept_mx and isinstance(value, DateTime.DateTimeType):
+                hour, minute, second = value.hour, value.minute, value.second
+            elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType):
+                hour, minute, second = value.hour, value.minute, value.second
+            else:
+                # Not a valid function argument
+                if accept_mx:
+                    error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value)
+                else:
+                    error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value)
+##                dbg(indent=0, suspend=0)
+                raise ValueError(error)
+
+            wxdt = wx.DateTimeFromDMY(1, 0, 1970)
+            wxdt.SetHour(hour)
+            wxdt.SetMinute(minute)
+            wxdt.SetSecond(second)
+
+##        dbg('wxdt:', wxdt, indent=0, suspend=0)
+        return wxdt
+
+
+    def SetMxDateTime(self, mxdt):
+        """
+        Because SetValue can take an mxDateTime, (if DateTime is importable),
+        this is now just an alias.
+        """
+        self.SetValue(value)
+
+
+    def GetMxDateTime(self, value=None):
+        if value is None:
+            t = self.GetValue(as_mxDateTime=True)
+        else:
+            # Convert string 1st to wxDateTime, then use components, since
+            # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
+            wxdt = self.GetWxDateTime(value)
+            hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()
+            t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second)
+        return t
+
+
+    def SetMin(self, min=None):
+        """
+        Sets the minimum value of the control.  If a value of None
+        is provided, then the control will have no explicit minimum value.
+        If the value specified is greater than the current maximum value,
+        then the function returns 0 and the minimum will not change from
+        its current setting.  On success, the function returns 1.
+
+        If successful and the current value is lower than the new lower
+        bound, if the control is limited, the value will be automatically
+        adjusted to the new minimum value; if not limited, the value in the
+        control will be colored as invalid.
+        """
+##        dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1)
+        if min is not None:
+            try:
+                min = self.GetWxDateTime(min)
+                self.__min = self._toGUI(min)
+            except:
+##                dbg('exception occurred', indent=0)
+                return False
+        else:
+            self.__min = min
+
+        if self.IsLimited() and not self.IsInBounds():
+            self.SetLimited(self.__limited) # force limited value:
+        else:
+            self._CheckValid()
+        ret = True
+##        dbg('ret:', ret, indent=0)
+        return ret
+
+
+    def GetMin(self, as_string = False):
+        """
+        Gets the minimum value of the control.
+        If None, it will return None.  Otherwise it will return
+        the current minimum bound on the control, as a wxDateTime
+        by default, or as a string if as_string argument is True.
+        """
+##        dbg(suspend=1)
+##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
+        if self.__min is None:
+##            dbg('(min == None)')
+            ret = self.__min
+        elif as_string:
+            ret = self.__min
+##            dbg('ret:', ret)
+        else:
+            try:
+                ret = self.GetWxDateTime(self.__min)
+            except:
+##                dbg(suspend=0)
+##                dbg('exception occurred', indent=0)
+                raise
+##            dbg('ret:', repr(ret))
+##        dbg(indent=0, suspend=0)
+        return ret
+
+
+    def SetMax(self, max=None):
+        """
+        Sets the maximum value of the control. If a value of None
+        is provided, then the control will have no explicit maximum value.
+        If the value specified is less than the current minimum value, then
+        the function returns False and the maximum will not change from its
+        current setting. On success, the function returns True.
+
+        If successful and the current value is greater than the new upper
+        bound, if the control is limited the value will be automatically
+        adjusted to this maximum value; if not limited, the value in the
+        control will be colored as invalid.
+        """
+##        dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1)
+        if max is not None:
+            try:
+                max = self.GetWxDateTime(max)
+                self.__max = self._toGUI(max)
+            except:
+##                dbg('exception occurred', indent=0)
+                return False
+        else:
+            self.__max = max
+##        dbg('max:', repr(self.__max))
+        if self.IsLimited() and not self.IsInBounds():
+            self.SetLimited(self.__limited) # force limited value:
+        else:
+            self._CheckValid()
+        ret = True
+##        dbg('ret:', ret, indent=0)
+        return ret
+
+
+    def GetMax(self, as_string = False):
+        """
+        Gets the minimum value of the control.
+        If None, it will return None.  Otherwise it will return
+        the current minimum bound on the control, as a wxDateTime
+        by default, or as a string if as_string argument is True.
+        """
+##        dbg(suspend=1)
+##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
+        if self.__max is None:
+##            dbg('(max == None)')
+            ret = self.__max
+        elif as_string:
+            ret = self.__max
+##            dbg('ret:', ret)
+        else:
+            try:
+                ret = self.GetWxDateTime(self.__max)
+            except:
+##                dbg(suspend=0)
+##                dbg('exception occurred', indent=0)
+                raise
+##            dbg('ret:', repr(ret))
+##        dbg(indent=0, suspend=0)
+        return ret
+
+
+    def SetBounds(self, min=None, max=None):
+        """
+        This function is a convenience function for setting the min and max
+        values at the same time.  The function only applies the maximum bound
+        if setting the minimum bound is successful, and returns True
+        only if both operations succeed.
+        NOTE: leaving out an argument will remove the corresponding bound.
+        """
+        ret = self.SetMin(min)
+        return ret and self.SetMax(max)
+
+
+    def GetBounds(self, as_string = False):
+        """
+        This function returns a two-tuple (min,max), indicating the
+        current bounds of the control.  Each value can be None if
+        that bound is not set.
+        """
+        return (self.GetMin(as_string), self.GetMax(as_string))
+
+
+    def SetLimited(self, limited):
+        """
+        If called with a value of True, this function will cause the control
+        to limit the value to fall within the bounds currently specified.
+        If the control's value currently exceeds the bounds, it will then
+        be limited accordingly.
+
+        If called with a value of 0, this function will disable value
+        limiting, but coloring of out-of-bounds values will still take
+        place if bounds have been set for the control.
+        """
+##        dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1)
+        self.__limited = limited
+
+        if not limited:
+            self.SetMaskParameters(validRequired = False)
+            self._CheckValid()
+##            dbg(indent=0)
+            return
+
+##        dbg('requiring valid value')
+        self.SetMaskParameters(validRequired = True)
+
+        min = self.GetMin()
+        max = self.GetMax()
+        if min is None or max is None:
+##            dbg('both bounds not set; no further action taken')
+            return  # can't limit without 2 bounds
+
+        elif not self.IsInBounds():
+            # set value to the nearest bound:
+            try:
+                value = self.GetWxDateTime()
+            except:
+##                dbg('exception occurred', indent=0)
+                raise
+
+            if min <= max:   # valid range doesn't span midnight
+##                dbg('min <= max')
+                # which makes the "nearest bound" computation trickier...
+
+                # determine how long the "invalid" pie wedge is, and cut
+                # this interval in half for comparison purposes:
+
+                # Note: relies on min and max and value date portions
+                # always being the same.
+                interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max
+
+                half_interval = wx.TimeSpan(
+                                    0,      # hours
+                                    0,      # minutes
+                                    interval.GetSeconds() / 2,  # seconds
+                                    0)      # msec
+
+                if value < min: # min is on next day, so use value on
+                    # "next day" for "nearest" interval calculation:
+                    cmp_value = value + wx.TimeSpan(24, 0, 0, 0)
+                else:   # "before midnight; ok
+                    cmp_value = value
+
+                if (cmp_value - max) > half_interval:
+##                    dbg('forcing value to min (%s)' % min.FormatTime())
+                    self.SetValue(min)
+                else:
+##                    dbg('forcing value to max (%s)' % max.FormatTime())
+                    self.SetValue(max)
+            else:
+##                dbg('max < min')
+                # therefore  max < value < min guaranteed to be true,
+                # so "nearest bound" calculation is much easier:
+                if (value - max) >= (min - value):
+                    # current value closer to min; pick that edge of pie wedge
+##                    dbg('forcing value to min (%s)' % min.FormatTime())
+                    self.SetValue(min)
+                else:
+##                    dbg('forcing value to max (%s)' % max.FormatTime())
+                    self.SetValue(max)
+
+##        dbg(indent=0)
+
+
+
+    def IsLimited(self):
+        """
+        Returns True if the control is currently limiting the
+        value to fall within any current bounds.  Note: can
+        be set even if there are no current bounds.
+        """
+        return self.__limited
+
+
+    def IsInBounds(self, value=None):
+        """
+        Returns True if no value is specified and the current value
+        of the control falls within the current bounds.  As the clock
+        is a "circle", both minimum and maximum bounds must be set for
+        a value to ever be considered "out of bounds".  This function can
+        also be called with a value to see if that value would fall within
+        the current bounds of the given control.
+        """
+        if value is not None:
+            try:
+                value = self.GetWxDateTime(value)   # try to regularize passed value
+            except ValueError:
+##                dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0)
+                raise
+
+##        dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1)
+        if self.__min is None or self.__max is None:
+##            dbg(indent=0)
+            return True
+
+        elif value is None:
+            try:
+                value = self.GetWxDateTime()
+            except:
+##                dbg('exception occurred', indent=0)
+                raise
+
+##        dbg('value:', value.FormatTime())
+
+        # Get wxDateTime representations of bounds:
+        min = self.GetMin()
+        max = self.GetMax()
+
+        midnight = wx.DateTimeFromDMY(1, 0, 1970)
+        if min <= max:   # they don't span midnight
+            ret = min <= value <= max
+
+        else:
+            # have to break into 2 tests; to be in bounds
+            # either "min" <= value (<= midnight of *next day*)
+            # or midnight <= value <= "max"
+            ret = min <= value or (midnight <= value <= max)
+##        dbg('in bounds?', ret, indent=0)
+        return ret
+
+
+    def IsValid( self, value ):
+        """
+        Can be used to determine if a given value would be a legal and
+        in-bounds value for the control.
+        """
+        try:
+            self.__validateValue(value)
+            return True
+        except ValueError:
+            return False
+
+    def SetFormat(self, format):
+        self.SetParameters(format=format)
+
+    def GetFormat(self):
+        if self.__displaySeconds:
+            if self.__fmt24hr: return '24HHMMSS'
+            else:              return 'HHMMSS'
+        else:
+            if self.__fmt24hr: return '24HHMM'
+            else:              return 'HHMM'
+
+#-------------------------------------------------------------------------------------------------------------
+# these are private functions and overrides:
+
+
+    def __OnTextChange(self, event=None):
+##        dbg('TimeCtrl::OnTextChange', indent=1)
+
+        # Allow Maskedtext base control to color as appropriate,
+        # and Skip the EVT_TEXT event (if appropriate.)
+        ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
+        ## call is generating two (2) EVT_TEXT events. (!)
+        ## The the only mechanism I can find to mask this problem is to
+        ## keep track of last value seen, and declare a valid EVT_TEXT
+        ## event iff the value has actually changed.  The masked edit
+        ## OnTextChange routine does this, and returns True on a valid event,
+        ## False otherwise.
+        if not BaseMaskedTextCtrl._OnTextChange(self, event):
+            return
+
+##        dbg('firing TimeUpdatedEvent...')
+        evt = TimeUpdatedEvent(self.GetId(), self.GetValue())
+        evt.SetEventObject(self)
+        self.GetEventHandler().ProcessEvent(evt)
+##        dbg(indent=0)
+
+
+    def SetInsertionPoint(self, pos):
+        """
+        Records the specified position and associated cell before calling base class' function.
+        This is necessary to handle the optional spin button, because the insertion
+        point is lost when the focus shifts to the spin button.
+        """
+##        dbg('TimeCtrl::SetInsertionPoint', pos, indent=1)
+        BaseMaskedTextCtrl.SetInsertionPoint(self, pos)                 # (causes EVT_TEXT event to fire)
+        self.__posCurrent = self.GetInsertionPoint()
+##        dbg(indent=0)
+
+
+    def SetSelection(self, sel_start, sel_to):
+##        dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1)
+
+        # Adjust selection range to legal extent if not already
+        if sel_start < 0:
+            sel_start = 0
+
+        if self.__posCurrent != sel_start:                      # force selection and insertion point to match
+            self.SetInsertionPoint(sel_start)
+        cell_start, cell_end = self._FindField(sel_start)._extent
+        if not cell_start <= sel_to <= cell_end:
+            sel_to = cell_end
+
+        self.__bSelection = sel_start != sel_to
+        BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
+##        dbg(indent=0)
+
+
+    def __OnSpin(self, key):
+        """
+        This is the function that gets called in response to up/down arrow or
+        bound spin button events.
+        """
+        self.__IncrementValue(key, self.__posCurrent)   # changes the value
+
+        # Ensure adjusted control regains focus and has adjusted portion
+        # selected:
+        self.SetFocus()
+        start, end = self._FindField(self.__posCurrent)._extent
+        self.SetInsertionPoint(start)
+        self.SetSelection(start, end)
+##        dbg('current position:', self.__posCurrent)
+
+
+    def __OnSpinUp(self, event):
+        """
+        Event handler for any bound spin button on EVT_SPIN_UP;
+        causes control to behave as if up arrow was pressed.
+        """
+##        dbg('TimeCtrl::OnSpinUp', indent=1)
+        self.__OnSpin(wx.WXK_UP)
+        keep_processing = False
+##        dbg(indent=0)
+        return keep_processing
+
+
+    def __OnSpinDown(self, event):
+        """
+        Event handler for any bound spin button on EVT_SPIN_DOWN;
+        causes control to behave as if down arrow was pressed.
+        """
+##        dbg('TimeCtrl::OnSpinDown', indent=1)
+        self.__OnSpin(wx.WXK_DOWN)
+        keep_processing = False
+##        dbg(indent=0)
+        return keep_processing
+
+
+    def __OnChar(self, event):
+        """
+        Handler to explicitly look for ':' keyevents, and if found,
+        clear the m_shiftDown field, so it will behave as forward tab.
+        It then calls the base control's _OnChar routine with the modified
+        event instance.
+        """
+##        dbg('TimeCtrl::OnChar', indent=1)
+        keycode = event.GetKeyCode()
+##        dbg('keycode:', keycode)
+        if keycode == ord(':'):
+##            dbg('colon seen! removing shift attribute')
+            event.m_shiftDown = False
+        BaseMaskedTextCtrl._OnChar(self, event )              ## handle each keypress
+##        dbg(indent=0)
+
+
+    def __OnSetToNow(self, event):
+        """
+        This is the key handler for '!' and 'c'; this allows the user to
+        quickly set the value of the control to the current time.
+        """
+        self.SetValue(wx.DateTime_Now().FormatTime())
+        keep_processing = False
+        return keep_processing
+
+
+    def __LimitSelection(self, event):
+        """
+        Event handler for motion events; this handler
+        changes limits the selection to the new cell boundaries.
+        """
+##        dbg('TimeCtrl::LimitSelection', indent=1)
+        pos = self.GetInsertionPoint()
+        self.__posCurrent = pos
+        sel_start, sel_to = self.GetSelection()
+        selection = sel_start != sel_to
+        if selection:
+            # only allow selection to end of current cell:
+            start, end = self._FindField(sel_start)._extent
+            if sel_to < pos:   sel_to = start
+            elif sel_to > pos: sel_to = end
+
+##        dbg('new pos =', self.__posCurrent, 'select to ', sel_to)
+        self.SetInsertionPoint(self.__posCurrent)
+        self.SetSelection(self.__posCurrent, sel_to)
+        if event: event.Skip()
+##        dbg(indent=0)
+
+
+    def __IncrementValue(self, key, pos):
+##        dbg('TimeCtrl::IncrementValue', key, pos, indent=1)
+        text = self.GetValue()
+        field = self._FindField(pos)
+##        dbg('field: ', field._index)
+        start, end = field._extent
+        slice = text[start:end]
+        if key == wx.WXK_UP: increment = 1
+        else:             increment = -1
+
+        if slice in ('A', 'P'):
+            if slice == 'A': newslice = 'P'
+            elif slice == 'P': newslice = 'A'
+            newvalue = text[:start] + newslice + text[end:]
+
+        elif field._index == 0:
+            # adjusting this field is trickier, as its value can affect the
+            # am/pm setting.  So, we use wxDateTime to generate a new value for us:
+            # (Use a fixed date not subject to DST variations:)
+            converter = wx.DateTimeFromDMY(1, 0, 1970)
+##            dbg('text: "%s"' % text)
+            converter.ParseTime(text.strip())
+            currenthour = converter.GetHour()
+##            dbg('current hour:', currenthour)
+            newhour = (currenthour + increment) % 24
+##            dbg('newhour:', newhour)
+            converter.SetHour(newhour)
+##            dbg('converter.GetHour():', converter.GetHour())
+            newvalue = converter     # take advantage of auto-conversion for am/pm in .SetValue()
+
+        else:   # minute or second field; handled the same way:
+            newslice = "%02d" % ((int(slice) + increment) % 60)
+            newvalue = text[:start] + newslice + text[end:]
+
+        try:
+            self.SetValue(newvalue)
+
+        except ValueError:  # must not be in bounds:
+            if not wx.Validator_IsSilent():
+                wx.Bell()
+##        dbg(indent=0)
+
+
+    def _toGUI( self, wxdt ):
+        """
+        This function takes a wxdt as an unambiguous representation of a time, and
+        converts it to a string appropriate for the format of the control.
+        """
+        if self.__fmt24hr:
+            if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S')
+            else:                     strval = wxdt.Format('%H:%M')
+        else:
+            if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p')
+            else:                     strval = wxdt.Format('%I:%M %p')
+
+        return strval
+
+
+    def __validateValue( self, value ):
+        """
+        This function converts the value to a wxDateTime if not already one,
+        does bounds checking and raises ValueError if argument is
+        not a valid value for the control as currently specified.
+        It is used by both the SetValue() and the IsValid() methods.
+        """
+##        dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1)
+        if not value:
+##            dbg(indent=0)
+            raise ValueError('%s not a valid time value' % repr(value))
+
+        valid = True    # assume true
+        try:
+            value = self.GetWxDateTime(value)   # regularize form; can generate ValueError if problem doing so
+        except:
+##            dbg('exception occurred', indent=0)
+            raise
+
+        if self.IsLimited() and not self.IsInBounds(value):
+##            dbg(indent=0)
+            raise ValueError (
+                'value %s is not within the bounds of the control' % str(value) )
+##        dbg(indent=0)
+        return value
+
+#----------------------------------------------------------------------------
+# Test jig for TimeCtrl:
+
+if __name__ == '__main__':
+    import traceback
+
+    class TestPanel(wx.Panel):
+        def __init__(self, parent, id,
+                     pos = wx.DefaultPosition, size = wx.DefaultSize,
+                     fmt24hr = 0, test_mx = 0,
+                     style = wx.TAB_TRAVERSAL ):
+
+            wx.Panel.__init__(self, parent, id, pos, size, style)
+
+            self.test_mx = test_mx
+
+            self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr)
+            sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 )
+            self.tc.BindSpinButton(sb)
+
+            sizer = wx.BoxSizer( wx.HORIZONTAL )
+            sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 )
+            sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 )
+
+            self.SetAutoLayout( True )
+            self.SetSizer( sizer )
+            sizer.Fit( self )
+            sizer.SetSizeHints( self )
+
+            self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc)
+
+        def OnTimeChange(self, event):
+##            dbg('OnTimeChange: value = ', event.GetValue())
+            wxdt = self.tc.GetWxDateTime()
+##            dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
+            if self.test_mx:
+                mxdt = self.tc.GetMxDateTime()
+##                dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
+
+
+    class MyApp(wx.App):
+        def OnInit(self):
+            import sys
+            fmt24hr = '24' in sys.argv
+            test_mx = 'mx' in sys.argv
+            try:
+                frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
+                panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
+                frame.Show(True)
+            except:
+                traceback.print_exc()
+                return False
+            return True
+
+    try:
+        app = MyApp(0)
+        app.MainLoop()
+    except:
+        traceback.print_exc()
+i=0
+## Version 1.2
+##   1. Changed parameter name display_seconds to displaySeconds, to follow
+##      other masked edit conventions.
+##   2. Added format parameter, to remove need to use both fmt24hr and displaySeconds.
+##   3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of
+##      nonsensical parameter methods from the control, so it will work
+##      properly with Boa.
diff --git a/wxPython/wx/lib/maskedctrl.py b/wxPython/wx/lib/maskedctrl.py
deleted file mode 100644 (file)
index d553777..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-#----------------------------------------------------------------------------
-# Name:         wxPython.lib.maskedctrl.py
-# Author:       Will Sadkin
-# Created:      09/24/2003
-# Copyright:   (c) 2003 by Will Sadkin
-# RCS-ID:      $Id$
-# License:     wxWindows license
-#----------------------------------------------------------------------------
-# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Updated for wx namespace (minor)
-# 
-# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Removed wx prefix
-# 
-
-"""<html><body>
-<P>
-<B>MaskedCtrl</B> is actually a factory function for several types of
-masked edit controls:
-<P>
-<UL>
-    <LI><b>MaskedTextCtrl</b> - standard masked edit text box</LI>
-    <LI><b>MaskedComboBox</b> - adds combobox capabilities</LI>
-    <LI><b>IpAddrCtrl</b> - adds logical input semantics for IP address entry</LI>
-    <LI><b>TimeCtrl</b> - special subclass handling lots of time formats as values</LI>
-    <LI><b>MaskedNumCtrl</b> - special subclass handling numeric values</LI>
-</UL>
-<P>
-<B>MaskedCtrl</B> works by looking for a special <b><i>controlType</i></b>
-parameter in the variable arguments of the control, to determine
-what kind of instance to return.
-controlType can be one of:
-<PRE><FONT SIZE=-1>
-    controlTypes.MASKEDTEXT
-    controlTypes.MASKEDCOMBO
-    controlTypes.IPADDR
-    controlTypes.TIME
-    controlTypes.NUMBER
-</FONT></PRE>
-These constants are also available individually, ie, you can
-use either of the following:
-<PRE><FONT SIZE=-1>
-    from wxPython.wx.lib.maskedctrl import MaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER
-    from wxPython.wx.lib.maskedctrl import MaskedCtrl, controlTypes
-</FONT></PRE>
-If not specified as a keyword argument, the default controlType is
-controlTypes.MASKEDTEXT.
-<P>
-Each of the above classes has its own unique arguments, but MaskedCtrl
-provides a single "unified" interface for masked controls.  MaskedTextCtrl,
-MaskedComboBox and IpAddrCtrl are all documented below; the others have
-their own demo pages and interface descriptions.
-</body></html>
-"""
-
-from wx.lib.maskededit      import MaskedTextCtrl, MaskedComboBox, IpAddrCtrl
-from wx.lib.maskednumctrl   import MaskedNumCtrl
-from wx.lib.timectrl        import TimeCtrl
-
-
-# "type" enumeration for class instance factory function
-MASKEDTEXT  = 0
-MASKEDCOMBO = 1
-IPADDR      = 2
-TIME        = 3
-NUMBER      = 4
-
-# for ease of import
-class controlTypes:
-    MASKEDTEXT  = MASKEDTEXT
-    MASKEDCOMBO = MASKEDCOMBO
-    IPADDR      = IPADDR
-    TIME        = TIME
-    NUMBER      = NUMBER
-
-
-def MaskedCtrl( *args, **kwargs):
-    """
-    Actually a factory function providing a unifying
-    interface for generating masked controls.
-    """
-    if not kwargs.has_key('controlType'):
-        controlType = MASKEDTEXT
-    else:
-        controlType = kwargs['controlType']
-        del kwargs['controlType']
-
-    if controlType == MASKEDTEXT:
-        return MaskedTextCtrl(*args, **kwargs)
-
-    elif controlType == MASKEDCOMBO:
-        return MaskedComboBox(*args, **kwargs)
-
-    elif controlType == IPADDR:
-        return IpAddrCtrl(*args, **kwargs)
-
-    elif controlType == TIME:
-        return TimeCtrl(*args, **kwargs)
-
-    elif controlType == NUMBER:
-        return MaskedNumCtrl(*args, **kwargs)
-
-    else:
-        raise AttributeError(
-            "invalid controlType specified: %s" % repr(controlType))
-
-
diff --git a/wxPython/wx/lib/maskededit.py b/wxPython/wx/lib/maskededit.py
deleted file mode 100644 (file)
index bdc4635..0000000
+++ /dev/null
@@ -1,7608 +0,0 @@
-#----------------------------------------------------------------------------
-# Name:         maskededit.py
-# Authors:      Jeff Childers, Will Sadkin
-# Email:        jchilders_98@yahoo.com, wsadkin@nameconnector.com
-# Created:      02/11/2003
-# Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
-# Portions:     (c) 2002 by Will Sadkin, 2002-2003
-# RCS-ID:       $Id$
-# License:      wxWidgets license
-#----------------------------------------------------------------------------
-# NOTE:
-#   MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
-#   Jason Hihn, and borrows liberally from Will Sadkin's original masked edit
-#   control for time entry, TimeCtrl (which is now rewritten using this
-#   control!).
-#
-#   MaskedEdit controls do not normally use validators, because they do
-#   careful manipulation of the cursor in the text window on each keystroke,
-#   and validation is cursor-position specific, so the control intercepts the
-#   key codes before the validator would fire.  However, validators can be
-#   provided to do data transfer to the controls.
-#
-#----------------------------------------------------------------------------
-#
-# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Updated for wx namespace. No guarantees. This is one huge file.
-# 
-# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Missed wx.DateTime stuff earlier.
-# 
-# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o wxMaskedEditMixin -> MaskedEditMixin
-# o wxMaskedTextCtrl -> MaskedTextCtrl
-# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent
-# o wxMaskedComboBox -> MaskedComboBox
-# o wxIpAddrCtrl -> IpAddrCtrl
-# o wxTimeCtrl -> TimeCtrl
-#
-
-"""\
-<b>Masked Edit Overview:
-=====================</b>
-<b>MaskedTextCtrl</b>
-    is a sublassed text control that can carefully control the user's input
-    based on a mask string you provide.
-
-    General usage example:
-        control = MaskedTextCtrl( win, -1, '', mask = '(###) ###-####')
-
-    The example above will create a text control that allows only numbers to be
-    entered and then only in the positions indicated in the mask by the # sign.
-
-<b>MaskedComboBox</b>
-    is a similar subclass of wxComboBox that allows the same sort of masking,
-    but also can do auto-complete of values, and can require the value typed
-    to be in the list of choices to be colored appropriately.
-
-<b>wxMaskedCtrl</b>
-    is actually a factory function for several types of masked edit controls:
-
-    <b>MaskedTextCtrl</b>   - standard masked edit text box
-    <b>MaskedComboBox</b>   - adds combobox capabilities
-    <b>IpAddrCtrl</b>       - adds special semantics for IP address entry
-    <b>TimeCtrl</b>         - special subclass handling lots of types as values
-    <b>wxMaskedNumCtrl</b>    - special subclass handling numeric values
-
-    It works by looking for a <b><i>controlType</i></b> parameter in the keyword
-    arguments of the control, to determine what kind of instance to return.
-    If not specified as a keyword argument, the default control type returned
-    will be MaskedTextCtrl.
-
-    Each of the above classes has its own set of arguments, but wxMaskedCtrl
-    provides a single "unified" interface for masked controls.  Those for
-    MaskedTextCtrl, MaskedComboBox and 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 wxMaskedCtrl()
-    to select the above control types.)
-
-
-<b>INITILIZATION PARAMETERS
-========================
-mask=</b>
-Allowed mask characters and function:
-    Character   Function
-            #       Allow numeric only (0-9)
-            N       Allow letters and numbers (0-9)
-            A       Allow uppercase letters only
-            a       Allow lowercase letters only
-            C       Allow any letter, upper or lower
-            X       Allow string.letters, string.punctuation, string.digits
-            &amp;       Allow string.punctuation only
-
-
-    These controls define these sets of characters using string.letters,
-    string.uppercase, etc.  These sets are affected by the system locale
-    setting, so in order to have the masked controls accept characters
-    that are specific to your users' language, your application should
-    set the locale.
-    For example, to allow international characters to be used in the
-    above masks, you can place the following in your code as part of
-    your application's initialization code:
-
-      import locale
-      locale.setlocale(locale.LC_ALL, '')
-
-
-  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
-  number, etc.  If any of these characters are needed as template rather
-  than mask characters, they can be escaped with \, ie. \N means "literal N".
-  (use \\ for literal backslash, as in: r'CCC\\NNN'.)
-
-
-  <b>Note:</b>
-      Masks containing only # characters and one optional decimal point
-      character are handled specially, as "numeric" controls.  Such
-      controls have special handling for typing the '-' key, handling
-      the "decimal point" character as truncating the integer portion,
-      optionally allowing grouping characters and so forth.
-      There are several parameters and format codes that only make sense
-      when combined with such masks, eg. groupChar, decimalChar, and so
-      forth (see below).  These allow you to construct reasonable
-      numeric entry controls.
-
-  <b>Note:</b>
-      Changing the mask for a control deletes any previous field classes
-      (and any associated validation or formatting constraints) for them.
-
-<b>useFixedWidthFont=</b>
-  By default, masked edit controls use a fixed width font, so that
-  the mask characters are fixed within the control, regardless of
-  subsequent modifications to the value.  Set to False if having
-  the control font be the same as other controls is required.
-
-
-<b>formatcodes=</b>
-  These other properties can be passed to the class when instantiating it:
-    Formatcodes are specified as a string of single character formatting
-    codes that modify  behavior of the control:
-            _  Allow spaces
-            !  Force upper
-            ^  Force lower
-            R  Right-align field(s)
-            r  Right-insert in field(s) (implies R)
-            &lt;  Stay in field until explicit navigation out of it
-
-            &gt;  Allow insert/delete within partially filled fields (as
-               opposed to the default "overwrite" mode for fixed-width
-               masked edit controls.)  This allows single-field controls
-               or each field within a multi-field control to optionally
-               behave more like standard text controls.
-               (See EMAIL or phone number autoformat examples.)
-
-               <i>Note: This also governs whether backspace/delete operations
-               shift contents of field to right of cursor, or just blank the
-               erased section.
-
-               Also, when combined with 'r', this indicates that the field
-               or control allows right insert anywhere within the current
-               non-empty value in the field.  (Otherwise right-insert behavior
-               is only performed to when the entire right-insertable field is
-               selected or the cursor is at the right edge of the field.</i>
-
-
-            ,  Allow grouping character in integer fields of numeric controls
-               and auto-group/regroup digits (if the result fits) when leaving
-               such a field.  (If specified, .SetValue() will attempt to
-               auto-group as well.)
-               ',' is also the default grouping character.  To change the
-               grouping character and/or decimal character, use the groupChar
-               and decimalChar parameters, respectively.
-               Note: typing the "decimal point" character in such fields will
-               clip the value to that left of the cursor for integer
-               fields of controls with "integer" or "floating point" masks.
-               If the ',' format code is specified, this will also cause the
-               resulting digits to be regrouped properly, using the current
-               grouping character.
-            -  Prepend and reserve leading space for sign to mask and allow
-               signed values (negative #s shown in red by default.) Can be
-               used with argument useParensForNegatives (see below.)
-            0  integer fields get leading zeros
-            D  Date[/time] field
-            T  Time field
-            F  Auto-Fit: the control calulates its size from
-               the length of the template mask
-            V  validate entered chars against validRegex before allowing them
-               to be entered vs. being allowed by basic mask and then having
-               the resulting value just colored as invalid.
-               (See USSTATE autoformat demo for how this can be used.)
-            S  select entire field when navigating to new field
-
-<b>fillChar=
-defaultValue=</b>
-  These controls have two options for the initial state of the control.
-  If a blank control with just the non-editable characters showing
-  is desired, simply leave the constructor variable fillChar as its
-  default (' ').  If you want some other character there, simply
-  change the fillChar to that value.  Note: changing the control's fillChar
-  will implicitly reset all of the fields' fillChars to this value.
-
-  If you need different default characters in each mask position,
-  you can specify a defaultValue parameter in the constructor, or
-  set them for each field individually.
-  This value must satisfy the non-editable characters of the mask,
-  but need not conform to the replaceable characters.
-
-<b>groupChar=
-decimalChar=</b>
-  These parameters govern what character is used to group numbers
-  and is used to indicate the decimal point for numeric format controls.
-  The default groupChar is ',', the default decimalChar is '.'
-  By changing these, you can customize the presentation of numbers
-  for your location.
-  eg: formatcodes = ',', groupChar="'"                   allows  12'345.34
-      formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34
-
-<b>shiftDecimalChar=</b>
-  The default "shiftDecimalChar" (used for "backwards-tabbing" until
-  shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for
-  other keyboards, you may want to customize this, eg '?' for shift ',' on
-  AZERTY keyboards, ':' or ';' for other European keyboards, etc.
-
-<b>useParensForNegatives=False</b>
-  This option can be used with signed numeric format controls to
-  indicate signs via () rather than '-'.
-
-<b>autoSelect=False</b>
-  This option can be used to have a field or the control try to
-  auto-complete on each keystroke if choices have been specified.
-
-<b>autoCompleteKeycodes=[]</b>
-  By default, DownArrow, PageUp and PageDown will auto-complete a
-  partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp
-  and PageDown will also auto-complete, but if the field already
-  contains a matched value, these keys will cycle through the list
-  of choices forward or backward as appropriate.  Shift-Up and
-  Shift-Down also take you to the next/previous field after any
-  auto-complete action.
-
-  Additional auto-complete keys can be specified via this parameter.
-  Any keys so specified will act like PageDown.
-
-
-
-<b>Validating User Input:
-======================</b>
-  There are a variety of initialization parameters that are used to validate
-  user input.  These parameters can apply to the control as a whole, and/or
-  to individual fields:
-
-        excludeChars=   A string of characters to exclude even if otherwise allowed
-        includeChars=   A string of characters to allow even if otherwise disallowed
-        validRegex=     Use a regular expression to validate the contents of the text box
-        validRange=     Pass a rangeas list (low,high) to limit numeric fields/values
-        choices=        A list of strings that are allowed choices for the control.
-        choiceRequired= value must be member of choices list
-        compareNoCase=  Perform case-insensitive matching when validating against list
-                        <i>Note: for MaskedComboBox, this defaults to True.</i>
-        emptyInvalid=   Boolean indicating whether an empty value should be considered invalid
-
-        validFunc=      A function to call of the form: bool = func(candidate_value)
-                        which will return True if the candidate_value satisfies some
-                        external criteria for the control in addition to the the
-                        other validation, or False if not.  (This validation is
-                        applied last in the chain of validations.)
-
-        validRequired=  Boolean indicating whether or not keys that are allowed by the
-                        mask, but result in an invalid value are allowed to be entered
-                        into the control.  Setting this to True implies that a valid
-                        default value is set for the control.
-
-        retainFieldValidation=
-                        False by default; if True, this allows individual fields to
-                        retain their own validation constraints independently of any
-                        subsequent changes to the control's overall parameters.
-
-        validator=      Validators are not normally needed for masked controls, because
-                        of the nature of the validation and control of input.  However,
-                        you can supply one to provide data transfer routines for the
-                        controls.
-
-
-<b>Coloring Behavior:
-==================</b>
-  The following parameters have been provided to allow you to change the default
-  coloring behavior of the control.   These can be set at construction, or via
-  the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow':
-
-        emptyBackgroundColour=      Control Background color when identified as empty. Default=White
-        invalidBackgroundColour=    Control Background color when identified as Not valid. Default=Yellow
-        validBackgroundColour=      Control Background color when identified as Valid. Default=white
-
-
-  The following parameters control the default foreground color coloring behavior of the
-  control. Pass a color as string e.g. 'Yellow':
-        foregroundColour=           Control foreground color when value is not negative.  Default=Black
-        signedForegroundColour=     Control foreground color when value is negative. Default=Red
-
-
-<b>Fields:
-=======</b>
-  Each part of the mask that allows user input is considered a field.  The fields
-  are represented by their own class instances.  You can specify field-specific
-  constraints by constructing or accessing the field instances for the control
-  and then specifying those constraints via parameters.
-
-<b>fields=</b>
-  This parameter allows you to specify Field instances containing
-  constraints for the individual fields of a control, eg: local
-  choice lists, validation rules, functions, regexps, etc.
-  It can be either an ordered list or a dictionary.  If a list,
-  the fields will be applied as fields 0, 1, 2, etc.
-  If a dictionary, it should be keyed by field index.
-  the values should be a instances of maskededit.Field.
-
-  Any field not represented by the list or dictionary will be
-  implicitly created by the control.
-
-  eg:
-    fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ]
-  or
-    fields = {
-              1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']),
-              3: ( Field(choices=['01', '02', '03'], choiceRequired=True)
-             }
-
-  The following parameters are available for individual fields, with the
-  same semantics as for the whole control but applied to the field in question:
-
-    fillChar        # if set for a field, it will override the control's fillChar for that field
-    groupChar       # if set for a field, it will override the control's default
-    defaultValue    # sets field-specific default value; overrides any default from control
-    compareNoCase   # overrides control's settings
-    emptyInvalid    # determines whether field is required to be filled at all times
-    validRequired   # if set, requires field to contain valid value
-
-  If any of the above parameters are subsequently specified for the control as a
-  whole, that new value will be propagated to each field, unless the
-  retainFieldValidation control-level parameter is set.
-
-    formatcodes     # Augments control's settings
-    excludeChars    #     '       '        '
-    includeChars    #     '       '        '
-    validRegex      #     '       '        '
-    validRange      #     '       '        '
-    choices         #     '       '        '
-    choiceRequired  #     '       '        '
-    validFunc       #     '       '        '
-
-
-
-<b>Control Class Functions:
-========================
-  .GetPlainValue(value=None)</b>
-                    Returns the value specified (or the control's text value
-                    not specified) without the formatting text.
-                    In the example above, might return phone no='3522640075',
-                    whereas control.GetValue() would return '(352) 264-0075'
-  <b>.ClearValue()</b>
-                    Returns the control's value to its default, and places the
-                    cursor at the beginning of the control.
-  <b>.SetValue()</b>
-                    Does "smart replacement" of passed value into the control, as does
-                    the .Paste() method.  As with other text entry controls, the
-                    .SetValue() text replacement begins at left-edge of the control,
-                    with missing mask characters inserted as appropriate.
-                    .SetValue will also adjust integer, float or date mask entry values,
-                    adding commas, auto-completing years, etc. as appropriate.
-                    For "right-aligned" numeric controls, it will also now automatically
-                    right-adjust any value whose length is less than the width of the
-                    control before attempting to set the value.
-                    If a value does not follow the format of the control's mask, or will
-                    not fit into the control, a ValueError exception will be raised.
-                    Eg:
-                      mask = '(###) ###-####'
-                          .SetValue('1234567890')           => '(123) 456-7890'
-                          .SetValue('(123)4567890')         => '(123) 456-7890'
-                          .SetValue('(123)456-7890')        => '(123) 456-7890'
-                          .SetValue('123/4567-890')         => illegal paste; ValueError
-
-                      mask = '#{6}.#{2}', formatcodes = '_,-',
-                          .SetValue('111')                  => ' 111   .  '
-                          .SetValue(' %9.2f' % -111.12345 ) => '   -111.12'
-                          .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00'
-                          .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError
-
-                      mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control
-                          .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit
-                          .SetValue('%.2f' % 111 )          => '    111.00'
-                          .SetValue('%.2f' % -111.12345 )   => '   -111.12'
-
-
-  <b>.IsValid(value=None)</b>
-                    Returns True if the value specified (or the value of the control
-                    if not specified) passes validation tests
-  <b>.IsEmpty(value=None)</b>
-                    Returns True if the value specified (or the value of the control
-                    if not specified) is equal to an "empty value," ie. all
-                    editable characters == the fillChar for their respective fields.
-  <b>.IsDefault(value=None)</b>
-                    Returns True if the value specified (or the value of the control
-                    if not specified) is equal to the initial value of the control.
-
-  <b>.Refresh()</b>
-                    Recolors the control as appropriate to its current settings.
-
-  <b>.SetCtrlParameters(**kwargs)</b>
-                    This function allows you to set up and/or change the control parameters
-                    after construction; it takes a list of key/value pairs as arguments,
-                    where the keys can be any of the mask-specific parameters in the constructor.
-                    Eg:
-                        ctl = MaskedTextCtrl( self, -1 )
-                        ctl.SetCtrlParameters( mask='###-####',
-                                               defaultValue='555-1212',
-                                               formatcodes='F')
-
-  <b>.GetCtrlParameter(parametername)</b>
-                    This function allows you to retrieve the current value of a parameter
-                    from the control.
-
-  <b><i>Note:</i></b> Each of the control parameters can also be set using its
-      own Set and Get function.  These functions follow a regular form:
-      All of the parameter names start with lower case; for their
-      corresponding Set/Get function, the parameter name is capitalized.
-      Eg: ctl.SetMask('###-####')
-          ctl.SetDefaultValue('555-1212')
-          ctl.GetChoiceRequired()
-          ctl.GetFormatcodes()
-
-  <b><i>Note:</i></b> After any change in parameters, the choices for the
-      control are reevaluated to ensure that they are still legal.  If you
-      have large choice lists, it is therefore more efficient to set parameters
-      before setting the choices available.
-
-  <b>.SetFieldParameters(field_index, **kwargs)</b>
-                    This function allows you to specify change individual field
-                    parameters after construction. (Indices are 0-based.)
-
-  <b>.GetFieldParameter(field_index, parametername)</b>
-                    Allows the retrieval of field parameters after construction
-
-
-The control detects certain common constructions. In order to use the signed feature
-(negative numbers and coloring), the mask has to be all numbers with optionally one
-decimal point. Without a decimal (e.g. '######', the control will treat it as an integer
-value. With a decimal (e.g. '###.##'), the control will act as a floating point control
-(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
-integer control truncates the value.  However, for a true numeric control,
-MaskedNumCtrl provides all this, and true numeric input/output support as well.
-
-
-Check your controls by calling each control's .IsValid() function and the
-.IsEmpty() function to determine which controls have been a) filled in and
-b) filled in properly.
-
-
-Regular expression validations can be used flexibly and creatively.
-Take a look at the demo; the zip-code validation succeeds as long as the
-first five numerals are entered. the last four are optional, but if
-any are entered, there must be 4 to be valid.
-
-<B>wxMaskedCtrl Configuration
-==========================</B>
-wxMaskedCtrl works by looking for a special <b><i>controlType</i></b>
-parameter in the variable arguments of the control, to determine
-what kind of instance to return.
-controlType can be one of:
-
-    controlTypes.MASKEDTEXT
-    controlTypes.MASKEDCOMBO
-    controlTypes.IPADDR
-    controlTypes.TIME
-    controlTypes.NUMBER
-
-These constants are also available individually, ie, you can
-use either of the following:
-
-    from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, controlTypes
-    from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER
-
-If not specified as a keyword argument, the default controlType is
-controlTypes.TEXT.
-"""
-
-"""
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-DEVELOPER COMMENTS:
-
-Naming Conventions
-------------------
-  All methods of the Mixin that are not meant to be exposed to the external
-  interface are prefaced with '_'.  Those functions that are primarily
-  intended to be internal subroutines subsequently start with a lower-case
-  letter; those that are primarily intended to be used and/or overridden
-  by derived subclasses start with a capital letter.
-
-  The following methods must be used and/or defined when deriving a control
-  from wxMaskedEditMixin.  NOTE: if deriving from a *masked edit* control
-  (eg. class IpAddrCtrl(MaskedTextCtrl) ), then this is NOT necessary,
-  as it's already been done for you in the base class.
-
-        ._SetInitialValue()
-                        This function must be called after the associated base
-                        control has been initialized in the subclass __init__
-                        function.  It sets the initial value of the control,
-                        either to the value specified if non-empty, the
-                        default value if specified, or the "template" for
-                        the empty control as necessary.  It will also set/reset
-                        the font if necessary and apply formatting to the
-                        control at this time.
-
-        ._GetSelection()
-                        REQUIRED
-                        Each class derived from wxMaskedEditMixin must define
-                        the function for getting the start and end of the
-                        current text selection.  The reason for this is
-                        that not all controls have the same function name for
-                        doing this; eg. wxTextCtrl uses .GetSelection(),
-                        whereas we had to write a .GetMark() function for
-                        wxComboBox, because .GetSelection() for the control
-                        gets the currently selected list item from the combo
-                        box, and the control doesn't (yet) natively provide
-                        a means of determining the text selection.
-        ._SetSelection()
-                        REQUIRED
-                        Similarly to _GetSelection, each class derived from
-                        wxMaskedEditMixin must define the function for setting
-                        the start and end of the current text selection.
-                        (eg. .SetSelection() for MaskedTextCtrl, and .SetMark() for
-                        MaskedComboBox.
-
-        ._GetInsertionPoint()
-        ._SetInsertionPoint()
-                        REQUIRED
-                        For consistency, and because the mixin shouldn't rely
-                        on fixed names for any manipulations it does of any of
-                        the base controls, we require each class derived from
-                        wxMaskedEditMixin to define these functions as well.
-
-        ._GetValue()
-        ._SetValue()    REQUIRED
-                        Each class derived from wxMaskedEditMixin must define
-                        the functions used to get and set the raw value of the
-                        control.
-                        This is necessary so that recursion doesn't take place
-                        when setting the value, and so that the mixin can
-                        call the appropriate function after doing all its
-                        validation and manipulation without knowing what kind
-                        of base control it was mixed in with.  To handle undo
-                        functionality, the ._SetValue() must record the current
-                        selection prior to setting the value.
-
-        .Cut()
-        .Paste()
-        .Undo()
-        .SetValue()     REQUIRED
-                        Each class derived from wxMaskedEditMixin must redefine
-                        these functions to call the _Cut(), _Paste(), _Undo()
-                        and _SetValue() methods, respectively for the control,
-                        so as to prevent programmatic corruption of the control's
-                        value.  This must be done in each derivation, as the
-                        mixin cannot itself override a member of a sibling class.
-
-        ._Refresh()     REQUIRED
-                        Each class derived from wxMaskedEditMixin must define
-                        the function used to refresh the base control.
-
-        .Refresh()      REQUIRED
-                        Each class derived from wxMaskedEditMixin must redefine
-                        this function so that it checks the validity of the
-                        control (via self._CheckValid) and then refreshes
-                        control using the base class method.
-
-        ._IsEditable()  REQUIRED
-                        Each class derived from wxMaskedEditMixin must define
-                        the function used to determine if the base control is
-                        editable or not.  (For MaskedComboBox, this has to
-                        be done with code, rather than specifying the proper
-                        function in the base control, as there isn't one...)
-        ._CalcSize()    REQUIRED
-                        Each class derived from wxMaskedEditMixin must define
-                        the function used to determine how wide the control
-                        should be given the mask.  (The mixin function
-                        ._calcSize() provides a baseline estimate.)
-
-
-Event Handling
---------------
-  Event handlers are "chained", and wxMaskedEditMixin usually
-  swallows most of the events it sees, thereby preventing any other
-  handlers from firing in the chain.  It is therefore required that
-  each class derivation using the mixin to have an option to hook up
-  the event handlers itself or forego this operation and let a
-  subclass of the masked control do so.  For this reason, each
-  subclass should probably include the following code:
-
-    if setupEventHandling:
-        ## Setup event handlers
-        EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection
-        EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator
-        EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick
-        EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu
-        EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab.
-        EVT_CHAR( self, self._OnChar )              ## handle each keypress
-        EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep
-                                                            ## track of previous value for undo
-
-  where setupEventHandling is an argument to its constructor.
-
-  These 5 handlers must be "wired up" for the wxMaskedEdit
-  control to provide default behavior.  (The setupEventHandling
-  is an argument to MaskedTextCtrl and MaskedComboBox, so
-  that controls derived from *them* may replace one of these
-  handlers if they so choose.)
-
-  If your derived control wants to preprocess events before
-  taking action, it should then set up the event handling itself,
-  so it can be first in the event handler chain.
-
-
-  The following routines are available to facilitate changing
-  the default behavior of wxMaskedEdit controls:
-
-        ._SetKeycodeHandler(keycode, func)
-        ._SetKeyHandler(char, func)
-                        Use to replace default handling for any given keycode.
-                        func should take the key event as argument and return
-                        False if no further action is required to handle the
-                        key. Eg:
-                            self._SetKeycodeHandler(WXK_UP, self.IncrementValue)
-                            self._SetKeyHandler('-', self._OnChangeSign)
-
-        "Navigation" keys are assumed to change the cursor position, and
-        therefore don't cause automatic motion of the cursor as insertable
-        characters do.
-
-        ._AddNavKeycode(keycode, handler=None)
-        ._AddNavKey(char, handler=None)
-                        Allows controls to specify other keys (and optional handlers)
-                        to be treated as navigational characters. (eg. '.' in IpAddrCtrl)
-
-        ._GetNavKeycodes()  Returns the current list of navigational keycodes.
-
-        ._SetNavKeycodes(key_func_tuples)
-                        Allows replacement of the current list of keycode
-                        processed as navigation keys, and bind associated
-                        optional keyhandlers. argument is a list of key/handler
-                        tuples.  Passing a value of None for the handler in a
-                        given tuple indicates that default processing for the key
-                        is desired.
-
-        ._FindField(pos) Returns the Field object associated with this position
-                        in the control.
-
-        ._FindFieldExtent(pos, getslice=False, value=None)
-                        Returns edit_start, edit_end of the field corresponding
-                        to the specified position within the control, and
-                        optionally also returns the current contents of that field.
-                        If value is specified, it will retrieve the slice the corresponding
-                        slice from that value, rather than the current value of the
-                        control.
-
-        ._AdjustField(pos)
-                        This is, the function that gets called for a given position
-                        whenever the cursor is adjusted to leave a given field.
-                        By default, it adjusts the year in date fields if mask is a date,
-                        It can be overridden by a derived class to
-                        adjust the value of the control at that time.
-                        (eg. IpAddrCtrl reformats the address in this way.)
-
-        ._Change()      Called by internal EVT_TEXT handler. Return False to force
-                        skip of the normal class change event.
-        ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force
-                        skip of the normal class keypress event.
-        ._LostFocus()   Called by internal EVT_KILL_FOCUS handler
-
-        ._OnKeyDown(event)
-                        This is the default EVT_KEY_DOWN routine; it just checks for
-                        "navigation keys", and if event.ControlDown(), it fires the
-                        mixin's _OnChar() routine, as such events are not always seen
-                        by the "cooked" EVT_CHAR routine.
-
-        ._OnChar(event) This is the main EVT_CHAR handler for the
-                        wxMaskedEditMixin.
-
-    The following routines are used to handle standard actions
-    for control keys:
-        _OnArrow(event)         used for arrow navigation events
-        _OnCtrl_A(event)        'select all'
-        _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive)
-        _OnCtrl_S(event)        'save' (does nothing)
-        _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste
-        _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection
-        _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any)
-
-        _OnChangeField(event)   primarily used for tab events, but can be
-                                used for other keys (eg. '.' in IpAddrCtrl)
-
-        _OnErase(event)         used for backspace and delete
-        _OnHome(event)
-        _OnEnd(event)
-
-    The following routine provides a hook back to any class derivations, so that
-    they can react to parameter changes before any value is set/reset as a result of
-    those changes.  (eg. MaskedComboBox needs to detect when the choices list is
-    modified, either implicitly or explicitly, so it can reset the base control
-    to have the appropriate choice list *before* the initial value is reset to match.)
-
-        _OnCtrlParametersChanged()
-
-Accessor Functions
-------------------
-    For convenience, each class derived from MaskedEditMixin should
-    define an accessors mixin, so that it exposes only those parameters
-    that make sense for the derivation.  This is done with an intermediate
-    level of inheritance, ie:
-
-    class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ):
-
-    class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
-    class MaskedNumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ):
-    class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
-    class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ):
-
-    etc.
-
-    Each accessors mixin defines Get/Set functions for the base class parameters
-    that are appropriate for that derivation.
-    This allows the base classes to be "more generic," exposing the widest
-    set of options, while not requiring derived classes to be so general.
-"""
-
-import  copy
-import  difflib
-import  re
-import  string
-import  types
-
-import  wx
-
-# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
-# be a good place to implement the 2.3 logger class
-from wx.tools.dbg import Logger 
-
-dbg = Logger()
-##dbg(enable=0)
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## Constants for identifying control keys and classes of keys:
-
-WXK_CTRL_A = (ord('A')+1) - ord('A')   ## These keys are not already defined in wx
-WXK_CTRL_C = (ord('C')+1) - ord('A')
-WXK_CTRL_S = (ord('S')+1) - ord('A')
-WXK_CTRL_V = (ord('V')+1) - ord('A')
-WXK_CTRL_X = (ord('X')+1) - ord('A')
-WXK_CTRL_Z = (ord('Z')+1) - ord('A')
-
-nav = (
-    wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, 
-    wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT
-    )
-
-control = (
-    wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, 
-    WXK_CTRL_X, WXK_CTRL_Z
-    )
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## 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)
-## Note: locale settings affect what "uppercase", lowercase, etc comprise.
-##
-maskchars = ("#","A","a","X","C","N", '&')
-
-months = '(01|02|03|04|05|06|07|08|09|10|11|12)'
-charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
-charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
-                   'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
-
-days   = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)'
-hours  = '(0\d| \d|1[012])'
-milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)'
-minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
-16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\
-36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\
-56|57|58|59)"""
-seconds = minutes
-am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde'
-
-states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',')
-
-state_names = ['Alabama','Alaska','Arizona','Arkansas',
-               'California','Colorado','Connecticut',
-               'Delaware','District of Columbia',
-               'Florida','Georgia','Hawaii',
-               'Idaho','Illinois','Indiana','Iowa',
-               'Kansas','Kentucky','Louisiana',
-               'Maine','Maryland','Massachusetts','Michigan',
-               'Minnesota','Mississippi','Missouri','Montana',
-               'Nebraska','Nevada','New Hampshire','New Jersey',
-               'New Mexico','New York','North Carolina','North Dakokta',
-               'Ohio','Oklahoma','Oregon',
-               'Pennsylvania','Puerto Rico','Rhode Island',
-               'South Carolina','South Dakota',
-               'Tennessee','Texas','Utah',
-               'Vermont','Virginia',
-               'Washington','West Virginia',
-               'Wisconsin','Wyoming']
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## The following dictionary defines the current set of autoformats:
-
-masktags = {
-       "USPHONEFULLEXT": {
-           'mask': "(###) ###-#### x:###",
-           'formatcodes': 'F^->',
-           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
-           'description': "Phone Number w/opt. ext"
-           },
-       "USPHONETIGHTEXT": {
-           'mask': "###-###-#### x:###",
-           'formatcodes': 'F^->',
-           'validRegex': "^\d{3}-\d{3}-\d{4}",
-           'description': "Phone Number\n (w/hyphens and opt. ext)"
-           },
-       "USPHONEFULL": {
-           'mask': "(###) ###-####",
-           'formatcodes': 'F^->',
-           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
-           'description': "Phone Number only"
-           },
-       "USPHONETIGHT": {
-           'mask': "###-###-####",
-           'formatcodes': 'F^->',
-           'validRegex': "^\d{3}-\d{3}-\d{4}",
-           'description': "Phone Number\n(w/hyphens)"
-           },
-       "USSTATE": {
-           'mask': "AA",
-           'formatcodes': 'F!V',
-           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'),
-           'choices': states,
-           'choiceRequired': True,
-           'description': "US State Code"
-           },
-       "USSTATENAME": {
-           'mask': "ACCCCCCCCCCCCCCCCCCC",
-           'formatcodes': 'F_',
-           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'),
-           'choices': state_names,
-           'choiceRequired': True,
-           'description': "US State Name"
-           },
-
-       "USDATETIMEMMDDYYYY/HHMMSS": {
-           'mask': "##/##/#### ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "US Date + Time"
-           },
-       "USDATETIMEMMDDYYYY-HHMMSS": {
-           'mask': "##-##-#### ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "US Date + Time\n(w/hypens)"
-           },
-       "USDATE24HRTIMEMMDDYYYY/HHMMSS": {           
-           'mask': "##/##/#### ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "US Date + 24Hr (Military) Time"           
-           },
-       "USDATE24HRTIMEMMDDYYYY-HHMMSS": {
-           'mask': "##-##-#### ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "US Date + 24Hr Time\n(w/hypens)"           
-           },
-       "USDATETIMEMMDDYYYY/HHMM": {
-           'mask': "##/##/#### ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "US Date + Time\n(without seconds)"
-           },
-       "USDATE24HRTIMEMMDDYYYY/HHMM": {
-           'mask': "##/##/#### ##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,
-           'description': "US Date + 24Hr Time\n(without seconds)"           
-           },
-       "USDATETIMEMMDDYYYY-HHMM": {
-           'mask': "##-##-#### ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "US Date + Time\n(w/hypens and w/o secs)"
-           },
-       "USDATE24HRTIMEMMDDYYYY-HHMM": {
-           'mask': "##-##-#### ##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes,
-           'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)"
-           },
-       "USDATEMMDDYYYY/": {
-           'mask': "##/##/####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '/' + days + '/' + '\d{4}',
-           'description': "US Date\n(MMDDYYYY)"
-           },
-       "USDATEMMDDYY/": {
-           'mask': "##/##/##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '/' + days + '/\d\d',
-           'description': "US Date\n(MMDDYY)"
-           },
-       "USDATEMMDDYYYY-": {
-           'mask': "##-##-####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + months + '-' + days + '-' +'\d{4}',
-           'description': "MM-DD-YYYY"
-           },
-
-       "EUDATEYYYYMMDD/": {
-           'mask': "####/##/##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days,
-           'description': "YYYY/MM/DD"
-           },
-       "EUDATEYYYYMMDD.": {
-           'mask': "####.##.##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days,
-           'description': "YYYY.MM.DD"
-           },
-       "EUDATEDDMMYYYY/": {
-           'mask': "##/##/####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '/' + months + '/' + '\d{4}',
-           'description': "DD/MM/YYYY"
-           },
-       "EUDATEDDMMYYYY.": {
-           'mask': "##.##.####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '.' + months + '.' + '\d{4}',
-           'description': "DD.MM.YYYY"
-           },
-       "EUDATEDDMMMYYYY.": {
-           'mask': "##.CCC.####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}',
-           'description': "DD.Month.YYYY"
-           },
-       "EUDATEDDMMMYYYY/": {
-           'mask': "##/CCC/####",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}',
-           'description': "DD/Month/YYYY"
-           },
-
-       "EUDATETIMEYYYYMMDD/HHMMSS": {
-           'mask': "####/##/## ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "YYYY/MM/DD HH:MM:SS"
-           },
-       "EUDATETIMEYYYYMMDD.HHMMSS": {
-           'mask': "####.##.## ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "YYYY.MM.DD HH:MM:SS"
-           },
-       "EUDATETIMEDDMMYYYY/HHMMSS": {
-           'mask': "##/##/#### ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "DD/MM/YYYY HH:MM:SS"
-           },
-       "EUDATETIMEDDMMYYYY.HHMMSS": {
-           'mask': "##.##.#### ##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "DD.MM.YYYY HH:MM:SS"
-           },
-
-       "EUDATETIMEYYYYMMDD/HHMM": {
-           'mask': "####/##/## ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "YYYY/MM/DD HH:MM"
-           },
-       "EUDATETIMEYYYYMMDD.HHMM": {
-           'mask': "####.##.## ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "YYYY.MM.DD HH:MM"
-           },
-       "EUDATETIMEDDMMYYYY/HHMM": {
-           'mask': "##/##/#### ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "DD/MM/YYYY HH:MM"
-           },
-       "EUDATETIMEDDMMYYYY.HHMM": {
-           'mask': "##.##.#### ##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'DF!',
-           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
-           'description': "DD.MM.YYYY HH:MM"
-           },
-
-       "EUDATE24HRTIMEYYYYMMDD/HHMMSS": {
-           'mask': "####/##/## ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "YYYY/MM/DD 24Hr Time"
-           },
-       "EUDATE24HRTIMEYYYYMMDD.HHMMSS": {
-           'mask': "####.##.## ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "YYYY.MM.DD 24Hr Time"
-           },
-       "EUDATE24HRTIMEDDMMYYYY/HHMMSS": {
-           'mask': "##/##/#### ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "DD/MM/YYYY 24Hr Time"
-           },
-       "EUDATE24HRTIMEDDMMYYYY.HHMMSS": {
-           'mask': "##.##.#### ##:##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
-           'description': "DD.MM.YYYY 24Hr Time"
-           },
-       "EUDATE24HRTIMEYYYYMMDD/HHMM": {
-           'mask': "####/##/## ##:##",
-           'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes,
-           'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)"
-           },
-       "EUDATE24HRTIMEYYYYMMDD.HHMM": {
-           'mask': "####.##.## ##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes,
-           'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)"
-           },
-       "EUDATE24HRTIMEDDMMYYYY/HHMM": {
-           'mask': "##/##/#### ##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes,
-           'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)"
-           },
-       "EUDATE24HRTIMEDDMMYYYY.HHMM": {
-           'mask': "##.##.#### ##:##",
-           'formatcodes': 'DF',
-           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes,
-           'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)"
-           },
-
-       "TIMEHHMMSS": {
-           'mask': "##:##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'TF!',
-           'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
-           'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)"
-           },
-       "TIMEHHMM": {
-           'mask': "##:## AM",
-           'excludeChars': am_pm_exclude,
-           'formatcodes': 'TF!',
-           'validRegex': '^' + hours + ':' + minutes + ' (A|P)M',
-           'description': "HH:MM (A|P)M\n(see TimeCtrl)"
-           },
-       "24HRTIMEHHMMSS": {
-           'mask': "##:##:##",
-           'formatcodes': 'TF',
-           'validRegex': '^' + milhours + ':' + minutes + ':' + seconds,
-           'description': "24Hr HH:MM:SS\n(see TimeCtrl)"
-           },
-       "24HRTIMEHHMM": {
-           'mask': "##:##",
-           'formatcodes': 'TF',
-           'validRegex': '^' + milhours + ':' + minutes,
-           'description': "24Hr HH:MM\n(see TimeCtrl)"
-           },
-       "USSOCIALSEC": {
-           'mask': "###-##-####",
-           'formatcodes': 'F',
-           'validRegex': "\d{3}-\d{2}-\d{4}",
-           'description': "Social Sec#"
-           },
-       "CREDITCARD": {
-           'mask': "####-####-####-####",
-           'formatcodes': 'F',
-           'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}",
-           'description': "Credit Card"
-           },
-       "EXPDATEMMYY": {
-           'mask': "##/##",
-           'formatcodes': "F",
-           'validRegex': "^" + months + "/\d\d",
-           'description': "Expiration MM/YY"
-           },
-       "USZIP": {
-           'mask': "#####",
-           'formatcodes': 'F',
-           'validRegex': "^\d{5}",
-           'description': "US 5-digit zip code"
-           },
-       "USZIPPLUS4": {
-           'mask': "#####-####",
-           'formatcodes': 'F',
-           'validRegex': "\d{5}-(\s{4}|\d{4})",
-           'description': "US zip+4 code"
-           },
-       "PERCENT": {
-           'mask': "0.##",
-           'formatcodes': 'F',
-           'validRegex': "^0.\d\d",
-           'description': "Percentage"
-           },
-       "AGE": {
-           'mask': "###",
-           'formatcodes': "F",
-           'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]",
-           'description': "Age"
-           },
-       "EMAIL": {
-           'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
-           'excludeChars': " \\/*&%$#!+='\"",
-           'formatcodes': "F>",
-           'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$",
-           'description': "Email address"
-           },
-       "IPADDR": {
-           'mask': "###.###.###.###",
-           'formatcodes': 'F_Sr',
-           'validRegex': "(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}",
-           'description': "IP Address\n(see IpAddrCtrl)"
-           }
-       }
-
-# build demo-friendly dictionary of descriptions of autoformats
-autoformats = []
-for key, value in masktags.items():
-    autoformats.append((key, value['description']))
-autoformats.sort()
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class Field:
-    valid_params = {
-              'index': None,                    ## which field of mask; set by parent control.
-              'mask': "",                       ## mask chars for this field
-              'extent': (),                     ## (edit start, edit_end) of field; set by parent control.
-              'formatcodes':  "",               ## codes indicating formatting options for the control
-              'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given
-              'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits
-              'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction
-              'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard
-              'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers.
-              'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar
-              'excludeChars': "",               ## optional string of chars to exclude even if main mask type does
-              'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't
-              'validRegex':   "",               ## optional regular expression to use to validate the control
-              'validRange':   (),               ## Optional hi-low range for numerics
-              'choices':    [],                 ## Optional list for character expressions
-              'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list
-              'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search
-              'autoSelect': False,              ## Set to True to try auto-completion on each keystroke:
-              'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl
-              '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
-              }
-
-    # 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')
-
-    def __init__(self, **kwargs):
-        """
-        This is the "constructor" for setting up parameters for fields.
-        a field_index of -1 is used to indicate "the entire control."
-        """
-####        dbg('Field::Field', indent=1)
-        # Validate legitimate set of parameters:
-        for key in kwargs.keys():
-            if key not in Field.valid_params.keys():
-####                dbg(indent=0)
-                raise TypeError('invalid parameter "%s"' % (key))
-
-        # Set defaults for each parameter for this instance, and fully
-        # populate initial parameter list for configuration:
-        for key, value in Field.valid_params.items():
-            setattr(self, '_' + key, copy.copy(value))
-            if not kwargs.has_key(key):
-                kwargs[key] = copy.copy(value)
-
-        self._autoCompleteIndex = -1
-        self._SetParameters(**kwargs)
-        self._ValidateParameters(**kwargs)
-
-####        dbg(indent=0)
-
-
-    def _SetParameters(self, **kwargs):
-        """
-        This function can be used to set individual or multiple parameters for
-        a masked edit field parameter after construction.
-        """
-##        dbg(suspend=1)
-##        dbg('maskededit.Field::_SetParameters', indent=1)
-        # Validate keyword arguments:
-        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)
-
-        if self._index is not None: dbg('field index:', self._index)
-##        dbg('parameters:', indent=1)
-        for key, value in kwargs.items():
-##            dbg('%s:' % key, value)
-            pass
-##        dbg(indent=0)
-
-
-        old_fillChar = self._fillChar   # store so we can change choice lists accordingly if it changes
-
-        # First, Assign all parameters specified:
-        for key in Field.valid_params.keys():
-            if kwargs.has_key(key):
-                setattr(self, '_' + key, kwargs[key] )
-
-        if kwargs.has_key('formatcodes'):   # (set/changed)
-            self._forceupper  = '!' in self._formatcodes
-            self._forcelower  = '^' in self._formatcodes
-            self._groupdigits = ',' in self._formatcodes
-            self._okSpaces    = '_' in self._formatcodes
-            self._padZero     = '0' in self._formatcodes
-            self._autofit     = 'F' in self._formatcodes
-            self._insertRight = 'r' in self._formatcodes
-            self._allowInsert = '>' in self._formatcodes
-            self._alignRight  = 'R' in self._formatcodes or 'r' in self._formatcodes
-            self._moveOnFieldFull = not '<' in self._formatcodes
-            self._selectOnFieldEntry = 'S' in self._formatcodes
-
-            if kwargs.has_key('groupChar'):
-                self._groupChar = kwargs['groupChar']
-            if kwargs.has_key('decimalChar'):
-                self._decimalChar = kwargs['decimalChar']
-            if kwargs.has_key('shiftDecimalChar'):
-                self._shiftDecimalChar = kwargs['shiftDecimalChar']
-
-        if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'):
-            self._regexMask   = 'V' in self._formatcodes and self._validRegex
-
-        if kwargs.has_key('fillChar'):
-            self._old_fillChar = old_fillChar
-####            dbg("self._old_fillChar: '%s'" % self._old_fillChar)
-
-        if kwargs.has_key('mask') or kwargs.has_key('validRegex'):  # (set/changed)
-            self._isInt = isInteger(self._mask)
-##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask)
-
-##        dbg(indent=0, suspend=0)
-
-
-    def _ValidateParameters(self, **kwargs):
-        """
-        This function can be used to validate individual or multiple parameters for
-        a masked edit field parameter after construction.
-        """
-##        dbg(suspend=1)
-##        dbg('maskededit.Field::_ValidateParameters', indent=1)
-        if self._index is not None: dbg('field index:', self._index)
-####        dbg('parameters:', indent=1)
-##        for key, value in kwargs.items():
-####            dbg('%s:' % key, value)
-####        dbg(indent=0)
-####        dbg("self._old_fillChar: '%s'" % self._old_fillChar)
-
-        # 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))
-
-
-        # Now go do validation, semantic and inter-dependency parameter processing:
-        if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed)
-
-            self._compareChoices = [choice.strip() for choice in self._choices]
-
-            if self._compareNoCase and self._choices:
-                self._compareChoices = [item.lower() for item in self._compareChoices]
-
-            if kwargs.has_key('choices'):
-                self._autoCompleteIndex = -1
-
-
-        if kwargs.has_key('validRegex'):    # (set/changed)
-            if self._validRegex:
-                try:
-                    if self._compareNoCase:
-                        self._filter = re.compile(self._validRegex, re.IGNORECASE)
-                    else:
-                        self._filter = re.compile(self._validRegex)
-                except:
-##                    dbg(indent=0, suspend=0)
-                    raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
-            else:
-                self._filter = None
-
-        if kwargs.has_key('validRange'):    # (set/changed)
-            self._hasRange  = False
-            self._rangeHigh = 0
-            self._rangeLow  = 0
-            if self._validRange:
-                if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]:
-##                    dbg(indent=0, suspend=0)
-                    raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b'
-                                    % (str(self._index), repr(self._validRange)) )
-
-                self._hasRange  = True
-                self._rangeLow  = self._validRange[0]
-                self._rangeHigh = self._validRange[1]
-
-        if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)):       # (set/changed)
-            self._hasList   = False
-            if self._choices and type(self._choices) not in (types.TupleType, types.ListType):
-##                dbg(indent=0, suspend=0)
-                raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
-            elif len( self._choices) > 0:
-                for choice in self._choices:
-                    if type(choice) not in (types.StringType, types.UnicodeType):
-##                        dbg(indent=0, suspend=0)
-                        raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
-
-                length = len(self._mask)
-##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight)
-                if len(self._choices) and length:
-                    if len(self._choices[0]) > length:
-                        # changed mask without respecifying choices; readjust the width as appropriate:
-                        self._choices = [choice.strip() for choice in self._choices]
-                    if self._alignRight:
-                        self._choices = [choice.rjust( length ) for choice in self._choices]
-                    else:
-                        self._choices = [choice.ljust( length ) for choice in self._choices]
-##                    dbg('aligned choices:', self._choices)
-
-                if hasattr(self, '_template'):
-                    # Verify each choice specified is valid:
-                    for choice in self._choices:
-                        if self.IsEmpty(choice) and not self._validRequired:
-                            # allow empty values even if invalid, (just colored differently)
-                            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))
-                self._hasList = True
-
-####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
-####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar)
-        if kwargs.has_key('fillChar') and len(self._choices) > 0:
-            if kwargs['fillChar'] != ' ':
-                self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices]
-            else:
-                self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices]
-##            dbg('updated choices:', self._choices)
-
-
-        if kwargs.has_key('autoSelect') and kwargs['autoSelect']:
-            if not self._hasList:
-##                dbg('no list to auto complete; ignoring "autoSelect=True"')
-                self._autoSelect = False
-
-        # reset field validity assumption:
-        self._valid = True
-##        dbg(indent=0, suspend=0)
-
-
-    def _GetParameter(self, paramname):
-        """
-        Routine for retrieving the value of any given parameter
-        """
-        if Field.valid_params.has_key(paramname):
-            return getattr(self, '_' + paramname)
-        else:
-            TypeError('Field._GetParameter: invalid parameter "%s"' % key)
-
-
-    def IsEmpty(self, slice):
-        """
-        Indicates whether the specified slice is considered empty for the
-        field.
-        """
-##        dbg('Field::IsEmpty("%s")' % slice, indent=1)
-        if not hasattr(self, '_template'):
-##            dbg(indent=0)
-            raise AttributeError('_template')
-
-##        dbg('self._template: "%s"' % self._template)
-##        dbg('self._defaultValue: "%s"' % str(self._defaultValue))
-        if slice == self._template and not self._defaultValue:
-##            dbg(indent=0)
-            return True
-
-        elif slice == self._template:
-            empty = True
-            for pos in range(len(self._template)):
-####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos])
-                if slice[pos] not in (' ', self._fillChar):
-                    empty = False
-                    break
-##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0)
-            return empty
-        else:
-##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0)
-            return False
-
-
-    def IsValid(self, slice):
-        """
-        Indicates whether the specified slice is considered a valid value for the
-        field.
-        """
-##        dbg(suspend=1)
-##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1)
-        valid = True    # assume true to start
-
-        if self.IsEmpty(slice):
-##            dbg(indent=0, suspend=0)
-            if self._emptyInvalid:
-                return False
-            else:
-                return True
-
-        elif self._hasList and self._choiceRequired:
-##            dbg("(member of list required)")
-            # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices):
-            if self._fillChar != ' ':
-                slice = slice.replace(self._fillChar, ' ')
-##                dbg('updated slice:"%s"' % slice)
-            compareStr = slice.strip()
-
-            if self._compareNoCase:
-                compareStr = compareStr.lower()
-            valid = compareStr in self._compareChoices
-
-        elif self._hasRange and not self.IsEmpty(slice):
-##            dbg('validating against range')
-            try:
-                # allow float as well as int ranges (int comparisons for free.)
-                valid = self._rangeLow <= float(slice) <= self._rangeHigh
-            except:
-                valid = False
-
-        elif self._validRegex and self._filter:
-##            dbg('validating against regex')
-            valid = (re.match( self._filter, slice) is not None)
-
-        if valid and self._validFunc:
-##            dbg('validating against supplied function')
-            valid = self._validFunc(slice)
-##        dbg('valid?', valid, indent=0, suspend=0)
-        return valid
-
-
-    def _AdjustField(self, slice):
-        """ 'Fixes' an integer field. Right or left-justifies, as required."""
-##        dbg('Field::_AdjustField("%s")' % slice, indent=1)
-        length = len(self._mask)
-####        dbg('length(self._mask):', length)
-####        dbg('self._useParensForNegatives?', self._useParensForNegatives)
-        if self._isInt:
-            if self._useParensForNegatives:
-                signpos = slice.find('(')
-                right_signpos = slice.find(')')
-                intStr = slice.replace('(', '').replace(')', '')    # drop sign, if any
-            else:
-                signpos = slice.find('-')
-                intStr = slice.replace( '-', '' )                   # drop sign, if any
-                right_signpos = -1
-
-            intStr = intStr.replace(' ', '')                        # drop extra spaces
-            intStr = string.replace(intStr,self._fillChar,"")       # drop extra fillchars
-            intStr = string.replace(intStr,"-","")                  # drop sign, if any
-            intStr = string.replace(intStr, self._groupChar, "")    # lose commas/dots
-####            dbg('intStr:"%s"' % intStr)
-            start, end = self._extent
-            field_len = end - start
-            if not self._padZero and len(intStr) != field_len and intStr.strip():
-                intStr = str(long(intStr))
-####            dbg('raw int str: "%s"' % intStr)
-####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes)
-            if self._groupdigits:
-                new = ''
-                cnt = 1
-                for i in range(len(intStr)-1, -1, -1):
-                    new = intStr[i] + new
-                    if (cnt) % 3 == 0:
-                        new = self._groupChar + new
-                    cnt += 1
-                if new and new[0] == self._groupChar:
-                    new = new[1:]
-                if len(new) <= length:
-                    # expanded string will still fit and leave room for sign:
-                    intStr = new
-                # else... leave it without the commas...
-
-##            dbg('padzero?', self._padZero)
-##            dbg('len(intStr):', len(intStr), 'field length:', length)
-            if self._padZero and len(intStr) < length:
-                intStr = '0' * (length - len(intStr)) + intStr
-                if signpos != -1:   # we had a sign before; restore it
-                    if self._useParensForNegatives:
-                        intStr = '(' + intStr[1:]
-                        if right_signpos != -1:
-                            intStr += ')'
-                    else:
-                        intStr = '-' + intStr[1:]
-            elif signpos != -1 and slice[0:signpos].strip() == '':    # - was before digits
-                if self._useParensForNegatives:
-                    intStr = '(' + intStr
-                    if right_signpos != -1:
-                        intStr += ')'
-                else:
-                    intStr = '-' + intStr
-            elif right_signpos != -1:
-                # must have had ')' but '(' was before field; re-add ')'
-                intStr += ')'
-            slice = intStr
-
-        slice = slice.strip() # drop extra spaces
-
-        if self._alignRight:     ## Only if right-alignment is enabled
-            slice = slice.rjust( length )
-        else:
-            slice = slice.ljust( length )
-        if self._fillChar != ' ':
-            slice = slice.replace(' ', self._fillChar)
-##        dbg('adjusted slice: "%s"' % slice, indent=0)
-        return slice
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class MaskedEditMixin:
-    """
-    This class allows us to abstract the masked edit functionality that could
-    be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.)
-    """
-    valid_ctrl_params = {
-              'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control
-              'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary
-              'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask
-              'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year
-              'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete
-              'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control
-              'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently,
-                                                ## from field validation constraints
-              'emptyBackgroundColour': "White",
-              'validBackgroundColour': "White",
-              'invalidBackgroundColour': "Yellow",
-              'foregroundColour': "Black",
-              'signedForegroundColour': "Red",
-              'demo': False}
-
-
-    def __init__(self, name = 'wxMaskedEdit', **kwargs):
-        """
-        This is the "constructor" for setting up the mixin variable parameters for the composite class.
-        """
-
-        self.name = name
-
-        # set up flag for doing optional things to base control if possible
-        if not hasattr(self, 'controlInitialized'):
-            self.controlInitialized = False
-
-        # Set internal state var for keeping track of whether or not a character
-        # action results in a modification of the control, since .SetValue()
-        # doesn't modify the base control's internal state:
-        self.modified = False
-        self._previous_mask = None
-
-        # Validate legitimate set of parameters:
-        for key in kwargs.keys():
-            if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
-                raise TypeError('%s: invalid parameter "%s"' % (name, key))
-
-        ## Set up dictionary that can be used by subclasses to override or add to default
-        ## behavior for individual characters.  Derived subclasses needing to change
-        ## default behavior for keys can either redefine the default functions for the
-        ## common keys or add functions for specific keys to this list.  Each function
-        ## added should take the key event as argument, and return False if the key
-        ## requires no further processing.
-        ##
-        ## Initially populated with navigation and function control keys:
-        self._keyhandlers = {
-            # default navigation keys and handlers:
-            wx.WXK_BACK:   self._OnErase,
-            wx.WXK_LEFT:   self._OnArrow,
-            wx.WXK_RIGHT:  self._OnArrow,
-            wx.WXK_UP:     self._OnAutoCompleteField,
-            wx.WXK_DOWN:   self._OnAutoCompleteField,
-            wx.WXK_TAB:    self._OnChangeField,
-            wx.WXK_HOME:   self._OnHome,
-            wx.WXK_END:    self._OnEnd,
-            wx.WXK_RETURN: self._OnReturn,
-            wx.WXK_PRIOR:  self._OnAutoCompleteField,
-            wx.WXK_NEXT:   self._OnAutoCompleteField,
-
-            # default function control keys and handlers:
-            wx.WXK_DELETE: self._OnErase,
-            WXK_CTRL_A: self._OnCtrl_A,
-            WXK_CTRL_C: self._OnCtrl_C,
-            WXK_CTRL_S: self._OnCtrl_S,
-            WXK_CTRL_V: self._OnCtrl_V,
-            WXK_CTRL_X: self._OnCtrl_X,
-            WXK_CTRL_Z: self._OnCtrl_Z,
-            }
-
-        ## bind standard navigational and control keycodes to this instance,
-        ## so that they can be augmented and/or changed in derived classes:
-        self._nav = list(nav)
-        self._control = list(control)
-
-        ## Dynamically evaluate and store string constants for mask chars
-        ## so that locale settings can be made after this module is imported
-        ## and the controls created after that is done can allow the
-        ## appropriate characters:
-        self.maskchardict  = {
-            '#': string.digits,
-            'A': string.uppercase,
-            'a': string.lowercase,
-            'X': string.letters + string.punctuation + string.digits,
-            'C': string.letters,
-            'N': string.letters + string.digits,
-            '&': string.punctuation
-        }
-
-        ## self._ignoreChange is used by MaskedComboBox, because
-        ## of the hack necessary to determine the selection; it causes
-        ## EVT_TEXT messages from the combobox to be ignored if set.
-        self._ignoreChange = False
-
-        # These are used to keep track of previous value, for undo functionality:
-        self._curValue  = None
-        self._prevValue = None
-
-        self._valid     = True
-
-        # Set defaults for each parameter for this instance, and fully
-        # populate initial parameter list for configuration:
-        for key, value in MaskedEditMixin.valid_ctrl_params.items():
-            setattr(self, '_' + key, copy.copy(value))
-            if not kwargs.has_key(key):
-####                dbg('%s: "%s"' % (key, repr(value)))
-                kwargs[key] = copy.copy(value)
-
-        # Create a "field" that holds global parameters for control constraints
-        self._ctrl_constraints = self._fields[-1] = Field(index=-1)
-        self.SetCtrlParameters(**kwargs)
-
-
-
-    def SetCtrlParameters(self, **kwargs):
-        """
-        This public function can be used to set individual or multiple masked edit
-        parameters after construction.
-        """
-##        dbg(suspend=1)
-##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1)
-####        dbg('kwargs:', indent=1)
-##        for key, value in kwargs.items():
-####            dbg(key, '=', value)
-####        dbg(indent=0)
-
-        # Validate keyword arguments:
-        constraint_kwargs = {}
-        ctrl_kwargs = {}
-        for key, value in kwargs.items():
-            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))
-            elif key in Field.valid_params.keys():
-                constraint_kwargs[key] = value
-            else:
-                ctrl_kwargs[key] = value
-
-        mask = None
-        reset_args = {}
-
-        if ctrl_kwargs.has_key('autoformat'):
-            autoformat = ctrl_kwargs['autoformat']
-        else:
-            autoformat = None
-
-        # handle "parochial name" backward compatibility:
-        if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys():
-            autoformat = autoformat.replace('MILTIME', '24HRTIME')
-
-        if autoformat != self._autoformat and autoformat in masktags.keys():
-##            dbg('autoformat:', autoformat)
-            self._autoformat                  = autoformat
-            mask                              = masktags[self._autoformat]['mask']
-            # gather rest of any autoformat parameters:
-            for param, value in masktags[self._autoformat].items():
-                if param == 'mask': continue    # (must be present; already accounted for)
-                constraint_kwargs[param] = value
-
-        elif autoformat and not autoformat in masktags.keys():
-            raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
-        else:
-##            dbg('autoformat not selected')
-            if kwargs.has_key('mask'):
-                mask = kwargs['mask']
-##                dbg('mask:', mask)
-
-        ## Assign style flags
-        if mask is None:
-##            dbg('preserving previous mask')
-            mask = self._previous_mask   # preserve previous mask
-        else:
-##            dbg('mask (re)set')
-            reset_args['reset_mask'] = mask
-            constraint_kwargs['mask'] = mask
-
-            # wipe out previous fields; preserve new control-level constraints
-            self._fields = {-1: self._ctrl_constraints}
-
-
-        if ctrl_kwargs.has_key('fields'):
-            # do field parameter type validation, and conversion to internal dictionary
-            # as appropriate:
-            fields = ctrl_kwargs['fields']
-            if type(fields) in (types.ListType, types.TupleType):
-                for i in range(len(fields)):
-                    field = fields[i]
-                    if not isinstance(field, Field):
-##                        dbg(indent=0, suspend=0)
-                        raise AttributeError('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)
-                        raise AttributeError('invalid type for field parameter: %s' % repr(field))
-                    self._fields[index] = field
-            else:
-##                dbg(indent=0, suspend=0)
-                raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
-
-        # Assign constraint parameters for entire control:
-####        dbg('control constraints:', indent=1)
-##        for key, value in constraint_kwargs.items():
-####            dbg('%s:' % key, value)
-####        dbg(indent=0)
-
-        # determine if changing parameters that should affect the entire control:
-        for key in MaskedEditMixin.valid_ctrl_params.keys():
-            if key in ( 'mask', 'fields' ): continue    # (processed separately)
-            if ctrl_kwargs.has_key(key):
-                setattr(self, '_' + key, ctrl_kwargs[key])
-
-        # Validate color parameters, converting strings to named colors and validating
-        # result if appropriate:
-        for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour',
-                    'foregroundColour', 'signedForegroundColour'):
-            if ctrl_kwargs.has_key(key):
-                if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType):
-                    c = wx.NamedColour(ctrl_kwargs[key])
-                    if c.Get() == (-1, -1, -1):
-                        raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
-                    else:
-                        # replace attribute with wxColour object:
-                        setattr(self, '_' + key, c)
-                        # attach a python dynamic attribute to wxColour for debug printouts
-                        c._name = ctrl_kwargs[key]
-
-                elif type(ctrl_kwargs[key]) != type(wx.BLACK):
-                    raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
-
-
-##        dbg('self._retainFieldValidation:', self._retainFieldValidation)
-        if not self._retainFieldValidation:
-            # Build dictionary of any changing parameters which should be propagated to the
-            # component fields:
-            for arg in Field.propagating_params:
-####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg))
-####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg))
-                reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg)
-####                dbg('reset_args[%s]?' % arg, reset_args[arg])
-
-        # Set the control-level constraints:
-        self._ctrl_constraints._SetParameters(**constraint_kwargs)
-
-        # This routine does the bulk of the interdependent parameter processing, determining
-        # the field extents of the mask if changed, resetting parameters as appropriate,
-        # determining the overall template value for the control, etc.
-        self._configure(mask, **reset_args)
-
-        # now that we've propagated the field constraints and mask portions to the
-        # various fields, validate the constraints
-        self._ctrl_constraints._ValidateParameters(**constraint_kwargs)
-
-        # Validate that all choices for given fields are at least of the
-        # necessary length, and that they all would be valid pastes if pasted
-        # into their respective fields:
-####        dbg('validating choices')
-        self._validateChoices()
-
-
-        self._autofit = self._ctrl_constraints._autofit
-        self._isNeg      = False
-
-        self._isDate     = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask)
-        self._isTime     = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask)
-        if self._isDate:
-            # Set _dateExtent, used in date validation to locate date in string;
-            # always set as though year will be 4 digits, even if mask only has
-            # 2 digits, so we can always properly process the intended year for
-            # date validation (leap years, etc.)
-            if self._mask.find('CCC') != -1: self._dateExtent = 11
-            else:                            self._dateExtent = 10
-
-            self._4digityear = len(self._mask) > 8 and self._mask[9] == '#'
-
-        if self._isDate and self._autoformat:
-            # Auto-decide datestyle:
-            if self._autoformat.find('MDDY')    != -1: self._datestyle = 'MDY'
-            elif self._autoformat.find('YMMD')  != -1: self._datestyle = 'YMD'
-            elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD'
-            elif self._autoformat.find('DMMY')  != -1: self._datestyle = 'DMY'
-            elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY'
-
-        # Give derived controls a chance to react to parameter changes before
-        # potentially changing current value of the control.
-        self._OnCtrlParametersChanged()
-
-        if self.controlInitialized:
-            # Then the base control is available for configuration;
-            # take action on base control based on new settings, as appropriate.
-            if kwargs.has_key('useFixedWidthFont'):
-                # Set control font - fixed width by default
-                self._setFont()
-
-            if reset_args.has_key('reset_mask'):
-##                dbg('reset mask')
-                curvalue = self._GetValue()
-                if curvalue.strip():
-                    try:
-##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue())
-                        self._SetInitialValue(self._GetValue())
-                    except Exception, e:
-##                        dbg('exception caught:', e)
-##                        dbg("current value doesn't work; attempting to reset to template")
-                        self._SetInitialValue()
-                else:
-##                    dbg('attempting to _SetInitialValue() with template')
-                    self._SetInitialValue()
-
-            elif kwargs.has_key('useParensForNegatives'):
-                newvalue = self._getSignedValue()[0]
-
-                if newvalue is not None:
-                    # Adjust for new mask:
-                    if len(newvalue) < len(self._mask):
-                        newvalue += ' '
-                    elif len(newvalue) > len(self._mask):
-                        if newvalue[-1] in (' ', ')'):
-                            newvalue = newvalue[:-1]
-
-##                    dbg('reconfiguring value for parens:"%s"' % newvalue)
-                    self._SetValue(newvalue)
-
-                    if self._prevValue != newvalue:
-                        self._prevValue = newvalue  # disallow undo of sign type
-
-            if self._autofit:
-##                dbg('setting client size to:', self._CalcSize())
-                self.SetClientSize(self._CalcSize())
-
-            # Set value/type-specific formatting
-            self._applyFormatting()
-##        dbg(indent=0, suspend=0)
-
-    def SetMaskParameters(self, **kwargs):
-        """ old name for this function """
-        return self.SetCtrlParameters(**kwargs)
-
-
-    def GetCtrlParameter(self, paramname):
-        """
-        Routine for retrieving the value of any given parameter
-        """
-        if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')):
-            return getattr(self, '_' + paramname.replace('Color', 'Colour'))
-        elif Field.valid_params.has_key(paramname):
-            return self._ctrl_constraints._GetParameter(paramname)
-        else:
-            TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname))
-
-    def GetMaskParameter(self, paramname):
-        """ old name for this function """
-        return self.GetCtrlParameter(paramname)
-
-
-## This idea worked, but Boa was unable to use this solution...
-##    def _attachMethod(self, func):
-##        import new
-##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__))
-##
-##
-##    def _DefinePropertyFunctions(exposed_params):
-##        for param in exposed_params:
-##            propname = param[0].upper() + param[1:]
-##
-##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-##            self._attachMethod(locals()['Set%s' % propname])
-##            self._attachMethod(locals()['Get%s' % propname])
-##
-##            if param.find('Colour') != -1:
-##                # add non-british spellings, for backward-compatibility
-##                propname.replace('Colour', 'Color')
-##
-##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-##                self._attachMethod(locals()['Set%s' % propname])
-##                self._attachMethod(locals()['Get%s' % propname])
-##
-
-
-    def SetFieldParameters(self, field_index, **kwargs):
-        """
-        Routine provided to modify the parameters of a given field.
-        Because changes to fields can affect the overall control,
-        direct access to the fields is prevented, and the control
-        is always "reconfigured" after setting a field parameter.
-        """
-        if field_index not in self._field_indices:
-            raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
-        # set parameters as requested:
-        self._fields[field_index]._SetParameters(**kwargs)
-
-        # Possibly reprogram control template due to resulting changes, and ensure
-        # control-level params are still propagated to fields:
-        self._configure(self._previous_mask)
-        self._fields[field_index]._ValidateParameters(**kwargs)
-
-        if self.controlInitialized:
-            if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'):
-                self._SetInitialValue()
-
-                if self._autofit:
-                    self.SetClientSize(self._CalcSize())
-
-            # Set value/type-specific formatting
-            self._applyFormatting()
-
-
-    def GetFieldParameter(self, field_index, paramname):
-        """
-        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))
-        elif Field.valid_params.has_key(paramname):
-            return self._fields[field_index]._GetParameter(paramname)
-        else:
-            TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
-
-
-    def _SetKeycodeHandler(self, keycode, func):
-        """
-        This function adds and/or replaces key event handling functions
-        used by the control.  <func> should take the event as argument
-        and return False if no further action on the key is necessary.
-        """
-        self._keyhandlers[keycode] = func
-
-
-    def _SetKeyHandler(self, char, func):
-        """
-        This function adds and/or replaces key event handling functions
-        for ascii characters.  <func> should take the event as argument
-        and return False if no further action on the key is necessary.
-        """
-        self._SetKeycodeHandler(ord(char), func)
-
-
-    def _AddNavKeycode(self, keycode, handler=None):
-        """
-        This function allows a derived subclass to augment the list of
-        keycodes that are considered "navigational" keys.
-        """
-        self._nav.append(keycode)
-        if handler:
-            self._keyhandlers[keycode] = handler
-
-
-    def _AddNavKey(self, char, handler=None):
-        """
-        This function is a convenience function so you don't have to
-        remember to call ord() for ascii chars to be used for navigation.
-        """
-        self._AddNavKeycode(ord(char), handler)
-
-
-    def _GetNavKeycodes(self):
-        """
-        This function retrieves the current list of navigational keycodes for
-        the control.
-        """
-        return self._nav
-
-
-    def _SetNavKeycodes(self, keycode_func_tuples):
-        """
-        This function allows you to replace the current list of keycode processed
-        as navigation keys, and bind associated optional keyhandlers.
-        """
-        self._nav = []
-        for keycode, func in keycode_func_tuples:
-            self._nav.append(keycode)
-            if func:
-                self._keyhandlers[keycode] = func
-
-
-    def _processMask(self, mask):
-        """
-        This subroutine expands {n} syntax in mask strings, and looks for escaped
-        special characters and returns the expanded mask, and an dictionary
-        of booleans indicating whether or not a given position in the mask is
-        a mask character or not.
-        """
-##        dbg('_processMask: mask', mask, indent=1)
-        # regular expression for parsing c{n} syntax:
-        rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}')
-        s = mask
-        match = rex.search(s)
-        while match:    # found an(other) occurrence
-            maskchr = s[match.start(1):match.end(1)]            # char to be repeated
-            repcount = int(s[match.start(2):match.end(2)])      # the number of times
-            replacement = string.join( maskchr * repcount, "")  # the resulting substr
-            s = s[:match.start(1)] + replacement + s[match.end(2)+1:]   #account for trailing '}'
-            match = rex.search(s)                               # look for another such entry in mask
-
-        self._decimalChar = self._ctrl_constraints._decimalChar
-        self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar
-
-        self._isFloat      = isFloatingPoint(s) and not self._ctrl_constraints._validRegex
-        self._isInt      = isInteger(s) and not self._ctrl_constraints._validRegex
-        self._signOk     = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt)
-        self._useParens  = self._ctrl_constraints._useParensForNegatives
-        self._isNeg      = False
-####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens)
-####        dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s),
-##            'ctrl regex:', self._ctrl_constraints._validRegex)
-
-        if self._signOk and s[0] != ' ':
-            s = ' ' + s
-            if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ':
-                self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue
-            self._signpos = 0
-
-            if self._useParens:
-                s += ' '
-                self._ctrl_constraints._defaultValue += ' '
-
-        # Now, go build up a dictionary of booleans, indexed by position,
-        # indicating whether or not a given position is masked or not
-        ismasked = {}
-        i = 0
-        while i < len(s):
-            if s[i] == '\\':            # if escaped character:
-                ismasked[i] = False     #     mark position as not a mask char
-                if i+1 < len(s):        #     if another char follows...
-                    s = s[:i] + s[i+1:] #         elide the '\'
-                    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
-            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:', ismasked)
-##        dbg('new mask: "%s"' % s, indent=0)
-
-        return s, ismasked
-
-
-    def _calcFieldExtents(self):
-        """
-        Subroutine responsible for establishing/configuring field instances with
-        indices and editable extents appropriate to the specified mask, and building
-        the lookup table mapping each position to the corresponding field.
-        """
-        self._lookupField = {}
-        if self._mask:
-
-            ## Create dictionary of positions,characters in mask
-            self.maskdict = {}
-            for charnum in range( len( self._mask)):
-                self.maskdict[charnum] = self._mask[charnum:charnum+1]
-
-            # For the current mask, create an ordered list of field extents
-            # and a dictionary of positions that map to field indices:
-
-            if self._signOk: start = 1
-            else: start = 0
-
-            if self._isFloat:
-                # Skip field "discovery", and just construct a 2-field control with appropriate
-                # constraints for a floating-point entry.
-
-                # .setdefault always constructs 2nd argument even if not needed, so we do this
-                # the old-fashioned way...
-                if not self._fields.has_key(0):
-                    self._fields[0] = Field()
-                if not self._fields.has_key(1):
-                    self._fields[1] = Field()
-
-                self._decimalpos = string.find( self._mask, '.')
-##                dbg('decimal pos =', self._decimalpos)
-
-                formatcodes = self._fields[0]._GetParameter('formatcodes')
-                if 'R' not in formatcodes: formatcodes += 'R'
-                self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos),
-                                               mask=self._mask[start:self._decimalpos], formatcodes=formatcodes)
-                end = len(self._mask)
-                if self._signOk and self._useParens:
-                    end -= 1
-                self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end),
-                                               mask=self._mask[self._decimalpos+1:end])
-
-                for i in range(self._decimalpos+1):
-                    self._lookupField[i] = 0
-
-                for i in range(self._decimalpos+1, len(self._mask)+1):
-                    self._lookupField[i] = 1
-
-            elif self._isInt:
-                # Skip field "discovery", and just construct a 1-field control with appropriate
-                # constraints for a integer entry.
-                if not self._fields.has_key(0):
-                    self._fields[0] = Field(index=0)
-                end = len(self._mask)
-                if self._signOk and self._useParens:
-                    end -= 1
-                self._fields[0]._SetParameters(index=0, extent=(start, end),
-                                               mask=self._mask[start:end])
-                for i in range(len(self._mask)+1):
-                    self._lookupField[i] = 0
-            else:
-                # generic control; parse mask to figure out where the fields are:
-                field_index = 0
-                pos = 0
-                i = self._findNextEntry(pos,adjustInsert=False)  # go to 1st entry point:
-                if i < len(self._mask):   # no editable chars!
-                    for j in range(pos, i+1):
-                        self._lookupField[j] = field_index
-                    pos = i       # figure out field for 1st editable space:
-
-                while i <= len(self._mask):
-####                    dbg('searching: outer field loop: i = ', i)
-                    if self._isMaskChar(i):
-####                        dbg('1st char is mask char; recording edit_start=', i)
-                        edit_start = i
-                        # Skip to end of editable part of current field:
-                        while i < len(self._mask) and self._isMaskChar(i):
-                            self._lookupField[i] = field_index
-                            i += 1
-####                        dbg('edit_end =', i)
-                        edit_end = i
-                        self._lookupField[i] = field_index
-####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index))
-                        if not self._fields.has_key(field_index):
-                            kwargs = Field.valid_params.copy()
-                            kwargs['index'] = field_index
-                            kwargs['extent'] = (edit_start, edit_end)
-                            kwargs['mask'] = self._mask[edit_start:edit_end]
-                            self._fields[field_index] = Field(**kwargs)
-                        else:
-                            self._fields[field_index]._SetParameters(
-                                                                index=field_index,
-                                                                extent=(edit_start, edit_end),
-                                                                mask=self._mask[edit_start:edit_end])
-                    pos = i
-                    i = self._findNextEntry(pos, adjustInsert=False)  # go to next field:
-                    if i > pos:
-                        for j in range(pos, i+1):
-                            self._lookupField[j] = field_index
-                    if i >= len(self._mask):
-                        break           # if past end, we're done
-                    else:
-                        field_index += 1
-####                        dbg('next field:', field_index)
-
-        indices = self._fields.keys()
-        indices.sort()
-        self._field_indices = indices[1:]
-####        dbg('lookupField map:', indent=1)
-##        for i in range(len(self._mask)):
-####            dbg('pos %d:' % i, self._lookupField[i])
-####        dbg(indent=0)
-
-        # 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))
-
-
-    def _calcTemplate(self, reset_fillchar, reset_default):
-        """
-        Subroutine for processing current fillchars and default values for
-        whole control and individual fields, constructing the resulting
-        overall template, and adjusting the current value as necessary.
-        """
-        default_set = False
-        if self._ctrl_constraints._defaultValue:
-            default_set = True
-        else:
-            for field in self._fields.values():
-                if field._defaultValue and not reset_default:
-                    default_set = True
-##        dbg('default set?', default_set)
-
-        # Determine overall new template for control, and keep track of previous
-        # values, so that current control value can be modified as appropriate:
-        if self.controlInitialized: curvalue = list(self._GetValue())
-        else:                       curvalue = None
-
-        if hasattr(self, '_fillChar'): old_fillchars = self._fillChar
-        else:                          old_fillchars = None
-
-        if hasattr(self, '_template'): old_template = self._template
-        else:                          old_template = None
-
-        self._template = ""
-
-        self._fillChar = {}
-        reset_value = False
-
-        for field in self._fields.values():
-            field._template = ""
-
-        for pos in range(len(self._mask)):
-####            dbg('pos:', pos)
-            field = self._FindField(pos)
-####            dbg('field:', field._index)
-            start, end = field._extent
-
-            if pos == 0 and self._signOk:
-                self._template = ' ' # always make 1st 1st position blank, regardless of fillchar
-            elif self._isFloat and pos == self._decimalpos:
-                self._template += self._decimalChar
-            elif self._isMaskChar(pos):
-                if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar:
-                    fillChar = field._fillChar
-                else:
-                    fillChar = self._ctrl_constraints._fillChar
-                self._fillChar[pos] = fillChar
-
-                # Replace any current old fillchar with new one in current value;
-                # if action required, set reset_value flag so we can take that action
-                # after we're all done
-                if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue:
-                    if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar:
-                        reset_value = True
-                        curvalue[pos] = fillChar
-
-                if not field._defaultValue and not self._ctrl_constraints._defaultValue:
-####                    dbg('no default value')
-                    self._template += fillChar
-                    field._template += fillChar
-
-                elif field._defaultValue and not reset_default:
-####                    dbg('len(field._defaultValue):', len(field._defaultValue))
-####                    dbg('pos-start:', pos-start)
-                    if len(field._defaultValue) > pos-start:
-####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start])
-                        self._template += field._defaultValue[pos-start]
-                        field._template += field._defaultValue[pos-start]
-                    else:
-####                        dbg('field default not long enough; using fillChar')
-                        self._template += fillChar
-                        field._template += fillChar
-                else:
-                    if len(self._ctrl_constraints._defaultValue) > pos:
-####                        dbg('using control default')
-                        self._template += self._ctrl_constraints._defaultValue[pos]
-                        field._template += self._ctrl_constraints._defaultValue[pos]
-                    else:
-####                        dbg('ctrl default not long enough; using fillChar')
-                        self._template += fillChar
-                        field._template += fillChar
-####                dbg('field[%d]._template now "%s"' % (field._index, field._template))
-####                dbg('self._template now "%s"' % self._template)
-            else:
-                self._template += self._mask[pos]
-
-        self._fields[-1]._template = self._template     # (for consistency)
-
-        if curvalue:    # had an old value, put new one back together
-            newvalue = string.join(curvalue, "")
-        else:
-            newvalue = None
-
-        if default_set:
-            self._defaultValue = self._template
-##            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))
-
-            # if no fillchar change, but old value == old template, replace it:
-            if newvalue == old_template:
-                newvalue = self._template
-                reset_value = True
-        else:
-            self._defaultValue = None
-
-        if reset_value:
-##            dbg('resetting value to: "%s"' % newvalue)
-            pos = self._GetInsertionPoint()
-            sel_start, sel_to = self._GetSelection()
-            self._SetValue(newvalue)
-            self._SetInsertionPoint(pos)
-            self._SetSelection(sel_start, sel_to)
-
-
-    def _propagateConstraints(self, **reset_args):
-        """
-        Subroutine for propagating changes to control-level constraints and
-        formatting to the individual fields as appropriate.
-        """
-        parent_codes = self._ctrl_constraints._formatcodes
-        parent_includes = self._ctrl_constraints._includeChars
-        parent_excludes = self._ctrl_constraints._excludeChars
-        for i in self._field_indices:
-            field = self._fields[i]
-            inherit_args = {}
-            if len(self._field_indices) == 1:
-                inherit_args['formatcodes'] = parent_codes
-                inherit_args['includeChars'] = parent_includes
-                inherit_args['excludeChars'] = parent_excludes
-            else:
-                field_codes = current_codes = field._GetParameter('formatcodes')
-                for c in parent_codes:
-                    if c not in field_codes: field_codes += c
-                if field_codes != current_codes:
-                    inherit_args['formatcodes'] = field_codes
-
-                include_chars = current_includes = field._GetParameter('includeChars')
-                for c in parent_includes:
-                    if not c in include_chars: include_chars += c
-                if include_chars != current_includes:
-                    inherit_args['includeChars'] = include_chars
-
-                exclude_chars = current_excludes = field._GetParameter('excludeChars')
-                for c in parent_excludes:
-                    if not c in exclude_chars: exclude_chars += c
-                if exclude_chars != current_excludes:
-                    inherit_args['excludeChars'] = exclude_chars
-
-            if reset_args.has_key('defaultValue') and reset_args['defaultValue']:
-                inherit_args['defaultValue'] = ""   # (reset for field)
-
-            for param in Field.propagating_params:
-####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param))
-####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param])
-                if reset_args.has_key(param):
-                    inherit_args[param] = self.GetCtrlParameter(param)
-####                    dbg('inherit_args[%s]' % param, inherit_args[param])
-
-            if inherit_args:
-                field._SetParameters(**inherit_args)
-                field._ValidateParameters(**inherit_args)
-
-
-    def _validateChoices(self):
-        """
-        Subroutine that validates that all choices for given fields are at
-        least of the necessary length, and that they all would be valid pastes
-        if pasted into their respective fields.
-        """
-        for field in self._fields.values():
-            if field._choices:
-                index = field._index
-                if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices:
-##                    dbg('skipping (duplicate) choice validation of field 0')
-                    continue
-####                dbg('checking for choices for field', field._index)
-                start, end = field._extent
-                field_length = end - start
-####                dbg('start, end, length:', start, end, field_length)
-                for choice in field._choices:
-####                    dbg('testing "%s"' % choice)
-                    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))
-                    elif replace_to > end:
-####                        dbg(indent=0)
-                        raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
-####                    dbg(choice, 'valid in field', index)
-
-
-    def _configure(self, mask, **reset_args):
-        """
-        This function sets flags for automatic styling options.  It is
-        called whenever a control or field-level parameter is set/changed.
-
-        This routine does the bulk of the interdependent parameter processing, determining
-        the field extents of the mask if changed, resetting parameters as appropriate,
-        determining the overall template value for the control, etc.
-
-        reset_args is supplied if called from control's .SetCtrlParameters()
-        routine, and indicates which if any parameters which can be
-        overridden by individual fields have been reset by request for the
-        whole control.
-
-        """
-##        dbg(suspend=1)
-##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1)
-
-        # 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._masklength = len(self._mask)
-####        dbg('processed mask:', self._mask)
-
-        # Preserve original mask specified, for subsequent reprocessing
-        # if parameters change.
-##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask)
-        self._previous_mask = mask    # save unexpanded mask for next time
-            # Set expanded mask and extent of field -1 to width of entire control:
-        self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength))
-
-        # Go parse mask to determine where each field is, construct field
-        # instances as necessary, configure them with those extents, and
-        # build lookup table mapping each position for control to its corresponding
-        # field.
-####        dbg('calculating field extents')
-
-        self._calcFieldExtents()
-
-
-        # Go process defaultValues and fillchars to construct the overall
-        # template, and adjust the current value as necessary:
-        reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar']
-        reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue']
-
-####        dbg('calculating template')
-        self._calcTemplate(reset_fillchar, reset_default)
-
-        # Propagate control-level formatting and character constraints to each
-        # field if they don't already have them; if only one field, propagate
-        # control-level validation constraints to field as well:
-####        dbg('propagating constraints')
-        self._propagateConstraints(**reset_args)
-
-
-        if self._isFloat and self._fields[0]._groupChar == self._decimalChar:
-            raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' %
-                                 (self._fields[0]._groupChar, self._decimalChar) )
-
-####        dbg('fields:', indent=1)
-##        for i in [-1] + self._field_indices:
-####            dbg('field %d:' % i, self._fields[i].__dict__)
-####        dbg(indent=0)
-
-        # Set up special parameters for numeric control, if appropriate:
-        if self._signOk:
-            self._signpos = 0   # assume it starts here, but it will move around on floats
-            signkeys = ['-', '+', ' ']
-            if self._useParens:
-                signkeys += ['(', ')']
-            for key in signkeys:
-                keycode = ord(key)
-                if not self._keyhandlers.has_key(keycode):
-                    self._SetKeyHandler(key, self._OnChangeSign)
-
-
-
-        if self._isFloat or self._isInt:
-            if self.controlInitialized:
-                value = self._GetValue()
-####                dbg('value: "%s"' % value, 'len(value):', len(value),
-##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask))
-                if len(value) < len(self._ctrl_constraints._mask):
-                    newvalue = value
-                    if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1:
-                        newvalue += ' '
-                    if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1:
-                        newvalue = ' ' + newvalue
-                    if len(newvalue) < len(self._ctrl_constraints._mask):
-                        if self._ctrl_constraints._alignRight:
-                            newvalue = newvalue.rjust(len(self._ctrl_constraints._mask))
-                        else:
-                            newvalue = newvalue.ljust(len(self._ctrl_constraints._mask))
-##                    dbg('old value: "%s"' % value)
-##                    dbg('new value: "%s"' % newvalue)
-                    try:
-                        self._SetValue(newvalue)
-                    except Exception, e:
-##                        dbg('exception raised:', e, 'resetting to initial value')
-                        self._SetInitialValue()
-
-                elif len(value) > len(self._ctrl_constraints._mask):
-                    newvalue = value
-                    if not self._useParens and newvalue[-1] == ' ':
-                        newvalue = newvalue[:-1]
-                    if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask):
-                        newvalue = newvalue[1:]
-                    if not self._signOk:
-                        newvalue, signpos, right_signpos = self._getSignedValue(newvalue)
-
-##                    dbg('old value: "%s"' % value)
-##                    dbg('new value: "%s"' % newvalue)
-                    try:
-                        self._SetValue(newvalue)
-                    except Exception, e:
-##                        dbg('exception raised:', e, 'resetting to initial value')
-                        self._SetInitialValue()
-                elif not self._signOk and ('(' in value or '-' in value):
-                    newvalue, signpos, right_signpos = self._getSignedValue(value)
-##                    dbg('old value: "%s"' % value)
-##                    dbg('new value: "%s"' % newvalue)
-                    try:
-                        self._SetValue(newvalue)
-                    except e:
-##                        dbg('exception raised:', e, 'resetting to initial value')
-                        self._SetInitialValue()
-
-            # Replace up/down arrow default handling:
-            # make down act like tab, up act like shift-tab:
-
-####            dbg('Registering numeric navigation and control handlers (if not already set)')
-            if not self._keyhandlers.has_key(wx.WXK_DOWN):
-                self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField)
-            if not self._keyhandlers.has_key(wx.WXK_UP):
-                self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric)  # (adds "shift" to up arrow, and calls _OnChangeField)
-
-            # On ., truncate contents right of cursor to decimal point (if any)
-            # leaves cusor after decimal point if floating point, otherwise at 0.
-            if not self._keyhandlers.has_key(ord(self._decimalChar)):
-                self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint)
-            if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)):
-                self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField)   # (Shift-'.' == '>' on US keyboards)
-
-            # Allow selective insert of groupchar in numbers:
-            if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)):
-                self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar)
-
-##        dbg(indent=0, suspend=0)
-
-
-    def _SetInitialValue(self, value=""):
-        """
-        fills the control with the generated or supplied default value.
-        It will also set/reset the font if necessary and apply
-        formatting to the control at this time.
-        """
-##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
-        if not value:
-            self._prevValue = self._curValue = self._template
-            # don't apply external validation rules in this case, as template may
-            # not coincide with "legal" value...
-            try:
-                self._SetValue(self._curValue)  # note the use of "raw" ._SetValue()...
-            except Exception, e:
-##                dbg('exception thrown:', e, indent=0)
-                raise
-        else:
-            # Otherwise apply validation as appropriate to passed value:
-####            dbg('value = "%s", length:' % value, len(value))
-            self._prevValue = self._curValue = value
-            try:
-                self.SetValue(value)            # use public (validating) .SetValue()
-            except Exception, e:
-##                dbg('exception thrown:', e, indent=0)
-                raise
-
-
-        # Set value/type-specific formatting
-        self._applyFormatting()
-##        dbg(indent=0)
-
-
-    def _calcSize(self, size=None):
-        """ Calculate automatic size if allowed; must be called after the base control is instantiated"""
-####        dbg('MaskedEditMixin::_calcSize', indent=1)
-        cont = (size is None or size == wx.DefaultSize)
-
-        if cont and self._autofit:
-            sizing_text = 'M' * self._masklength
-            if wx.Platform != "__WXMSW__":   # give it a little extra space
-                sizing_text += 'M'
-            if wx.Platform == "__WXMAC__":   # give it even a little more...
-                sizing_text += 'M'
-####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
-            w, h = self.GetTextExtent(sizing_text)
-            size = (w+4, self.GetClientSize().height)
-####            dbg('size:', size, indent=0)
-        return size
-
-
-    def _setFont(self):
-        """ Set the control's font typeface -- pass the font name as str."""
-####        dbg('MaskedEditMixin::_setFont', indent=1)
-        if not self._useFixedWidthFont:
-            self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
-        else:
-            font = self.GetFont()   # get size, weight, etc from current font
-
-            # Set to teletype font (guaranteed to be mappable to all wxWidgets
-            # platforms:
-            self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(),
-                                 font.GetWeight(), font.GetUnderlined())
-####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString())
-
-        self.SetFont(self._font)
-####        dbg(indent=0)
-
-
-    def _OnTextChange(self, event):
-        """
-        Handler for EVT_TEXT event.
-        self._Change() is provided for subclasses, and may return False to
-        skip this method logic.  This function returns True if the event
-        detected was a legitimate event, or False if it was a "bogus"
-        EVT_TEXT event.  (NOTE: There is currently an issue with calling
-        .SetValue from within the EVT_CHAR handler that causes duplicate
-        EVT_TEXT events for the same change.)
-        """
-        newvalue = self._GetValue()
-##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1)
-        bValid = False
-        if self._ignoreChange:      # ie. if an "intermediate text change event"
-##            dbg(indent=0)
-            return bValid
-
-        ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue
-        ## call is generating two (2) EVT_TEXT events.
-        ## This is the only mechanism I can find to mask this problem:
-        if newvalue == self._curValue:
-##            dbg('ignoring bogus text change event', indent=0)
-            pass
-        else:
-##            dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue))
-            if self._Change():
-                if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
-##                    dbg('clearing self._isNeg')
-                    self._isNeg = False
-                    text, self._signpos, self._right_signpos = self._getSignedValue()
-                self._CheckValid()  # Recolor control as appropriate
-##            dbg('calling event.Skip()')
-            event.Skip()
-            bValid = True
-        self._prevValue = self._curValue    # save for undo
-        self._curValue = newvalue           # Save last seen value for next iteration
-##        dbg(indent=0)
-        return bValid
-
-
-    def _OnKeyDown(self, event):
-        """
-        This function allows the control to capture Ctrl-events like Ctrl-tab,
-        that are not normally seen by the "cooked" EVT_CHAR routine.
-        """
-        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
-        key    = event.GetKeyCode()
-        if key in self._nav and event.ControlDown():
-            # then this is the only place we will likely see these events;
-            # process them now:
-##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar')
-            self._OnChar(event)
-            return
-        # else allow regular EVT_CHAR key processing
-        event.Skip()
-
-
-    def _OnChar(self, event):
-        """
-        This is the engine of wxMaskedEdit controls.  It examines each keystroke,
-        decides if it's allowed, where it should go or what action to take.
-        """
-##        dbg('MaskedEditMixin::_OnChar', indent=1)
-
-        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
-        key = event.GetKeyCode()
-        orig_pos = self._GetInsertionPoint()
-        orig_value = self._GetValue()
-##        dbg('keycode = ', key)
-##        dbg('current pos = ', orig_pos)
-##        dbg('current selection = ', self._GetSelection())
-
-        if not self._Keypress(key):
-##            dbg(indent=0)
-            return
-
-        # If no format string for this control, or the control is marked as "read-only",
-        # skip the rest of the special processing, and just "do the standard thing:"
-        if not self._mask or not self._IsEditable():
-            event.Skip()
-##            dbg(indent=0)
-            return
-
-        # Process navigation and control keys first, with
-        # position/selection unadulterated:
-        if key in self._nav + self._control:
-            if self._keyhandlers.has_key(key):
-                keep_processing = self._keyhandlers[key](event)
-                if self._GetValue() != orig_value:
-                    self.modified = True
-                if not keep_processing:
-##                    dbg(indent=0)
-                    return
-                self._applyFormatting()
-##                dbg(indent=0)
-                return
-
-        # Else... adjust the position as necessary for next input key,
-        # and determine resulting selection:
-        pos = self._adjustPos( orig_pos, key )    ## get insertion position, adjusted as needed
-        sel_start, sel_to = self._GetSelection()                ## check for a range of selected text
-##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to)
-
-        keep_processing = True
-        # Capture user past end of format field
-        if pos > len(self.maskdict):
-##            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):
-            # there's an override for default behavior; use override function instead
-##            dbg('using supplied key handler:', self._keyhandlers[key])
-            keep_processing = self._keyhandlers[key](event)
-            if self._GetValue() != orig_value:
-                self.modified = True
-            if not keep_processing:
-##                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
-            keep_processing = False
-        else:
-            field = self._FindField(pos)
-##            dbg("key ='%s'" % chr(key))
-            if chr(key) == ' ':
-##                dbg('okSpaces?', field._okSpaces)
-                pass
-
-
-            if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars:
-                keep_processing = False
-
-            if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ):
-##                dbg("key allowed by mask")
-                # insert key into candidate new value, but don't change control yet:
-                oldstr = self._GetValue()
-                newstr, newpos, new_select_to, match_field, match_index = self._insertKey(
-                                chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True)
-##                dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr)
-                if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
-##                    dbg('not valid; checking to see if adjusted string is:')
-                    keep_processing = False
-                    if self._isFloat and newstr != self._template:
-                        newstr = self._adjustFloat(newstr)
-##                        dbg('adjusted str:', newstr)
-                        if self.IsValid(newstr):
-##                            dbg("it is!")
-                            keep_processing = True
-                            wx.CallAfter(self._SetInsertionPoint, self._decimalpos)
-                    if not keep_processing:
-##                        dbg("key disallowed by validation")
-                        if not wx.Validator_IsSilent() and orig_pos == pos:
-                            wx.Bell()
-
-                if keep_processing:
-                    unadjusted = newstr
-
-                    # special case: adjust date value as necessary:
-                    if self._isDate and newstr != self._template:
-                        newstr = self._adjustDate(newstr)
-##                    dbg('adjusted newstr:', newstr)
-
-                    if newstr != orig_value:
-                        self.modified = True
-
-                    wx.CallAfter(self._SetValue, newstr)
-
-                    # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits:
-                    if not self.IsDefault() and self._isDate and self._4digityear:
-                        year2dig = self._dateExtent - 2
-                        if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]:
-                            newpos = pos+2
-
-                    wx.CallAfter(self._SetInsertionPoint, newpos)
-
-                    if match_field is not None:
-##                        dbg('matched field')
-                        self._OnAutoSelect(match_field, match_index)
-
-                    if new_select_to != newpos:
-##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to))
-                        wx.CallAfter(self._SetSelection, newpos, new_select_to)
-                    else:
-                        newfield = self._FindField(newpos)
-                        if newfield != field and newfield._selectOnFieldEntry:
-##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1]))
-                            wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
-                    keep_processing = False
-
-            elif keep_processing:
-##                dbg('char not allowed')
-                keep_processing = False
-                if (not wx.Validator_IsSilent()) and orig_pos == pos:
-                    wx.Bell()
-
-        self._applyFormatting()
-
-        # Move to next insertion point
-        if keep_processing and key not in self._nav:
-            pos = self._GetInsertionPoint()
-            next_entry = self._findNextEntry( pos )
-            if pos != next_entry:
-##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals())
-                wx.CallAfter(self._SetInsertionPoint, next_entry )
-
-            if self._isTemplateChar(pos):
-                self._AdjustField(pos)
-##        dbg(indent=0)
-
-
-    def _FindFieldExtent(self, pos=None, getslice=False, value=None):
-        """ returns editable extent of field corresponding to
-        position pos, and, optionally, the contents of that field
-        in the control or the value specified.
-        Template chars are bound to the preceding field.
-        For masks beginning with template chars, these chars are ignored
-        when calculating the current field.
-
-        Eg: with template (###) ###-####,
-        >>> self._FindFieldExtent(pos=0)
-        1, 4
-        >>> self._FindFieldExtent(pos=1)
-        1, 4
-        >>> self._FindFieldExtent(pos=5)
-        1, 4
-        >>> self._FindFieldExtent(pos=6)
-        6, 9
-        >>> self._FindFieldExtent(pos=10)
-        10, 14
-        etc.
-        """
-##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1)
-
-        field = self._FindField(pos)
-        if not field:
-            if getslice:
-                return None, None, ""
-            else:
-                return None, None
-        edit_start, edit_end = field._extent
-        if getslice:
-            if value is None: value = self._GetValue()
-            slice = value[edit_start:edit_end]
-##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice)
-##            dbg(indent=0)
-            return edit_start, edit_end, slice
-        else:
-##            dbg('edit_start:', edit_start, 'edit_end:', edit_end)
-##            dbg(indent=0)
-            return edit_start, edit_end
-
-
-    def _FindField(self, pos=None):
-        """
-        Returns the field instance in which pos resides.
-        Template chars are bound to the preceding field.
-        For masks beginning with template chars, these chars are ignored
-        when calculating the current field.
-
-        """
-####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
-        if pos is None: pos = self._GetInsertionPoint()
-        elif pos < 0 or pos > self._masklength:
-            raise IndexError('position %s out of range of control' % str(pos))
-
-        if len(self._fields) == 0:
-##            dbg(indent=0)
-            return None
-
-        # else...
-####        dbg(indent=0)
-        return self._fields[self._lookupField[pos]]
-
-
-    def ClearValue(self):
-        """ Blanks the current control value by replacing it with the default value."""
-##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)")
-        self._SetValue( self._template )
-        self._SetInsertionPoint(0)
-        self.Refresh()
-
-
-    def _baseCtrlEventHandler(self, event):
-        """
-        This function is used whenever a key should be handled by the base control.
-        """
-        event.Skip()
-        return False
-
-
-    def _OnUpNumeric(self, event):
-        """
-        Makes up-arrow act like shift-tab should; ie. take you to start of
-        previous field.
-        """
-##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1)
-        event.m_shiftDown = 1
-##        dbg('event.ShiftDown()?', event.ShiftDown())
-        self._OnChangeField(event)
-##        dbg(indent=0)
-
-
-    def _OnArrow(self, event):
-        """
-        Used in response to left/right navigation keys; makes these actions skip
-        over mask template chars.
-        """
-##        dbg("MaskedEditMixin::_OnArrow", indent=1)
-        pos = self._GetInsertionPoint()
-        keycode = event.GetKeyCode()
-        sel_start, sel_to = self._GetSelection()
-        entry_end = self._goEnd(getPosOnly=True)
-        if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN):
-            if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
-                or ( self._isTemplateChar(pos) and pos >= entry_end) ):
-##                dbg("can't advance", indent=0)
-                return False
-            elif self._isTemplateChar(pos):
-                self._AdjustField(pos)
-        elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1):
-##            dbg('adjusting field')
-            self._AdjustField(pos)
-
-        # treat as shifted up/down arrows as tab/reverse tab:
-        if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN):
-            # remove "shifting" and treat as (forward) tab:
-            event.m_shiftDown = False
-            keep_processing = self._OnChangeField(event)
-
-        elif self._FindField(pos)._selectOnFieldEntry:
-            if( keycode in (wx.WXK_UP, wx.WXK_LEFT)
-                and sel_start != 0
-                and self._isTemplateChar(sel_start-1)
-                and sel_start != self._masklength
-                and not self._signOk and not self._useParens):
-
-                # call _OnChangeField to handle "ctrl-shifted event"
-                # (which moves to previous field and selects it.)
-                event.m_shiftDown = True
-                event.m_ControlDown = True
-                keep_processing = self._OnChangeField(event)
-            elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT)
-                  and sel_to != self._masklength
-                  and self._isTemplateChar(sel_to)):
-
-                # when changing field to the right, ensure don't accidentally go left instead
-                event.m_shiftDown = False
-                keep_processing = self._OnChangeField(event)
-            else:
-                # treat arrows as normal, allowing selection
-                # as appropriate:
-##                dbg('using base ctrl event processing')
-                event.Skip()
-        else:
-            if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT)
-                or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ):
-                if not wx.Validator_IsSilent():
-                    wx.Bell()
-            else:
-                # treat arrows as normal, allowing selection
-                # as appropriate:
-##                dbg('using base event processing')
-                event.Skip()
-
-        keep_processing = False
-##        dbg(indent=0)
-        return keep_processing
-
-
-    def _OnCtrl_S(self, event):
-        """ Default Ctrl-S handler; prints value information if demo enabled. """
-##        dbg("MaskedEditMixin::_OnCtrl_S")
-        if self._demo:
-            print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue())
-            print "Valid? => " + str(self.IsValid())
-            print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True))
-        return False
-
-
-    def _OnCtrl_X(self, event=None):
-        """ Handles ctrl-x keypress in control and Cut operation on context menu.
-            Should return False to skip other processing. """
-##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1)
-        self.Cut()
-##        dbg(indent=0)
-        return False
-
-    def _OnCtrl_C(self, event=None):
-        """ Handles ctrl-C keypress in control and Copy operation on context menu.
-            Uses base control handling. Should return False to skip other processing."""
-        self.Copy()
-        return False
-
-    def _OnCtrl_V(self, event=None):
-        """ Handles ctrl-V keypress in control and Paste operation on context menu.
-            Should return False to skip other processing. """
-##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
-        self.Paste()
-##        dbg(indent=0)
-        return False
-
-    def _OnCtrl_Z(self, event=None):
-        """ Handles ctrl-Z keypress in control and Undo operation on context menu.
-            Should return False to skip other processing. """
-##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1)
-        self.Undo()
-##        dbg(indent=0)
-        return False
-
-    def _OnCtrl_A(self,event=None):
-        """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
-        end = self._goEnd(getPosOnly=True)
-        if not event or event.ShiftDown():
-            wx.CallAfter(self._SetInsertionPoint, 0)
-            wx.CallAfter(self._SetSelection, 0, self._masklength)
-        else:
-            wx.CallAfter(self._SetInsertionPoint, 0)
-            wx.CallAfter(self._SetSelection, 0, end)
-        return False
-
-
-    def _OnErase(self, event=None):
-        """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
-##        dbg("MaskedEditMixin::_OnErase", indent=1)
-        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
-
-        if event is None:   # called as action routine from Cut() operation.
-            key = wx.WXK_DELETE
-        else:
-            key = event.GetKeyCode()
-
-        field = self._FindField(sel_to)
-        start, end = field._extent
-        value = self._GetValue()
-        oldstart = sel_start
-
-        # If trying to erase beyond "legal" bounds, disallow operation:
-        if( (sel_to == 0 and key == wx.WXK_BACK)
-            or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK)
-            or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight)
-            or (self._signOk and self._useParens
-                and sel_start == sel_to
-                and sel_to == self._masklength - 1
-                and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ):
-            if not wx.Validator_IsSilent():
-                wx.Bell()
-##            dbg(indent=0)
-            return False
-
-
-        if( field._insertRight                                  # an insert-right field
-            and value[start:end] != self._template[start:end]   # and field not empty
-            and sel_start >= start                              # and selection starts in field
-            and ((sel_to == sel_start                           # and no selection
-                  and sel_to == end                             # and cursor at right edge
-                  and key in (wx.WXK_BACK, wx.WXK_DELETE))            # and either delete or backspace key
-                 or                                             # or
-                 (key == wx.WXK_BACK                               # backspacing
-                    and (sel_to == end                          # and selection ends at right edge
-                         or sel_to < end and field._allowInsert)) ) ):  # or allow right insert at any point in field
-
-##            dbg('delete left')
-            # if backspace but left of cursor is empty, adjust cursor right before deleting
-            while( key == wx.WXK_BACK
-                   and sel_start == sel_to
-                   and sel_start < end
-                   and value[start:sel_start] == self._template[start:sel_start]):
-                sel_start += 1
-                sel_to = sel_start
-
-##            dbg('sel_start, start:', sel_start, start)
-
-            if sel_start == sel_to:
-                keep = sel_start -1
-            else:
-                keep = sel_start
-            newfield = value[start:keep] + value[sel_to:end]
-
-            # handle sign char moving from outside field into the field:
-            move_sign_into_field = False
-            if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('):
-                signchar = value[0]
-                newfield = signchar + newfield
-                move_sign_into_field = True
-##            dbg('cut newfield: "%s"' % newfield)
-
-            # handle what should fill in from the left:
-            left = ""
-            for i in range(start, end - len(newfield)):
-                if field._padZero:
-                    left += '0'
-                elif( self._signOk and self._isNeg and i == 1
-                      and ((self._useParens and newfield.find('(') == -1)
-                           or (not self._useParens and newfield.find('-') == -1)) ):
-                    left += ' '
-                else:
-                    left += self._template[i]   # this can produce strange results in combination with default values...
-            newfield = left + newfield
-##            dbg('filled newfield: "%s"' % newfield)
-
-            newstr = value[:start] + newfield + value[end:]
-
-            # (handle sign located in "mask position" in front of field prior to delete)
-            if move_sign_into_field:
-                newstr = ' ' + newstr[1:]
-            pos = sel_to
-        else:
-            # handle erasure of (left) sign, moving selection accordingly...
-            if self._signOk and sel_start == 0:
-                newstr = value = ' ' + value[1:]
-                sel_start += 1
-
-            if field._allowInsert and sel_start >= start:
-                # selection (if any) falls within current insert-capable field:
-                select_len = sel_to - sel_start
-                # determine where cursor should end up:
-                if key == wx.WXK_BACK:
-                    if select_len == 0:
-                        newpos = sel_start -1
-                    else:
-                        newpos = sel_start
-                    erase_to = sel_to
-                else:
-                    newpos = sel_start
-                    if sel_to == sel_start:
-                        erase_to = sel_to + 1
-                    else:
-                        erase_to = sel_to
-
-                if self._isTemplateChar(newpos) and select_len == 0:
-                    if self._signOk:
-                        if value[newpos] in ('(', '-'):
-                            newpos += 1     # don't move cusor
-                            newstr = ' ' + value[newpos:]
-                        elif value[newpos] == ')':
-                            # erase right sign, but don't move cursor; (matching left sign handled later)
-                            newstr = value[:newpos] + ' '
-                        else:
-                            # no deletion; just move cursor
-                            newstr = value
-                    else:
-                        # no deletion; just move cursor
-                        newstr = value
-                else:
-                    if erase_to > end: erase_to = end
-                    erase_len = erase_to - newpos
-
-                    left = value[start:newpos]
-##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end])
-                    right = value[erase_to:end] + self._template[end-erase_len:end]
-                    pos_adjust = 0
-                    if field._alignRight:
-                        rstripped = right.rstrip()
-                        if rstripped != right:
-                            pos_adjust = len(right) - len(rstripped)
-                        right = rstripped
-
-                    if not field._insertRight and value[-1] == ')' and end == self._masklength - 1:
-                        # need to shift ) into the field:
-                        right = right[:-1] + ')'
-                        value = value[:-1] + ' '
-
-                    newfield = left+right
-                    if pos_adjust:
-                        newfield = newfield.rjust(end-start)
-                        newpos += pos_adjust
-##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield))
-                    newstr = value[:start] + newfield + value[end:]
-
-                pos = newpos
-
-            else:
-                if sel_start == sel_to:
-##                    dbg("current sel_start, sel_to:", sel_start, sel_to)
-                    if key == wx.WXK_BACK:
-                        sel_start, sel_to = sel_to-1, sel_to-1
-##                        dbg("new sel_start, sel_to:", sel_start, sel_to)
-
-                    if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''):
-                        # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0:
-                        newchar = '0'
-                    else:
-                        newchar = self._template[sel_to] ## get an original template character to "clear" the current char
-##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start]))
-
-                    if self._isTemplateChar(sel_to):
-                        if sel_to == 0 and self._signOk and value[sel_to] == '-':   # erasing "template" sign char
-                            newstr = ' ' + value[1:]
-                            sel_to += 1
-                        elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('):
-                            # allow "change sign" by removing both parens:
-                            newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' '
-                        else:
-                            newstr = value
-                        newpos = sel_to
-                    else:
-                        if field._insertRight and sel_start == sel_to:
-                            # force non-insert-right behavior, by selecting char to be replaced:
-                            sel_to += 1
-                        newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value)
-
-                else:
-                    # selection made
-                    newstr = self._eraseSelection(value, sel_start, sel_to)
-
-                pos = sel_start  # put cursor back at beginning of selection
-
-        if self._signOk and self._useParens:
-            # account for resultant unbalanced parentheses:
-            left_signpos = newstr.find('(')
-            right_signpos = newstr.find(')')
-
-            if left_signpos == -1 and right_signpos != -1:
-                # erased left-sign marker; get rid of right sign marker:
-                newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:]
-
-            elif left_signpos != -1 and right_signpos == -1:
-                # erased right-sign marker; get rid of left-sign marker:
-                newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:]
-
-##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart)
-##        dbg("newstr:'%s'" % newstr, 'pos:', pos)
-
-        # if erasure results in an invalid field, disallow it:
-##        dbg('field._validRequired?', field._validRequired)
-##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end]))
-        if field._validRequired and not field.IsValid(newstr[start:end]):
-            if not wx.Validator_IsSilent():
-                wx.Bell()
-##            dbg(indent=0)
-            return False
-
-        # if erasure results in an invalid value, disallow it:
-        if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
-            if not wx.Validator_IsSilent():
-                wx.Bell()
-##            dbg(indent=0)
-            return False
-
-##        dbg('setting value (later) to', newstr)
-        wx.CallAfter(self._SetValue, newstr)
-##        dbg('setting insertion point (later) to', pos)
-        wx.CallAfter(self._SetInsertionPoint, pos)
-##        dbg(indent=0)
-        if newstr != value:
-            self.modified = True
-        return False
-
-
-    def _OnEnd(self,event):
-        """ Handles End keypress in control. Should return False to skip other processing. """
-##        dbg("MaskedEditMixin::_OnEnd", indent=1)
-        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-        if not event.ControlDown():
-            end = self._masklength  # go to end of control
-            if self._signOk and self._useParens:
-                end = end - 1       # account for reserved char at end
-        else:
-            end_of_input = self._goEnd(getPosOnly=True)
-            sel_start, sel_to = self._GetSelection()
-            if sel_to < pos: sel_to = pos
-            field = self._FindField(sel_to)
-            field_end = self._FindField(end_of_input)
-
-            # pick different end point if either:
-            # - cursor not in same field
-            # - or at or past last input already
-            # - or current selection = end of current field:
-####            dbg('field != field_end?', field != field_end)
-####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input)
-            if field != field_end or sel_to >= end_of_input:
-                edit_start, edit_end = field._extent
-####                dbg('edit_end:', edit_end)
-####                dbg('sel_to:', sel_to)
-####                dbg('sel_to == edit_end?', sel_to == edit_end)
-####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1])
-
-                if sel_to == edit_end and field._index < self._field_indices[-1]:
-                    edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end))  # go to end of next field:
-                    end = edit_end
-##                    dbg('end moved to', end)
-
-                elif sel_to == edit_end and field._index == self._field_indices[-1]:
-                    # already at edit end of last field; select to end of control:
-                    end = self._masklength
-##                    dbg('end moved to', end)
-                else:
-                    end = edit_end  # select to end of current field
-##                    dbg('end moved to ', end)
-            else:
-                # select to current end of input
-                end = end_of_input
-
-
-####        dbg('pos:', pos, 'end:', end)
-
-        if event.ShiftDown():
-            if not event.ControlDown():
-##                dbg("shift-end; select to end of control")
-                pass
-            else:
-##                dbg("shift-ctrl-end; select to end of non-whitespace")
-                pass                
-            wx.CallAfter(self._SetInsertionPoint, pos)
-            wx.CallAfter(self._SetSelection, pos, end)
-        else:
-            if not event.ControlDown():
-##                dbg('go to end of control:')
-                pass
-            wx.CallAfter(self._SetInsertionPoint, end)
-            wx.CallAfter(self._SetSelection, end, end)
-
-##        dbg(indent=0)
-        return False
-
-
-    def _OnReturn(self, event):
-         """
-         Changes the event to look like a tab event, so we can then call
-         event.Skip() on it, and have the parent form "do the right thing."
-         """
-##         dbg('MaskedEditMixin::OnReturn')
-         event.m_keyCode = wx.WXK_TAB
-         event.Skip()
-
-
-    def _OnHome(self,event):
-        """ Handles Home keypress in control. Should return False to skip other processing."""
-##        dbg("MaskedEditMixin::_OnHome", indent=1)
-        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-        sel_start, sel_to = self._GetSelection()
-
-        # There are 5 cases here:
-
-        # 1) shift: select from start of control to end of current
-        #    selection.
-        if event.ShiftDown() and not event.ControlDown():
-##            dbg("shift-home; select to start of control")
-            start = 0
-            end = sel_start
-
-        # 2) no shift, no control: move cursor to beginning of control.
-        elif not event.ControlDown():
-##            dbg("home; move to start of control")
-            start = 0
-            end = 0
-
-        # 3) No shift, control: move cursor back to beginning of field; if
-        #    there already, go to beginning of previous field.
-        # 4) shift, control, start of selection not at beginning of control:
-        #    move sel_start back to start of field; if already there, go to
-        #    start of previous field.
-        elif( event.ControlDown()
-              and (not event.ShiftDown()
-                   or (event.ShiftDown() and sel_start > 0) ) ):
-            if len(self._field_indices) > 1:
-                field = self._FindField(sel_start)
-                start, ignore = field._extent
-                if sel_start == start and field._index != self._field_indices[0]:  # go to start of previous field:
-                    start, ignore = self._FindFieldExtent(sel_start-1)
-                elif sel_start == start:
-                    start = 0   # go to literal beginning if edit start
-                                # not at that point
-                end_of_field = True
-
-            else:
-                start = 0
-
-            if not event.ShiftDown():
-##                dbg("ctrl-home; move to beginning of field")
-                end = start
-            else:
-##                dbg("shift-ctrl-home; select to beginning of field")
-                end = sel_to
-
-        else:
-        # 5) shift, control, start of selection at beginning of control:
-        #    unselect by moving sel_to backward to beginning of current field;
-        #    if already there, move to start of previous field.
-            start = sel_start
-            if len(self._field_indices) > 1:
-                # find end of previous field:
-                field = self._FindField(sel_to)
-                if sel_to > start and field._index != self._field_indices[0]:
-                    ignore, end = self._FindFieldExtent(field._extent[0]-1)
-                else:
-                    end = start
-                end_of_field = True
-            else:
-                end = start
-                end_of_field = False
-##            dbg("shift-ctrl-home; unselect to beginning of field")
-
-##        dbg('queuing new sel_start, sel_to:', (start, end))
-        wx.CallAfter(self._SetInsertionPoint, start)
-        wx.CallAfter(self._SetSelection, start, end)
-##        dbg(indent=0)
-        return False
-
-
-    def _OnChangeField(self, event):
-        """
-        Primarily handles TAB events, but can be used for any key that
-        designer wants to change fields within a masked edit control.
-        NOTE: at the moment, although coded to handle shift-TAB and
-        control-shift-TAB, these events are not sent to the controls
-        by the framework.
-        """
-##        dbg('MaskedEditMixin::_OnChangeField', indent = 1)
-        # determine end of current field:
-        pos = self._GetInsertionPoint()
-##        dbg('current pos:', pos)
-        sel_start, sel_to = self._GetSelection()
-
-        if self._masklength < 0:   # no fields; process tab normally
-            self._AdjustField(pos)
-            if event.GetKeyCode() == wx.WXK_TAB:
-##                dbg('tab to next ctrl')
-                event.Skip()
-            #else: do nothing
-##            dbg(indent=0)
-            return False
-
-
-        if event.ShiftDown():
-
-            # "Go backward"
-
-            # NOTE: doesn't yet work with SHIFT-tab under wx; the control
-            # never sees this event! (But I've coded for it should it ever work,
-            # and it *does* work for '.' in IpAddrCtrl.)
-            field = self._FindField(pos)
-            index = field._index
-            field_start = field._extent[0]
-            if pos < field_start:
-##                dbg('cursor before 1st field; cannot change to a previous field')
-                if not wx.Validator_IsSilent():
-                    wx.Bell()
-                return False
-
-            if event.ControlDown():
-##                dbg('queuing select to beginning of field:', field_start, pos)
-                wx.CallAfter(self._SetInsertionPoint, field_start)
-                wx.CallAfter(self._SetSelection, field_start, pos)
-##                dbg(indent=0)
-                return False
-
-            elif index == 0:
-                  # We're already in the 1st field; process shift-tab normally:
-                self._AdjustField(pos)
-                if event.GetKeyCode() == wx.WXK_TAB:
-##                    dbg('tab to previous ctrl')
-                    event.Skip()
-                else:
-##                    dbg('position at beginning')
-                    wx.CallAfter(self._SetInsertionPoint, field_start)
-##                dbg(indent=0)
-                return False
-            else:
-                # find beginning of previous field:
-                begin_prev = self._FindField(field_start-1)._extent[0]
-                self._AdjustField(pos)
-##                dbg('repositioning to', begin_prev)
-                wx.CallAfter(self._SetInsertionPoint, begin_prev)
-                if self._FindField(begin_prev)._selectOnFieldEntry:
-                    edit_start, edit_end = self._FindFieldExtent(begin_prev)
-##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end))
-                    wx.CallAfter(self._SetInsertionPoint, edit_start)
-                    wx.CallAfter(self._SetSelection, edit_start, edit_end)
-##                dbg(indent=0)
-                return False
-
-        else:
-            # "Go forward"
-            field = self._FindField(sel_to)
-            field_start, field_end = field._extent
-            if event.ControlDown():
-##                dbg('queuing select to end of field:', pos, field_end)
-                wx.CallAfter(self._SetInsertionPoint, pos)
-                wx.CallAfter(self._SetSelection, pos, field_end)
-##                dbg(indent=0)
-                return False
-            else:
-                if pos < field_start:
-##                    dbg('cursor before 1st field; go to start of field')
-                    wx.CallAfter(self._SetInsertionPoint, field_start)
-                    if field._selectOnFieldEntry:
-                        wx.CallAfter(self._SetSelection, field_start, field_end)
-                    else:
-                        wx.CallAfter(self._SetSelection, field_start, field_start)
-                    return False
-                # else...
-##                dbg('end of current field:', field_end)
-##                dbg('go to next field')
-                if field_end == self._fields[self._field_indices[-1]]._extent[1]:
-                    self._AdjustField(pos)
-                    if event.GetKeyCode() == wx.WXK_TAB:
-##                        dbg('tab to next ctrl')
-                        event.Skip()
-                    else:
-##                        dbg('position at end')
-                        wx.CallAfter(self._SetInsertionPoint, field_end)
-##                    dbg(indent=0)
-                    return False
-                else:
-                    # we have to find the start of the next field
-                    next_pos = self._findNextEntry(field_end)
-                    if next_pos == field_end:
-##                        dbg('already in last field')
-                        self._AdjustField(pos)
-                        if event.GetKeyCode() == wx.WXK_TAB:
-##                            dbg('tab to next ctrl')
-                            event.Skip()
-                        #else: do nothing
-##                        dbg(indent=0)
-                        return False
-                    else:
-                        self._AdjustField( pos )
-
-                        # move cursor to appropriate point in the next field and select as necessary:
-                        field = self._FindField(next_pos)
-                        edit_start, edit_end = field._extent
-                        if field._selectOnFieldEntry:
-##                            dbg('move to ', next_pos)
-                            wx.CallAfter(self._SetInsertionPoint, next_pos)
-                            edit_start, edit_end = self._FindFieldExtent(next_pos)
-##                            dbg('queuing select', edit_start, edit_end)
-                            wx.CallAfter(self._SetSelection, edit_start, edit_end)
-                        else:
-                            if field._insertRight:
-                                next_pos = field._extent[1]
-##                            dbg('move to ', next_pos)
-                            wx.CallAfter(self._SetInsertionPoint, next_pos)
-##                        dbg(indent=0)
-                        return False
-
-
-    def _OnDecimalPoint(self, event):
-##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
-
-        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-
-        if self._isFloat:       ## handle float value, move to decimal place
-##            dbg('key == Decimal tab; decimal pos:', self._decimalpos)
-            value = self._GetValue()
-            if pos < self._decimalpos:
-                clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:]
-##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
-                newstr = self._adjustFloat(clipped_text)
-            else:
-                newstr = self._adjustFloat(value)
-            wx.CallAfter(self._SetValue, newstr)
-            fraction = self._fields[1]
-            start, end = fraction._extent
-            wx.CallAfter(self._SetInsertionPoint, start)
-            if fraction._selectOnFieldEntry:
-##                dbg('queuing selection after decimal point to:', (start, end))
-                wx.CallAfter(self._SetSelection, start, end)
-            keep_processing = False
-
-        if self._isInt:      ## handle integer value, truncate from current position
-##            dbg('key == Integer decimal event')
-            value = self._GetValue()
-            clipped_text = value[0:pos]
-##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
-            newstr = self._adjustInt(clipped_text)
-##            dbg('newstr: "%s"' % newstr)
-            wx.CallAfter(self._SetValue, newstr)
-            newpos = len(newstr.rstrip())
-            if newstr.find(')') != -1:
-                newpos -= 1     # (don't move past right paren)
-            wx.CallAfter(self._SetInsertionPoint, newpos)
-            keep_processing = False
-##        dbg(indent=0)
-
-
-    def _OnChangeSign(self, event):
-##        dbg('MaskedEditMixin::_OnChangeSign', indent=1)
-        key = event.GetKeyCode()
-        pos = self._adjustPos(self._GetInsertionPoint(), key)
-        value = self._eraseSelection()
-        integer = self._fields[0]
-        start, end = integer._extent
-
-####        dbg('adjusted pos:', pos)
-        if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos):
-            cursign = self._isNeg
-##            dbg('cursign:', cursign)
-            if chr(key) in ('-','(', ')'):
-                self._isNeg = (not self._isNeg)   ## flip value
-            else:
-                self._isNeg = False
-##            dbg('isNeg?', self._isNeg)
-
-            text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value)
-##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos)
-            if text is None:
-                text = value
-
-            if self._isNeg and self._signpos is not None and self._signpos != -1:
-                if self._useParens and self._right_signpos is not None:
-                    text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:]
-                else:
-                    text = text[:self._signpos] + '-' + text[self._signpos+1:]
-            else:
-####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text))
-                if self._useParens:
-                    text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:]
-                else:
-                    text = text[:self._signpos] + ' ' + text[self._signpos+1:]
-##                dbg('clearing self._isNeg')
-                self._isNeg = False
-
-            wx.CallAfter(self._SetValue, text)
-            wx.CallAfter(self._applyFormatting)
-##            dbg('pos:', pos, 'signpos:', self._signpos)
-            if pos == self._signpos or integer.IsEmpty(text[start:end]):
-                wx.CallAfter(self._SetInsertionPoint, self._signpos+1)
-            else:
-                wx.CallAfter(self._SetInsertionPoint, pos)
-
-            keep_processing = False
-        else:
-            keep_processing = True
-##        dbg(indent=0)
-        return keep_processing
-
-
-    def _OnGroupChar(self, event):
-        """
-        This handler is only registered if the mask is a numeric mask.
-        It allows the insertion of ',' or '.' if appropriate.
-        """
-##        dbg('MaskedEditMixin::_OnGroupChar', indent=1)
-        keep_processing = True
-        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-        sel_start, sel_to = self._GetSelection()
-        groupchar = self._fields[0]._groupChar
-        if not self._isCharAllowed(groupchar, pos, checkRegex=True):
-            keep_processing = False
-            if not wx.Validator_IsSilent():
-                wx.Bell()
-
-        if keep_processing:
-            newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() )
-##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr)
-            if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
-                keep_processing = False
-                if not wx.Validator_IsSilent():
-                        wx.Bell()
-
-        if keep_processing:
-            wx.CallAfter(self._SetValue, newstr)
-            wx.CallAfter(self._SetInsertionPoint, newpos)
-        keep_processing = False
-##        dbg(indent=0)
-        return keep_processing
-
-
-    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
-            adjustInsert = adjustInsert
-        else:                           # else within a field; flag not relevant
-            adjustInsert = False
-
-        while self._isTemplateChar(pos) and pos < self._masklength:
-            pos += 1
-
-        # if changing fields, and we've been told to adjust insert point,
-        # look at new field; if empty and right-insert field,
-        # adjust to right edge:
-        if adjustInsert and pos < self._masklength:
-            field = self._FindField(pos)
-            start, end = field._extent
-            slice = self._GetValue()[start:end]
-            if field._insertRight and field.IsEmpty(slice):
-                pos = end
-        return pos
-
-
-    def _findNextTemplateChar(self, pos):
-        """ Find the position of the next non-editable character in the mask."""
-        while not self._isTemplateChar(pos) and pos < self._masklength:
-            pos += 1
-        return pos
-
-
-    def _OnAutoCompleteField(self, event):
-##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1)
-        pos = self._GetInsertionPoint()
-        field = self._FindField(pos)
-        edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
-
-        match_index = None
-        keycode = event.GetKeyCode()
-
-        if field._fillChar != ' ':
-            text = slice.replace(field._fillChar, '')
-        else:
-            text = slice
-        text = text.strip()
-        keep_processing = True  # (assume True to start)
-##        dbg('field._hasList?', field._hasList)
-        if field._hasList:
-##            dbg('choices:', field._choices)
-##            dbg('compareChoices:', field._compareChoices)
-            choices, choice_required = field._compareChoices, field._choiceRequired
-            if keycode in (wx.WXK_PRIOR, wx.WXK_UP):
-                direction = -1
-            else:
-                direction = 1
-            match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex)
-            if( match_index is None
-                and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
-                     or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ):
-                # Select the 1st thing from the list:
-                match_index = 0
-
-            if( match_index is not None
-                and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
-                      or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown())
-                      or (keycode == wx.WXK_DOWN and partial_match) ) ):
-
-                # We're allowed to auto-complete:
-##                dbg('match found')
-                value = self._GetValue()
-                newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:]
-##                dbg('setting value to "%s"' % newvalue)
-                self._SetValue(newvalue)
-                self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip())))
-                self._OnAutoSelect(field, match_index)
-                self._CheckValid()  # recolor as appopriate
-
-
-        if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT):
-            # treat as left right arrow if unshifted, tab/shift tab if shifted.
-            if event.ShiftDown():
-                if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT):
-                    # remove "shifting" and treat as (forward) tab:
-                    event.m_shiftDown = False
-                keep_processing = self._OnChangeField(event)
-            else:
-                keep_processing = self._OnArrow(event)
-        # else some other key; keep processing the key
-
-##        dbg('keep processing?', keep_processing, indent=0)
-        return keep_processing
-
-
-    def _OnAutoSelect(self, field, match_index = None):
-        """
-        Function called if autoselect feature is enabled and entire control
-        is selected:
-        """
-##        dbg('MaskedEditMixin::OnAutoSelect', field._index)
-        if match_index is not None:
-            field._autoCompleteIndex = match_index
-
-
-    def _autoComplete(self, direction, choices, value, compareNoCase, current_index):
-        """
-        This function gets called in response to Auto-complete events.
-        It attempts to find a match to the specified value against the
-        list of choices; if exact match, the index of then next
-        appropriate value in the list, based on the given direction.
-        If not an exact match, it will return the index of the 1st value from
-        the choice list for which the partial value can be extended to match.
-        If no match found, it will return None.
-        The function returns a 2-tuple, with the 2nd element being a boolean
-        that indicates if partial match was necessary.
-        """
-##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1)
-        if value is None:
-##            dbg('nothing to match against', indent=0)
-            return (None, False)
-
-        partial_match = False
-
-        if compareNoCase:
-            value = value.lower()
-
-        last_index = len(choices) - 1
-        if value in choices:
-##            dbg('"%s" in', choices)
-            if current_index is not None and choices[current_index] == value:
-                index = current_index
-            else:
-                index = choices.index(value)
-
-##            dbg('matched "%s" (%d)' % (choices[index], index))
-            if direction == -1:
-##                dbg('going to previous')
-                if index == 0: index = len(choices) - 1
-                else: index -= 1
-            else:
-                if index == len(choices) - 1: index = 0
-                else: index += 1
-##            dbg('change value to "%s" (%d)' % (choices[index], index))
-            match = index
-        else:
-            partial_match = True
-            value = value.strip()
-##            dbg('no match; try to auto-complete:')
-            match = None
-##            dbg('searching for "%s"' % value)
-            if current_index is None:
-                indices = range(len(choices))
-                if direction == -1:
-                    indices.reverse()
-            else:
-                if direction == 1:
-                    indices = range(current_index +1, len(choices)) + range(current_index+1)
-##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices)
-                else:
-                    indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1)
-##                    dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices)
-####            dbg('indices:', indices)
-            for index in indices:
-                choice = choices[index]
-                if choice.find(value, 0) == 0:
-##                    dbg('match found:', choice)
-                    match = index
-                    break
-                else: dbg('choice: "%s" - no match' % choice)
-            if match is not None:
-##                dbg('matched', match)
-                pass
-            else:
-##                dbg('no match found')
-                pass
-##        dbg(indent=0)
-        return (match, partial_match)
-
-
-    def _AdjustField(self, pos):
-        """
-        This function gets called by default whenever the cursor leaves a field.
-        The pos argument given is the char position before leaving that field.
-        By default, floating point, integer and date values are adjusted to be
-        legal in this function.  Derived classes may override this function
-        to modify the value of the control in a different way when changing fields.
-
-        NOTE: these change the value immediately, and restore the cursor to
-        the passed location, so that any subsequent code can then move it
-        based on the operation being performed.
-        """
-        newvalue = value = self._GetValue()
-        field = self._FindField(pos)
-        start, end, slice = self._FindFieldExtent(getslice=True)
-        newfield = field._AdjustField(slice)
-        newvalue = value[:start] + newfield + value[end:]
-
-        if self._isFloat and newvalue != self._template:
-            newvalue = self._adjustFloat(newvalue)
-
-        if self._ctrl_constraints._isInt and value != self._template:
-            newvalue = self._adjustInt(value)
-
-        if self._isDate and value != self._template:
-            newvalue = self._adjustDate(value, fixcentury=True)
-            if self._4digityear:
-                year2dig = self._dateExtent - 2
-                if pos == year2dig and value[year2dig] != newvalue[year2dig]:
-                    pos = pos+2
-
-        if newvalue != value:
-            self._SetValue(newvalue)
-            self._SetInsertionPoint(pos)
-
-
-    def _adjustKey(self, pos, key):
-        """ Apply control formatting to the key (e.g. convert to upper etc). """
-        field = self._FindField(pos)
-        if field._forceupper and key in range(97,123):
-            key = ord( chr(key).upper())
-
-        if field._forcelower and key in range(97,123):
-            key = ord( chr(key).lower())
-
-        return key
-
-
-    def _adjustPos(self, pos, key):
-        """
-        Checks the current insertion point position and adjusts it if
-        necessary to skip over non-editable characters.
-        """
-##        dbg('_adjustPos', pos, key, indent=1)
-        sel_start, sel_to = self._GetSelection()
-        # If a numeric or decimal mask, and negatives allowed, reserve the
-        # first space for sign, and last one if using parens.
-        if( self._signOk
-            and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) )
-                 or self._useParens and pos == self._masklength -1)):
-##            dbg('adjusted pos:', pos, indent=0)
-            return pos
-
-        if key not in self._nav:
-            field = self._FindField(pos)
-
-##            dbg('field._insertRight?', field._insertRight)
-            if field._insertRight:              # if allow right-insert
-                start, end = field._extent
-                slice = self._GetValue()[start:end].strip()
-                field_len = end - start
-                if pos == end:                      # if cursor at right edge of field
-                    # if not filled or supposed to stay in field, keep current position
-####                    dbg('pos==end')
-####                    dbg('len (slice):', len(slice))
-####                    dbg('field_len?', field_len)
-####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
-####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
-                    if len(slice) == field_len and field._moveOnFieldFull:
-                        # move cursor to next field:
-                        pos = self._findNextEntry(pos)
-                        self._SetInsertionPoint(pos)
-                        if pos < sel_to:
-                            self._SetSelection(pos, sel_to)     # restore selection
-                        else:
-                            self._SetSelection(pos, pos)        # remove selection
-                    else: # leave cursor alone
-                        pass
-                else:
-                    # if at start of control, move to right edge
-                    if sel_to == sel_start and self._isTemplateChar(pos) and pos != end:
-                        pos = end                   # move to right edge
-##                    elif sel_start <= start and sel_to == end:
-##                        # select to right edge of field - 1 (to replace char)
-##                        pos = end - 1
-##                        self._SetInsertionPoint(pos)
-##                        # restore selection
-##                        self._SetSelection(sel_start, pos)
-
-                    elif self._signOk and sel_start == 0:   # if selected to beginning and signed,
-                        # adjust to past reserved sign position:
-                        pos = self._fields[0]._extent[0]
-                        self._SetInsertionPoint(pos)
-                        # restore selection
-                        self._SetSelection(pos, sel_to)
-                    else:
-                        pass    # leave position/selection alone
-
-            # else make sure the user is not trying to type over a template character
-            # If they are, move them to the next valid entry position
-            elif self._isTemplateChar(pos):
-                if( not field._moveOnFieldFull
-                      and (not self._signOk
-                           or (self._signOk
-                               and field._index == 0
-                               and pos > 0) ) ):      # don't move to next field without explicit cursor movement
-                    pass
-                else:
-                    # find next valid position
-                    pos = self._findNextEntry(pos)
-                    self._SetInsertionPoint(pos)
-                    if pos < sel_to:    # restore selection
-                        self._SetSelection(pos, sel_to)
-##        dbg('adjusted pos:', pos, indent=0)
-        return pos
-
-
-    def _adjustFloat(self, candidate=None):
-        """
-        'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
-        """
-##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1)
-        lenInt,lenFraction  = [len(s) for s in self._mask.split('.')]  ## Get integer, fraction lengths
-
-        if candidate is None: value = self._GetValue()
-        else: value = candidate
-##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
-        intStr, fracStr = value.split(self._decimalChar)
-
-        intStr = self._fields[0]._AdjustField(intStr)
-##        dbg('adjusted intStr: "%s"' % intStr)
-        lenInt = len(intStr)
-        fracStr = fracStr + ('0'*(lenFraction-len(fracStr)))  # add trailing spaces to decimal
-
-##        dbg('intStr "%(intStr)s"' % locals())
-##        dbg('lenInt:', lenInt)
-
-        intStr = string.rjust( intStr[-lenInt:], lenInt)
-##        dbg('right-justifed intStr = "%(intStr)s"' % locals())
-        newvalue = intStr + self._decimalChar + fracStr
-
-        if self._signOk:
-            if len(newvalue) < self._masklength:
-                newvalue = ' ' + newvalue
-            signedvalue = self._getSignedValue(newvalue)[0]
-            if signedvalue is not None: newvalue = signedvalue
-
-        # Finally, align string with decimal position, left-padding with
-        # fillChar:
-        newdecpos = newvalue.find(self._decimalChar)
-        if newdecpos < self._decimalpos:
-            padlen = self._decimalpos - newdecpos
-            newvalue = string.join([' ' * padlen] + [newvalue] ,'')
-
-        if self._signOk and self._useParens:
-            if newvalue.find('(') != -1:
-                newvalue = newvalue[:-1] + ')'
-            else:
-                newvalue = newvalue[:-1] + ' '
-
-##        dbg('newvalue = "%s"' % newvalue)
-        if candidate is None:
-            wx.CallAfter(self._SetValue, newvalue)
-##        dbg(indent=0)
-        return newvalue
-
-
-    def _adjustInt(self, candidate=None):
-        """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
-##        dbg("MaskedEditMixin::_adjustInt", candidate)
-        lenInt = self._masklength
-        if candidate is None: value = self._GetValue()
-        else: value = candidate
-
-        intStr = self._fields[0]._AdjustField(value)
-        intStr = intStr.strip() # drop extra spaces
-##        dbg('adjusted field: "%s"' % intStr)
-
-        if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1:
-            if self._useParens:
-                intStr = '(' + intStr + ')'
-            else:
-                intStr = '-' + intStr
-        elif self._isNeg and intStr.find('-') != -1 and self._useParens:
-            intStr = intStr.replace('-', '(')
-
-        if( self._signOk and ((self._useParens and intStr.find('(') == -1)
-                                or (not self._useParens and intStr.find('-') == -1))):
-            intStr = ' ' + intStr
-            if self._useParens:
-                intStr += ' '   # space for right paren position
-
-        elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1:
-            # ensure closing right paren:
-            intStr += ')'
-
-        if self._fields[0]._alignRight:     ## Only if right-alignment is enabled
-            intStr = intStr.rjust( lenInt )
-        else:
-            intStr = intStr.ljust( lenInt )
-
-        if candidate is None:
-            wx.CallAfter(self._SetValue, intStr )
-        return intStr
-
-
-    def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False):
-        """
-        'Fixes' a date control, expanding the year if it can.
-        Applies various self-formatting options.
-        """
-##        dbg("MaskedEditMixin::_adjustDate", indent=1)
-        if candidate is None: text    = self._GetValue()
-        else: text = candidate
-##        dbg('text=', text)
-        if self._datestyle == "YMD":
-            year_field = 0
-        else:
-            year_field = 2
-
-##        dbg('getYear: "%s"' % getYear(text, self._datestyle))
-        year    = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"")  # drop extra fillChars
-        month   = getMonth( text, self._datestyle)
-        day     = getDay( text, self._datestyle)
-##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day)
-
-        yearVal = None
-        yearstart = self._dateExtent - 4
-        if( len(year) < 4
-            and (fixcentury
-                 or force4digit_year
-                 or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ')
-                 or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ):
-            ## user entered less than four digits and changing fields or past point where we could
-            ## enter another digit:
-            try:
-                yearVal = int(year)
-            except:
-##                dbg('bad year=', year)
-                year = text[yearstart:self._dateExtent]
-
-        if len(year) < 4 and yearVal:
-            if len(year) == 2:
-                # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the
-                # years pass...
-                now = wx.DateTime_Now()
-                century = (now.GetYear() /100) * 100        # "this century"
-                twodig_year = now.GetYear() - century       # "this year" (2 digits)
-                # if separation between today's 2-digit year and typed value > 50,
-                #      assume last century,
-                # else assume this century.
-                #
-                # Eg: if 2003 and yearVal == 30, => 2030
-                #     if 2055 and yearVal == 80, => 2080
-                #     if 2010 and yearVal == 96, => 1996
-                #
-                if abs(yearVal - twodig_year) > 50:
-                    yearVal = (century - 100) + yearVal
-                else:
-                    yearVal = century + yearVal
-                year = str( yearVal )
-            else:   # pad with 0's to make a 4-digit year
-                year = "%04d" % yearVal
-            if self._4digityear or force4digit_year:
-                text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:]
-##        dbg('newdate: "%s"' % text, indent=0)
-        return text
-
-
-    def _goEnd(self, getPosOnly=False):
-        """ Moves the insertion point to the end of user-entry """
-##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
-        text = self._GetValue()
-####        dbg('text: "%s"' % text)
-        i = 0
-        if len(text.rstrip()):
-            for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1):
-####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i))
-                if self._isMaskChar(i):
-                    char = text[i]
-####                    dbg("text[%d]: '%s'" % (i, char))
-                    if char != ' ':
-                        i += 1
-                        break
-
-        if i == 0:
-            pos = self._goHome(getPosOnly=True)
-        else:
-            pos = min(i,self._masklength)
-
-        field = self._FindField(pos)
-        start, end = field._extent
-        if field._insertRight and pos < end:
-            pos = end
-##        dbg('next pos:', pos)
-##        dbg(indent=0)
-        if getPosOnly:
-            return pos
-        else:
-            self._SetInsertionPoint(pos)
-
-
-    def _goHome(self, getPosOnly=False):
-        """ Moves the insertion point to the beginning of user-entry """
-##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1)
-        text = self._GetValue()
-        for i in range(self._masklength):
-            if self._isMaskChar(i):
-                break
-        pos = max(i, 0)
-##        dbg(indent=0)
-        if getPosOnly:
-            return pos
-        else:
-            self._SetInsertionPoint(max(i,0))
-
-
-
-    def _getAllowedChars(self, pos):
-        """ Returns a string of all allowed user input characters for the provided
-            mask character plus control options
-        """
-        maskChar = self.maskdict[pos]
-        okchars = self.maskchardict[maskChar]    ## entry, get mask approved characters
-        field = self._FindField(pos)
-        if okchars and field._okSpaces:          ## Allow spaces?
-            okchars += " "
-        if okchars and field._includeChars:      ## any additional included characters?
-            okchars += field._includeChars
-####        dbg('okchars[%d]:' % pos, okchars)
-        return okchars
-
-
-    def _isMaskChar(self, pos):
-        """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
-        """
-        if pos < self._masklength:
-            return self.ismasked[pos]
-        else:
-            return False
-
-
-    def _isTemplateChar(self,Pos):
-        """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#)
-        """
-        if Pos < self._masklength:
-            return not self._isMaskChar(Pos)
-        else:
-            return False
-
-
-    def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False):
-        """ Returns True if character is allowed at the specific position, otherwise False."""
-##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1)
-        field = self._FindField(pos)
-        right_insert = False
-
-        if self.controlInitialized:
-            sel_start, sel_to = self._GetSelection()
-        else:
-            sel_start, sel_to = pos, pos
-
-        if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight:
-            start, end = field._extent
-            field_len = end - start
-            if self.controlInitialized:
-                value = self._GetValue()
-                fstr = value[start:end].strip()
-                if field._padZero:
-                    while fstr and fstr[0] == '0':
-                        fstr = fstr[1:]
-                input_len = len(fstr)
-                if self._signOk and '-' in fstr or '(' in fstr:
-                    input_len -= 1  # sign can move out of field, so don't consider it in length
-            else:
-                value = self._template
-                input_len = 0   # can't get the current "value", so use 0
-
-
-            # if entire field is selected or position is at end and field is not full,
-            # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar:
-            if( (sel_start, sel_to) == field._extent
-                or (pos == end and input_len < field_len)):
-                pos = end - 1
-##                dbg('pos = end - 1 = ', pos, 'right_insert? 1')
-                right_insert = True
-            elif( field._allowInsert and sel_start == sel_to
-                  and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar))
-                  and input_len < field_len ):
-                pos = sel_to - 1    # where character will go
-##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1')
-                right_insert = True
-            # else leave pos alone...
-            else:
-##                dbg('pos stays ', pos, 'right_insert? 0')
-                pass
-
-        if self._isTemplateChar( pos ):  ## if a template character, return empty
-##            dbg('%d is a template character; returning False' % pos, indent=0)
-            return False
-
-        if self._isMaskChar( pos ):
-            okChars  = self._getAllowedChars(pos)
-
-            if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)):
-                okChars += self._fields[0]._groupChar
-
-            if self._signOk:
-                if self._isInt or (self._isFloat and pos < self._decimalpos):
-                    okChars += '-'
-                    if self._useParens:
-                        okChars += '('
-                elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)):
-                    okChars += ')'
-
-####            dbg('%s in %s?' % (char, okChars), char in okChars)
-            approved = char in okChars
-
-            if approved and checkRegex:
-##                dbg("checking appropriate regex's")
-                value = self._eraseSelection(self._GetValue())
-                if right_insert:
-                    at = pos+1
-                else:
-                    at = pos
-                if allowAutoSelect:
-                    newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True)
-                else:
-                    newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value)
-##                dbg('newvalue: "%s"' % newvalue)
-
-                fields = [self._FindField(pos)] + [self._ctrl_constraints]
-                for field in fields:    # includes fields[-1] == "ctrl_constraints"
-                    if field._regexMask and field._filter:
-##                        dbg('checking vs. regex')
-                        start, end = field._extent
-                        slice = newvalue[start:end]
-                        approved = (re.match( field._filter, slice) is not None)
-##                        dbg('approved?', approved)
-                    if not approved: break
-##            dbg(indent=0)
-            return approved
-        else:
-##            dbg('%d is a !???! character; returning False', indent=0)
-            return False
-
-
-    def _applyFormatting(self):
-        """ Apply formatting depending on the control's state.
-            Need to find a way to call this whenever the value changes, in case the control's
-            value has been changed or set programatically.
-        """
-##        dbg(suspend=1)
-##        dbg('MaskedEditMixin::_applyFormatting', indent=1)
-
-        # Handle negative numbers
-        if self._signOk:
-            text, signpos, right_signpos = self._getSignedValue()
-##            dbg('text: "%s", signpos:' % text, signpos)
-            if not text or text[signpos] not in ('-','('):
-                self._isNeg = False
-##                dbg('no valid sign found; new sign:', self._isNeg)
-                if text and signpos != self._signpos:
-                    self._signpos = signpos
-            elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('):
-##                dbg('setting _isNeg to True')
-                self._isNeg = True
-##            dbg('self._isNeg:', self._isNeg)
-
-        if self._signOk and self._isNeg:
-            fc = self._signedForegroundColour
-        else:
-            fc = self._foregroundColour
-
-        if hasattr(fc, '_name'):
-            c =fc._name
-        else:
-            c = fc
-##        dbg('setting foreground to', c)
-        self.SetForegroundColour(fc)
-
-        if self._valid:
-##            dbg('valid')
-            if self.IsEmpty():
-                bc = self._emptyBackgroundColour
-            else:
-                bc = self._validBackgroundColour
-        else:
-##            dbg('invalid')
-            bc = self._invalidBackgroundColour
-        if hasattr(bc, '_name'):
-            c =bc._name
-        else:
-            c = bc
-##        dbg('setting background to', c)
-        self.SetBackgroundColour(bc)
-        self._Refresh()
-##        dbg(indent=0, suspend=0)
-
-
-    def _getAbsValue(self, candidate=None):
-        """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s).
-        """
-##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1)
-        if candidate is None: text = self._GetValue()
-        else: text = candidate
-        right_signpos = text.find(')')
-
-        if self._isInt:
-            if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ':
-                signpos = text.find('-')
-                if signpos == -1:
-##                    dbg('no - found; searching for (')
-                    signpos = text.find('(')
-                elif signpos != -1:
-##                    dbg('- found at', signpos)
-                    pass
-
-                if signpos == -1:
-##                    dbg('signpos still -1')
-##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength)
-                    if len(text) < self._masklength:
-                        text = ' ' + text
-                    if len(text) < self._masklength:
-                        text += ' '
-                    if len(text) > self._masklength and text[-1] in (')', ' '):
-                        text = text[:-1]
-                    else:
-##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength))
-##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1))
-                        signpos = len(text) - (len(text.lstrip()) + 1)
-
-                        if self._useParens and not text.strip():
-                            signpos -= 1    # empty value; use penultimate space
-##                dbg('signpos:', signpos)
-                if signpos >= 0:
-                    text = text[:signpos] + ' ' + text[signpos+1:]
-
-            else:
-                if self._signOk:
-                    signpos = 0
-                    text = self._template[0] + text[1:]
-                else:
-                    signpos = -1
-
-            if right_signpos != -1:
-                if self._signOk:
-                    text = text[:right_signpos] + ' ' + text[right_signpos+1:]
-                elif len(text) > self._masklength:
-                    text = text[:right_signpos] + text[right_signpos+1:]
-                    right_signpos = -1
-
-
-            elif self._useParens and self._signOk:
-                # figure out where it ought to go:
-                right_signpos = self._masklength - 1     # initial guess
-                if not self._ctrl_constraints._alignRight:
-##                    dbg('not right-aligned')
-                    if len(text.strip()) == 0:
-                        right_signpos = signpos + 1
-                    elif len(text.strip()) < self._masklength:
-                        right_signpos = len(text.rstrip())
-##                dbg('right_signpos:', right_signpos)
-
-            groupchar = self._fields[0]._groupChar
-            try:
-                value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', ''))
-            except:
-##                dbg('invalid number', indent=0)
-                return None, signpos, right_signpos
-
-        else:   # float value
-            try:
-                groupchar = self._fields[0]._groupChar
-                value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', ''))
-##                dbg('value:', value)
-            except:
-                value = None
-
-            if value < 0 and value is not None:
-                signpos = text.find('-')
-                if signpos == -1:
-                    signpos = text.find('(')
-
-                text = text[:signpos] + self._template[signpos] + text[signpos+1:]
-            else:
-                # look forwards up to the decimal point for the 1st non-digit
-##                dbg('decimal pos:', self._decimalpos)
-##                dbg('text: "%s"' % text)
-                if self._signOk:
-                    signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1)
-                    # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET
-                    if len(text) >= signpos+1 and  text[signpos+1] in ('-','('):
-                        signpos += 1
-                else:
-                    signpos = -1
-##                dbg('signpos:', signpos)
-
-            if self._useParens:
-                if self._signOk:
-                    right_signpos = self._masklength - 1
-                    text = text[:right_signpos] + ' '
-                    if text[signpos] == '(':
-                        text = text[:signpos] + ' ' + text[signpos+1:]
-                else:
-                    right_signpos = text.find(')')
-                    if right_signpos != -1:
-                        text = text[:-1]
-                        right_signpos = -1
-
-            if value is None:
-##                dbg('invalid number')
-                text = None
-
-##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos)
-##        dbg(indent=0)
-        return text, signpos, right_signpos
-
-
-    def _getSignedValue(self, candidate=None):
-        """ Return a signed value by adding a "-" prefix if the value
-            is set to negative, or a space if positive.
-        """
-##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1)
-        if candidate is None: text = self._GetValue()
-        else: text = candidate
-
-
-        abstext, signpos, right_signpos = self._getAbsValue(text)
-        if self._signOk:
-            if abstext is None:
-##                dbg(indent=0)
-                return abstext, signpos, right_signpos
-
-            if self._isNeg or text[signpos] in ('-', '('):
-                if self._useParens:
-                    sign = '('
-                else:
-                    sign = '-'
-            else:
-                sign = ' '
-            if abstext[signpos] not in string.digits:
-                text = abstext[:signpos] + sign + abstext[signpos+1:]
-            else:
-                # this can happen if value passed is too big; sign assumed to be
-                # in position 0, but if already filled with a digit, prepend sign...
-                text = sign + abstext
-            if self._useParens and text.find('(') != -1:
-                text = text[:right_signpos] + ')' + text[right_signpos+1:]
-        else:
-            text = abstext
-##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos)
-##        dbg(indent=0)
-        return text, signpos, right_signpos
-
-
-    def GetPlainValue(self, candidate=None):
-        """ Returns control's value stripped of the template text.
-            plainvalue = MaskedEditMixin.GetPlainValue()
-        """
-##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1)
-
-        if candidate is None: text = self._GetValue()
-        else: text = candidate
-
-        if self.IsEmpty():
-##            dbg('returned ""', indent=0)
-            return ""
-        else:
-            plain = ""
-            for idx in range( min(len(self._template), len(text)) ):
-                if self._mask[idx] in maskchars:
-                    plain += text[idx]
-
-            if self._isFloat or self._isInt:
-##                dbg('plain so far: "%s"' % plain)
-                plain = plain.replace('(', '-').replace(')', ' ')
-##                dbg('plain after sign regularization: "%s"' % plain)
-
-                if self._signOk and self._isNeg and plain.count('-') == 0:
-                    # must be in reserved position; add to "plain value"
-                    plain = '-' + plain.strip()
-
-                if self._fields[0]._alignRight:
-                    lpad = plain.count(',')
-                    plain = ' ' * lpad + plain.replace(',','')
-                else:
-                    plain = plain.replace(',','')
-##                dbg('plain after pad and group:"%s"' % plain)
-
-##            dbg('returned "%s"' % plain.rstrip(), indent=0)
-            return plain.rstrip()
-
-
-    def IsEmpty(self, value=None):
-        """
-        Returns True if control is equal to an empty value.
-        (Empty means all editable positions in the template == fillChar.)
-        """
-        if value is None: value = self._GetValue()
-        if value == self._template and not self._defaultValue:
-####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)")
-            return True     # (all mask chars == fillChar by defn)
-        elif value == self._template:
-            empty = True
-            for pos in range(len(self._template)):
-####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos))
-####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos])
-                if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]):
-                    empty = False
-####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals())
-            return empty
-        else:
-####            dbg("IsEmpty? 0 (value doesn't match template)")
-            return False
-
-
-    def IsDefault(self, value=None):
-        """
-        Returns True if the value specified (or the value of the control if not specified)
-        is equal to the default value.
-        """
-        if value is None: value = self._GetValue()
-        return value == self._template
-
-
-    def IsValid(self, value=None):
-        """ Indicates whether the value specified (or the current value of the control
-        if not specified) is considered valid."""
-####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1)
-        if value is None: value = self._GetValue()
-        ret = self._CheckValid(value)
-####        dbg(indent=0)
-        return ret
-
-
-    def _eraseSelection(self, value=None, sel_start=None, sel_to=None):
-        """ Used to blank the selection when inserting a new character. """
-##        dbg("MaskedEditMixin::_eraseSelection", indent=1)
-        if value is None: value = self._GetValue()
-        if sel_start is None or sel_to is None:
-            sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
-##        dbg('value: "%s"' % value)
-##        dbg("current sel_start, sel_to:", sel_start, sel_to)
-
-        newvalue = list(value)
-        for i in range(sel_start, sel_to):
-            if self._signOk and newvalue[i] in ('-', '(', ')'):
-##                dbg('found sign (%s) at' % newvalue[i], i)
-
-                # balance parentheses:
-                if newvalue[i] == '(':
-                    right_signpos = value.find(')')
-                    if right_signpos != -1:
-                        newvalue[right_signpos] = ' '
-
-                elif newvalue[i] == ')':
-                    left_signpos = value.find('(')
-                    if left_signpos != -1:
-                        newvalue[left_signpos] = ' '
-
-                newvalue[i] = ' '
-
-            elif self._isMaskChar(i):
-                field = self._FindField(i)
-                if field._padZero:
-                    newvalue[i] = '0'
-                else:
-                    newvalue[i] = self._template[i]
-
-        value = string.join(newvalue,"")
-##        dbg('new value: "%s"' % value)
-##        dbg(indent=0)
-        return value
-
-
-    def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False):
-        """ Handles replacement of the character at the current insertion point."""
-##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
-
-        text = self._eraseSelection(value)
-        field = self._FindField(pos)
-        start, end = field._extent
-        newtext = ""
-        newpos = pos
-
-        if pos != sel_start and sel_start == sel_to:
-            # adjustpos must have moved the position; make selection match:
-            sel_start = sel_to = pos
-
-##        dbg('field._insertRight?', field._insertRight)
-        if( field._insertRight                                  # field allows right insert
-            and ((sel_start, sel_to) == field._extent           # and whole field selected
-                 or (sel_start == sel_to                        # or nothing selected
-                     and (sel_start == end                      # and cursor at right edge
-                          or (field._allowInsert                # or field allows right-insert
-                              and sel_start < end               # next to other char in field:
-                              and text[sel_start] != field._fillChar) ) ) ) ):
-##            dbg('insertRight')
-            fstr = text[start:end]
-            erasable_chars = [field._fillChar, ' ']
-
-            if field._padZero:
-                erasable_chars.append('0')
-
-            erased = ''
-####            dbg("fstr[0]:'%s'" % fstr[0])
-####            dbg('field_index:', field._index)
-####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars)
-####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?",
-##                 self._signOk and field._index == 0 and fstr[0] in ('-','('))
-            if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')):
-                erased = fstr[0]
-####                dbg('value:      "%s"' % text)
-####                dbg('fstr:       "%s"' % fstr)
-####                dbg("erased:     '%s'" % erased)
-                field_sel_start = sel_start - start
-                field_sel_to = sel_to - start
-##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start])
-##                dbg('right fstr: "%s"' % fstr[field_sel_to:end])
-                fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end]
-            if field._alignRight and sel_start != sel_to:
-                field_len = end - start
-##                pos += (field_len - len(fstr))    # move cursor right by deleted amount
-                pos = sel_to
-##                dbg('setting pos to:', pos)
-                if field._padZero:
-                    fstr = '0' * (field_len - len(fstr)) + fstr
-                else:
-                    fstr = fstr.rjust(field_len)   # adjust the field accordingly
-##            dbg('field str: "%s"' % fstr)
-
-            newtext = text[:start] + fstr + text[end:]
-            if erased in ('-', '(') and self._signOk:
-                newtext = erased + newtext[1:]
-##            dbg('newtext: "%s"' % newtext)
-
-            if self._signOk and field._index == 0:
-                start -= 1             # account for sign position
-
-####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
-####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
-            if( field._moveOnFieldFull and pos == end
-                and len(fstr.lstrip()) == end-start):   # if field now full
-                newpos = self._findNextEntry(end)       #   go to next field
-            else:
-                newpos = pos                            # else keep cursor at current position
-
-        if not newtext:
-##            dbg('not newtext')
-            if newpos != pos:
-##                dbg('newpos:', newpos)
-                pass
-            if self._signOk and self._useParens:
-                old_right_signpos = text.find(')')
-
-            if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start:
-                # inserting within a left-insert-capable field
-                field_len = end - start
-                before = text[start:sel_start]
-                after = text[sel_to:end].strip()
-####                dbg("current field:'%s'" % text[start:end])
-####                dbg("before:'%s'" % before, "after:'%s'" % after)
-                new_len = len(before) + len(after) + 1 # (for inserted char)
-####                dbg('new_len:', new_len)
-
-                if new_len < field_len:
-                    retained = after + self._template[end-(field_len-new_len):end]
-                elif new_len > end-start:
-                    retained = after[1:]
-                else:
-                    retained = after
-
-                left = text[0:start] + before
-####                dbg("left:'%s'" % left, "retained:'%s'" % retained)
-                right   = retained + text[end:]
-            else:
-                left  = text[0:pos]
-                right   = text[pos+1:]
-
-            newtext = left + char + right
-
-            if self._signOk and self._useParens:
-                # Balance parentheses:
-                left_signpos = newtext.find('(')
-
-                if left_signpos == -1:     # erased '('; remove ')'
-                    right_signpos = newtext.find(')')
-                    if right_signpos != -1:
-                        newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:]
-
-                elif old_right_signpos != -1:
-                    right_signpos = newtext.find(')')
-
-                    if right_signpos == -1: # just replaced right-paren
-                        if newtext[pos] == ' ': # we just erased '); erase '('
-                            newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:]
-                        else:   # replaced with digit; move ') over
-                            if self._ctrl_constraints._alignRight or self._isFloat:
-                                newtext = newtext[:-1] + ')'
-                            else:
-                                rstripped_text = newtext.rstrip()
-                                right_signpos = len(rstripped_text)
-##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos)
-                                newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:]
-
-            if( field._insertRight                                  # if insert-right field (but we didn't start at right edge)
-                and field._moveOnFieldFull                          # and should move cursor when full
-                and len(newtext[start:end].strip()) == end-start):  # and field now full
-                newpos = self._findNextEntry(end)                   #   go to next field
-##                dbg('newpos = nextentry =', newpos)
-            else:
-##                dbg('pos:', pos, 'newpos:', pos+1)
-                newpos = pos+1
-
-
-        if allowAutoSelect:
-            new_select_to = newpos     # (default return values)
-            match_field = None
-            match_index = None
-
-            if field._autoSelect:
-                match_index, partial_match = self._autoComplete(1,  # (always forward)
-                                                                field._compareChoices,
-                                                                newtext[start:end],
-                                                                compareNoCase=field._compareNoCase,
-                                                                current_index = field._autoCompleteIndex-1)
-                if match_index is not None and partial_match:
-                    matched_str = newtext[start:end]
-                    newtext = newtext[:start] + field._choices[match_index] + newtext[end:]
-                    new_select_to = end
-                    match_field = field
-                    if field._insertRight:
-                        # adjust position to just after partial match in field
-                        newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip()))
-
-            elif self._ctrl_constraints._autoSelect:
-                match_index, partial_match = self._autoComplete(
-                                        1,  # (always forward)
-                                        self._ctrl_constraints._compareChoices,
-                                        newtext,
-                                        self._ctrl_constraints._compareNoCase,
-                                        current_index = self._ctrl_constraints._autoCompleteIndex - 1)
-                if match_index is not None and partial_match:
-                    matched_str = newtext
-                    newtext = self._ctrl_constraints._choices[match_index]
-                    new_select_to = self._ctrl_constraints._extent[1]
-                    match_field = self._ctrl_constraints
-                    if self._ctrl_constraints._insertRight:
-                        # adjust position to just after partial match in control:
-                        newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip()))
-
-##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to)
-##            dbg(indent=0)
-            return newtext, newpos, new_select_to, match_field, match_index
-        else:
-##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos)
-##            dbg(indent=0)
-            return newtext, newpos
-
-
-    def _OnFocus(self,event):
-        """
-        This event handler is currently necessary to work around new default
-        behavior as of wxPython2.3.3;
-        The TAB key auto selects the entire contents of the wxTextCtrl *after*
-        the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
-        *here*, because it hasn't happened yet.  So to prevent this behavior, and
-        preserve the correct selection when the focus event is not due to tab,
-        we need to pull the following trick:
-        """
-##        dbg('MaskedEditMixin::_OnFocus')
-        wx.CallAfter(self._fixSelection)
-        event.Skip()
-        self.Refresh()
-
-
-    def _CheckValid(self, candidate=None):
-        """
-        This is the default validation checking routine; It verifies that the
-        current value of the control is a "valid value," and has the side
-        effect of coloring the control appropriately.
-        """
-##        dbg(suspend=1)
-##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1)
-        oldValid = self._valid
-        if candidate is None: value = self._GetValue()
-        else: value = candidate
-##        dbg('value: "%s"' % value)
-        oldvalue = value
-        valid = True    # assume True
-
-        if not self.IsDefault(value) and self._isDate:                    ## Date type validation
-            valid = self._validateDate(value)
-##            dbg("valid date?", valid)
-
-        elif not self.IsDefault(value) and self._isTime:
-            valid = self._validateTime(value)
-##            dbg("valid time?", valid)
-
-        elif not self.IsDefault(value) and (self._isInt or self._isFloat):  ## Numeric type
-            valid = self._validateNumeric(value)
-##            dbg("valid Number?", valid)
-
-        if valid:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault()
-            ## valid so far; ensure also allowed by any list or regex provided:
-            valid = self._validateGeneric(value)
-##            dbg("valid value?", valid)
-
-##        dbg('valid?', valid)
-
-        if not candidate:
-            self._valid = valid
-            self._applyFormatting()
-            if self._valid != oldValid:
-##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
-##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
-                pass
-##        dbg(indent=0, suspend=0)
-        return valid
-
-
-    def _validateGeneric(self, candidate=None):
-        """ Validate the current value using the provided list or Regex filter (if any).
-        """
-        if candidate is None:
-            text = self._GetValue()
-        else:
-            text = candidate
-
-        valid = True    # assume True
-        for i in [-1] + self._field_indices:   # process global constraints first:
-            field = self._fields[i]
-            start, end = field._extent
-            slice = text[start:end]
-            valid = field.IsValid(slice)
-            if not valid:
-                break
-
-        return valid
-
-
-    def _validateNumeric(self, candidate=None):
-        """ Validate that the value is within the specified range (if specified.)"""
-        if candidate is None: value = self._GetValue()
-        else: value = candidate
-        try:
-            groupchar = self._fields[0]._groupChar
-            if self._isFloat:
-                number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', ''))
-            else:
-                number = long( value.replace(groupchar, '').replace('(', '-').replace(')', ''))
-                if value.strip():
-                    if self._fields[0]._alignRight:
-                        require_digit_at = self._fields[0]._extent[1]-1
-                    else:
-                        require_digit_at = self._fields[0]._extent[0]
-##                    dbg('require_digit_at:', require_digit_at)
-##                    dbg("value[rda]: '%s'" % value[require_digit_at])
-                    if value[require_digit_at] not in list(string.digits):
-                        valid = False
-                        return valid
-                # else...
-##            dbg('number:', number)
-            if self._ctrl_constraints._hasRange:
-                valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh
-            else:
-                valid = True
-            groupcharpos = value.rfind(groupchar)
-            if groupcharpos != -1:  # group char present
-##                dbg('groupchar found at', groupcharpos)
-                if self._isFloat and groupcharpos > self._decimalpos:
-                    # 1st one found on right-hand side is past decimal point
-##                    dbg('groupchar in fraction; illegal')
-                    valid = False
-                elif self._isFloat:
-                    integer = value[:self._decimalpos].strip()
-                else:
-                    integer = value.strip()
-##                dbg("integer:'%s'" % integer)
-                if integer[0] in ('-', '('):
-                    integer = integer[1:]
-                if integer[-1] == ')':
-                    integer = integer[:-1]
-
-                parts = integer.split(groupchar)
-##                dbg('parts:', parts)
-                for i in range(len(parts)):
-                    if i == 0 and abs(int(parts[0])) > 999:
-##                        dbg('group 0 too long; illegal')
-                        valid = False
-                        break
-                    elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]):
-##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i]))
-                        valid = False
-                        break
-        except ValueError:
-##            dbg('value not a valid number')
-            valid = False
-        return valid
-
-
-    def _validateDate(self, candidate=None):
-        """ Validate the current date value using the provided Regex filter.
-            Generally used for character types.BufferType
-        """
-##        dbg('MaskedEditMixin::_validateDate', indent=1)
-        if candidate is None: value = self._GetValue()
-        else: value = candidate
-##        dbg('value = "%s"' % value)
-        text = self._adjustDate(value, force4digit_year=True)     ## Fix the date up before validating it
-##        dbg('text =', text)
-        valid = True   # assume True until proven otherwise
-
-        try:
-            # replace fillChar in each field with space:
-            datestr = text[0:self._dateExtent]
-            for i in range(3):
-                field = self._fields[i]
-                start, end = field._extent
-                fstr = datestr[start:end]
-                fstr.replace(field._fillChar, ' ')
-                datestr = datestr[:start] + fstr + datestr[end:]
-
-            year, month, day = getDateParts( datestr, self._datestyle)
-            year = int(year)
-##            dbg('self._dateExtent:', self._dateExtent)
-            if self._dateExtent == 11:
-                month = charmonths_dict[month.lower()]
-            else:
-                month = int(month)
-            day = int(day)
-##            dbg('year, month, day:', year, month, day)
-
-        except ValueError:
-##            dbg('cannot convert string to integer parts')
-            valid = False
-        except KeyError:
-##            dbg('cannot convert string to integer month')
-            valid = False
-
-        if valid:
-            # use wxDateTime to unambiguously try to parse the date:
-            # ### Note: because wxDateTime is *brain-dead* and expects months 0-11,
-            # rather than 1-12, so handle accordingly:
-            if month > 12:
-                valid = False
-            else:
-                month -= 1
-                try:
-##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year))
-                    dateHandler = wx.DateTimeFromDMY(day,month,year)
-##                    dbg("succeeded")
-                    dateOk = True
-                except:
-##                    dbg('cannot convert string to valid date')
-                    dateOk = False
-                if not dateOk:
-                    valid = False
-
-            if valid:
-                # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
-                # so we eliminate them here:
-                timeStr     = text[self._dateExtent+1:].strip()         ## time portion of the string
-                if timeStr:
-##                    dbg('timeStr: "%s"' % timeStr)
-                    try:
-                        checkTime    = dateHandler.ParseTime(timeStr)
-                        valid = checkTime == len(timeStr)
-                    except:
-                        valid = False
-                    if not valid:
-##                        dbg('cannot convert string to valid time')
-                        pass                        
-        if valid: dbg('valid date')
-##        dbg(indent=0)
-        return valid
-
-
-    def _validateTime(self, candidate=None):
-        """ Validate the current time value using the provided Regex filter.
-            Generally used for character types.BufferType
-        """
-##        dbg('MaskedEditMixin::_validateTime', indent=1)
-        # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
-        # so we eliminate them here:
-        if candidate is None: value = self._GetValue().strip()
-        else: value = candidate.strip()
-##        dbg('value = "%s"' % value)
-        valid = True   # assume True until proven otherwise
-
-        dateHandler = wx.DateTime_Today()
-        try:
-            checkTime    = dateHandler.ParseTime(value)
-##            dbg('checkTime:', checkTime, 'len(value)', len(value))
-            valid = checkTime == len(value)
-        except:
-            valid = False
-
-        if not valid:
-##            dbg('cannot convert string to valid time')
-            pass
-        if valid: dbg('valid time')
-##        dbg(indent=0)
-        return valid
-
-
-    def _OnKillFocus(self,event):
-        """ Handler for EVT_KILL_FOCUS event.
-        """
-##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1)
-        if self._mask and self._IsEditable():
-            self._AdjustField(self._GetInsertionPoint())
-            self._CheckValid()   ## Call valid handler
-
-        self._LostFocus()    ## Provided for subclass use
-        event.Skip()
-##        dbg(indent=0)
-
-
-    def _fixSelection(self):
-        """
-        This gets called after the TAB traversal selection is made, if the
-        focus event was due to this, but before the EVT_LEFT_* events if
-        the focus shift was due to a mouse event.
-
-        The trouble is that, a priori, there's no explicit notification of
-        why the focus event we received.  However, the whole reason we need to
-        do this is because the default behavior on TAB traveral in a wxTextCtrl is
-        now to select the entire contents of the window, something we don't want.
-        So we can *now* test the selection range, and if it's "the whole text"
-        we can assume the cause, change the insertion point to the start of
-        the control, and deselect.
-        """
-##        dbg('MaskedEditMixin::_fixSelection', indent=1)
-        if not self._mask or not self._IsEditable():
-##            dbg(indent=0)
-            return
-
-        sel_start, sel_to = self._GetSelection()
-##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty())
-
-        if( sel_start == 0 and sel_to >= len( self._mask )   #(can be greater in numeric controls because of reserved space)
-            and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ):
-            # This isn't normally allowed, and so assume we got here by the new
-            # "tab traversal" behavior, so we need to reset the selection
-            # and insertion point:
-##            dbg('entire text selected; resetting selection to start of control')
-            self._goHome()
-            field = self._FindField(self._GetInsertionPoint())
-            edit_start, edit_end = field._extent
-            if field._selectOnFieldEntry:
-                self._SetInsertionPoint(edit_start)
-                self._SetSelection(edit_start, edit_end)
-
-            elif field._insertRight:
-                self._SetInsertionPoint(edit_end)
-                self._SetSelection(edit_end, edit_end)
-
-        elif (self._isFloat or self._isInt):
-
-            text, signpos, right_signpos = self._getAbsValue()
-            if text is None or text == self._template:
-                integer = self._fields[0]
-                edit_start, edit_end = integer._extent
-
-                if integer._selectOnFieldEntry:
-##                    dbg('select on field entry:')
-                    self._SetInsertionPoint(edit_start)
-                    self._SetSelection(edit_start, edit_end)
-
-                elif integer._insertRight:
-##                    dbg('moving insertion point to end')
-                    self._SetInsertionPoint(edit_end)
-                    self._SetSelection(edit_end, edit_end)
-                else:
-##                    dbg('numeric ctrl is empty; start at beginning after sign')
-                    self._SetInsertionPoint(signpos+1)   ## Move past minus sign space if signed
-                    self._SetSelection(signpos+1, signpos+1)
-
-        elif sel_start > self._goEnd(getPosOnly=True):
-##            dbg('cursor beyond the end of the user input; go to end of it')
-            self._goEnd()
-        else:
-##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength)
-            pass
-##        dbg(indent=0)
-
-
-    def _Keypress(self,key):
-        """ Method provided to override OnChar routine. Return False to force
-            a skip of the 'normal' OnChar process. Called before class OnChar.
-        """
-        return True
-
-
-    def _LostFocus(self):
-        """ Method provided for subclasses. _LostFocus() is called after
-            the class processes its EVT_KILL_FOCUS event code.
-        """
-        pass
-
-
-    def _OnDoubleClick(self, event):
-        """ selects field under cursor on dclick."""
-        pos = self._GetInsertionPoint()
-        field = self._FindField(pos)
-        start, end = field._extent
-        self._SetInsertionPoint(start)
-        self._SetSelection(start, end)
-
-
-    def _Change(self):
-        """ Method provided for subclasses. Called by internal EVT_TEXT
-            handler. Return False to override the class handler, True otherwise.
-        """
-        return True
-
-
-    def _Cut(self):
-        """
-        Used to override the default Cut() method in base controls, instead
-        copying the selection to the clipboard and then blanking the selection,
-        leaving only the mask in the selected area behind.
-        Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the
-        derived control because the mixin functions can't override a method of
-        a sibling class.
-        """
-##        dbg("MaskedEditMixin::_Cut", indent=1)
-        value = self._GetValue()
-##        dbg('current value: "%s"' % value)
-        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
-##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
-        do = wxTextDataObject()
-        do.SetText(value[sel_start:sel_to].strip())
-        wxTheClipboard.Open()
-        wxTheClipboard.SetData(do)
-        wxTheClipboard.Close()
-
-        if sel_to - sel_start != 0:
-            self._OnErase()
-##        dbg(indent=0)
-
-
-# WS Note: overriding Copy is no longer necessary given that you
-# can no longer select beyond the last non-empty char in the control.
-#
-##    def _Copy( self ):
-##        """
-##        Override the wxTextCtrl's .Copy function, with our own
-##        that does validation.  Need to strip trailing spaces.
-##        """
-##        sel_start, sel_to = self._GetSelection()
-##        select_len = sel_to - sel_start
-##        textval = wxTextCtrl._GetValue(self)
-##
-##        do = wxTextDataObject()
-##        do.SetText(textval[sel_start:sel_to].strip())
-##        wxTheClipboard.Open()
-##        wxTheClipboard.SetData(do)
-##        wxTheClipboard.Close()
-
-
-    def _getClipboardContents( self ):
-        """ Subroutine for getting the current contents of the clipboard.
-        """
-        do = wxTextDataObject()
-        wxTheClipboard.Open()
-        success = wxTheClipboard.GetData(do)
-        wxTheClipboard.Close()
-
-        if not success:
-            return None
-        else:
-            # Remove leading and trailing spaces before evaluating contents
-            return do.GetText().strip()
-
-
-    def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False):
-        """
-        Used by paste routine and field choice validation to see
-        if a given slice of paste text is legal for the area in question:
-        returns validity, replacement text, and extent of paste in
-        template.
-        """
-##        dbg(suspend=1)
-##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1)
-        select_length = sel_to - sel_start
-        maxlength = select_length
-##        dbg('sel_to - sel_start:', maxlength)
-        if maxlength == 0:
-            maxlength = self._masklength - sel_start
-            item = 'control'
-        else:
-            item = 'selection'
-##        dbg('maxlength:', maxlength)
-        length_considered = len(paste_text)
-        if length_considered > maxlength:
-##            dbg('paste text will not fit into the %s:' % item, indent=0)
-            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))
-                else:
-                    raise ValueError('"%s" will not fit into the selection' % paste_text)
-            else:
-##                dbg(indent=0, suspend=0)
-                return False, None, None
-
-        text = self._template
-##        dbg('length_considered:', length_considered)
-
-        valid_paste = True
-        replacement_text = ""
-        replace_to = sel_start
-        i = 0
-        while valid_paste and i < length_considered and replace_to < self._masklength:
-            if paste_text[i:] == self._template[replace_to:length_considered]:
-                # remainder of paste matches template; skip char-by-char analysis
-##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered))
-                replacement_text += paste_text[i:]
-                replace_to = i = length_considered
-                continue
-            # else:
-            char = paste_text[i]
-            field = self._FindField(replace_to)
-            if not field._compareNoCase:
-                if field._forceupper:   char = char.upper()
-                elif field._forcelower: char = char.lower()
-
-##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to)
-##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to))
-            if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True):
-                replacement_text += char
-##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals())
-##                dbg("replacement_text:", '"'+replacement_text+'"')
-                i += 1
-                replace_to += 1
-            elif( char == self._template[replace_to]
-                  or (self._signOk and
-                          ( (i == 0 and (char == '-' or (self._useParens and char == '(')))
-                            or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ):
-                replacement_text += char
-##                dbg("'%(char)s' == template(%(replace_to)d)" % locals())
-##                dbg("replacement_text:", '"'+replacement_text+'"')
-                i += 1
-                replace_to += 1
-            else:
-                next_entry = self._findNextEntry(replace_to, adjustInsert=False)
-                if next_entry == replace_to:
-                    valid_paste = False
-                else:
-                    replacement_text += self._template[replace_to:next_entry]
-##                    dbg("skipping template; next_entry =", next_entry)
-##                    dbg("replacement_text:", '"'+replacement_text+'"')
-                    replace_to = next_entry  # so next_entry will be considered on next loop
-
-        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))
-
-        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))
-
-##        dbg('valid_paste?', valid_paste)
-        if valid_paste:
-##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
-            pass
-##        dbg(indent=0, suspend=0)
-        return valid_paste, replacement_text, replace_to
-
-
-    def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
-        """
-        Used to override the base control's .Paste() function,
-        with our own that does validation.
-        Note: _Paste must be called from a Paste() override in the
-        derived control because the mixin functions can't override a
-        method of a sibling class.
-        """
-##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
-        if value is None:
-            paste_text = self._getClipboardContents()
-        else:
-            paste_text = value
-
-        if paste_text is not None:
-##            dbg('paste text: "%s"' % paste_text)
-            # (conversion will raise ValueError if paste isn't legal)
-            sel_start, sel_to = self._GetSelection()
-##            dbg('selection:', (sel_start, sel_to))
-
-            # special case: handle allowInsert fields properly
-            field = self._FindField(sel_start)
-            edit_start, edit_end = field._extent
-            new_pos = None
-            if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end:
-                new_pos = sel_start + len(paste_text)   # store for subsequent positioning
-                paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
-##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end)
-                sel_to = sel_start + len(paste_text)
-
-            # Another special case: paste won't fit, but it's a right-insert field where entire
-            # non-empty value is selected, and there's room if the selection is expanded leftward:
-            if( len(paste_text) > sel_to - sel_start
-                and field._insertRight
-                and sel_start > edit_start
-                and sel_to >= edit_end
-                and not self._GetValue()[edit_start:sel_start].strip() ):
-                # text won't fit within selection, but left of selection is empty;
-                # check to see if we can expand selection to accomodate the value:
-                empty_space = sel_start - edit_start
-                amount_needed = len(paste_text) - (sel_to - sel_start)
-                if amount_needed <= empty_space:
-                    sel_start -= amount_needed
-##                    dbg('expanded selection to:', (sel_start, sel_to))
-
-
-            # another special case: deal with signed values properly:
-            if self._signOk:
-                signedvalue, signpos, right_signpos = self._getSignedValue()
-                paste_signpos = paste_text.find('-')
-                if paste_signpos == -1:
-                    paste_signpos = paste_text.find('(')
-
-                # if paste text will result in signed value:
-####                dbg('paste_signpos != -1?', paste_signpos != -1)
-####                dbg('sel_start:', sel_start, 'signpos:', signpos)
-####                dbg('field._insertRight?', field._insertRight)
-####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos)
-                if paste_signpos != -1 and (sel_start <= signpos
-                                            or (field._insertRight and sel_start - len(paste_text) <= signpos)):
-                    signed = True
-                else:
-                    signed = False
-                # remove "sign" from paste text, so we can auto-adjust for sign type after paste:
-                paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','')
-##                dbg('unsigned paste text: "%s"' % paste_text)
-            else:
-                signed = False
-
-            # another special case: deal with insert-right fields when selection is empty and
-            # cursor is at end of field:
-####            dbg('field._insertRight?', field._insertRight)
-####            dbg('sel_start == edit_end?', sel_start == edit_end)
-####            dbg('sel_start', sel_start, 'sel_to', sel_to)
-            if field._insertRight and sel_start == edit_end and sel_start == sel_to:
-                sel_start -= len(paste_text)
-                if sel_start < 0:
-                    sel_start = 0
-##                dbg('adjusted selection:', (sel_start, sel_to))
-
-            try:
-                valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
-            except:
-##                dbg('exception thrown', indent=0)
-                raise
-
-            if not valid_paste:
-##                dbg('paste text not legal for the selection or portion of the control following the cursor;')
-                if not wx.Validator_IsSilent():
-                    wx.Bell()
-##                dbg(indent=0)
-                return False
-            # else...
-            text = self._eraseSelection()
-
-            new_text = text[:sel_start] + replacement_text + text[replace_to:]
-            if new_text:
-                new_text = string.ljust(new_text,self._masklength)
-            if signed:
-                new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text)
-                if new_text:
-                    if self._useParens:
-                        new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:]
-                    else:
-                        new_text = new_text[:signpos] + '-' + new_text[signpos+1:]
-                    if not self._isNeg:
-                        self._isNeg = 1
-
-##            dbg("new_text:", '"'+new_text+'"')
-
-            if not just_return_value:
-                if new_text != self._GetValue():
-                    self.modified = True
-                if new_text == '':
-                    self.ClearValue()
-                else:
-                    wx.CallAfter(self._SetValue, new_text)
-                    if new_pos is None:
-                        new_pos = sel_start + len(replacement_text)
-                    wx.CallAfter(self._SetInsertionPoint, new_pos)
-            else:
-##                dbg(indent=0)
-                return new_text
-        elif just_return_value:
-##            dbg(indent=0)
-            return self._GetValue()
-##        dbg(indent=0)
-
-    def _Undo(self):
-        """ Provides an Undo() method in base controls. """
-##        dbg("MaskedEditMixin::_Undo", indent=1)
-        value = self._GetValue()
-        prev = self._prevValue
-##        dbg('current value:  "%s"' % value)
-##        dbg('previous value: "%s"' % prev)
-        if prev is None:
-##            dbg('no previous value', indent=0)
-            return
-
-        elif value != prev:
-            # Determine what to select: (relies on fixed-length strings)
-            # (This is a lot harder than it would first appear, because
-            # of mask chars that stay fixed, and so break up the "diff"...)
-
-            # Determine where they start to differ:
-            i = 0
-            length = len(value)     # (both are same length in masked control)
-
-            while( value[:i] == prev[:i] ):
-                    i += 1
-            sel_start = i - 1
-
-
-            # handle signed values carefully, so undo from signed to unsigned or vice-versa
-            # works properly:
-            if self._signOk:
-                text, signpos, right_signpos = self._getSignedValue(candidate=prev)
-                if self._useParens:
-                    if prev[signpos] == '(' and prev[right_signpos] == ')':
-                        self._isNeg = True
-                    else:
-                        self._isNeg = False
-                    # eliminate source of "far-end" undo difference if using balanced parens:
-                    value = value.replace(')', ' ')
-                    prev = prev.replace(')', ' ')
-                elif prev[signpos] == '-':
-                    self._isNeg = True
-                else:
-                    self._isNeg = False
-
-            # Determine where they stop differing in "undo" result:
-            sm = difflib.SequenceMatcher(None, a=value, b=prev)
-            i, j, k = sm.find_longest_match(sel_start, length, sel_start, length)
-##            dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
-
-            if k == 0:                              # no match found; select to end
-                sel_to = length
-            else:
-                code_5tuples = sm.get_opcodes()
-                for op, i1, i2, j1, j2 in code_5tuples:
-##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2]))
-                    pass
-
-                diff_found = False
-                # look backward through operations needed to produce "previous" value;
-                # first change wins:
-                for next_op in range(len(code_5tuples)-1, -1, -1):
-                    op, i1, i2, j1, j2 = code_5tuples[next_op]
-##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2])
-                    if op == 'insert' and prev[j1:j2] != self._template[j1:j2]:
-##                        dbg('insert found: selection =>', (j1, j2))
-                        sel_start = j1
-                        sel_to = j2
-                        diff_found = True
-                        break
-                    elif op == 'delete' and value[i1:i2] != self._template[i1:i2]:
-                        field = self._FindField(i2)
-                        edit_start, edit_end = field._extent
-                        if field._insertRight and i2 == edit_end:
-                            sel_start = i2
-                            sel_to = i2
-                        else:
-                            sel_start = i1
-                            sel_to = j1
-##                        dbg('delete found: selection =>', (sel_start, sel_to))
-                        diff_found = True
-                        break
-                    elif op == 'replace':
-##                        dbg('replace found: selection =>', (j1, j2))
-                        sel_start = j1
-                        sel_to = j2
-                        diff_found = True
-                        break
-
-
-                if diff_found:
-                    # now go forwards, looking for earlier changes:
-                    for next_op in range(len(code_5tuples)):
-                        op, i1, i2, j1, j2 = code_5tuples[next_op]
-                        field = self._FindField(i1)
-                        if op == 'equal':
-                            continue
-                        elif op == 'replace':
-##                            dbg('setting sel_start to', i1)
-                            sel_start = i1
-                            break
-                        elif op == 'insert' and not value[i1:i2]:
-##                            dbg('forward %s found' % op)
-                            if prev[j1:j2].strip():
-##                                dbg('item to insert non-empty; setting sel_start to', j1)
-                                sel_start = j1
-                                break
-                            elif not field._insertRight:
-##                                dbg('setting sel_start to inserted space:', j1)
-                                sel_start = j1
-                                break
-                        elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip():
-                            continue
-                        else:
-                            # we've got what we need
-                            break
-
-
-                if not diff_found:
-##                    dbg('no insert,delete or replace found (!)')
-                    # do "left-insert"-centric processing of difference based on l.c.s.:
-                    if i == j and j != sel_start:         # match starts after start of selection
-                        sel_to = sel_start + (j-sel_start)  # select to start of match
-                    else:
-                        sel_to = j                          # (change ends at j)
-
-
-            # There are several situations where the calculated difference is
-            # not what we want to select.  If changing sign, or just adding
-            # group characters, we really don't want to highlight the characters
-            # changed, but instead leave the cursor where it is.
-            # Also, there a situations in which the difference can be ambiguous;
-            # Consider:
-            #
-            # current value:    11234
-            # previous value:   1111234
-            #
-            # Where did the cursor actually lie and which 1s were selected on the delete
-            # operation?
-            #
-            # Also, difflib can "get it wrong;" Consider:
-            #
-            # current value:    "       128.66"
-            # previous value:   "       121.86"
-            #
-            # difflib produces the following opcodes, which are sub-optimal:
-            #    equal value[0:9] (       12) prev[0:9] (       12)
-            #   insert value[9:9] () prev[9:11] (1.)
-            #    equal value[9:10] (8) prev[11:12] (8)
-            #   delete value[10:11] (.) prev[12:12] ()
-            #    equal value[11:12] (6) prev[12:13] (6)
-            #   delete value[12:13] (6) prev[13:13] ()
-            #
-            # This should have been:
-            #    equal value[0:9] (       12) prev[0:9] (       12)
-            #  replace value[9:11] (8.6) prev[9:11] (1.8)
-            #    equal value[12:13] (6) prev[12:13] (6)
-            #
-            # But it didn't figure this out!
-            #
-            # To get all this right, we use the previous selection recorded to help us...
-
-            if (sel_start, sel_to) != self._prevSelection:
-##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection)
-
-                prev_sel_start, prev_sel_to = self._prevSelection
-                field = self._FindField(sel_start)
-
-                if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')')
-                                     or self._curValue[sel_start] in ('-', '(', ')')):
-                    # change of sign; leave cursor alone...
-                    sel_start, sel_to = self._prevSelection
-
-                elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar
-                                             or self._prevValue[sel_start:sel_to] == field._groupChar):
-                    # do not highlight grouping changes
-                    sel_start, sel_to = self._prevSelection
-
-                else:
-                    calc_select_len = sel_to - sel_start
-                    prev_select_len = prev_sel_to - prev_sel_start
-
-##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start)
-##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to)
-
-                    if prev_select_len >= calc_select_len:
-                        # old selection was bigger; trust it:
-                        sel_start, sel_to = self._prevSelection
-
-                    elif( sel_to > prev_sel_to                  # calculated select past last selection
-                          and prev_sel_to < len(self._template) # and prev_sel_to not at end of control
-                          and sel_to == len(self._template) ):  # and calculated selection goes to end of control
-
-                        i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length)
-##                        dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
-                        if k > 0:
-                            # difflib must not have optimized opcodes properly;
-                            sel_to = j
-
-                    else:
-                        # look for possible ambiguous diff:
-
-                        # if last change resulted in no selection, test from resulting cursor position:
-                        if prev_sel_start == prev_sel_to:
-                            calc_select_len = sel_to - sel_start
-                            field = self._FindField(prev_sel_start)
-
-                            # determine which way to search from last cursor position for ambiguous change:
-                            if field._insertRight:
-                                test_sel_start = prev_sel_start
-                                test_sel_to = prev_sel_start + calc_select_len
-                            else:
-                                test_sel_start = prev_sel_start - calc_select_len
-                                test_sel_to = prev_sel_start
-                        else:
-                            test_sel_start, test_sel_to = prev_sel_start, prev_sel_to
-
-##                        dbg('test selection:', (test_sel_start, test_sel_to))
-##                        dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to])
-##                        dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to])
-
-                        # if calculated selection spans characters, and same characters
-                        # "before" the previous insertion point are present there as well,
-                        # select the ones related to the last known selection instead.
-                        if( sel_start != sel_to
-                            and test_sel_to < len(self._template)
-                            and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ):
-
-                            sel_start, sel_to = test_sel_start, test_sel_to
-
-##            dbg('sel_start, sel_to:', sel_start, sel_to)
-##            dbg('previous value: "%s"' % self._prevValue)
-            self._SetValue(self._prevValue)
-            self._SetInsertionPoint(sel_start)
-            self._SetSelection(sel_start, sel_to)
-        else:
-##            dbg('no difference between previous value')
-            pass
-##        dbg(indent=0)
-
-
-    def _OnClear(self, event):
-        """ Provides an action for context menu delete operation """
-        self.ClearValue()
-
-
-    def _OnContextMenu(self, event):
-##        dbg('MaskedEditMixin::OnContextMenu()', indent=1)
-        menu = wxMenu()
-        menu.Append(wxID_UNDO, "Undo", "")
-        menu.AppendSeparator()
-        menu.Append(wxID_CUT, "Cut", "")
-        menu.Append(wxID_COPY, "Copy", "")
-        menu.Append(wxID_PASTE, "Paste", "")
-        menu.Append(wxID_CLEAR, "Delete", "")
-        menu.AppendSeparator()
-        menu.Append(wxID_SELECTALL, "Select All", "")
-
-        EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z)
-        EVT_MENU(menu, wxID_CUT, self._OnCtrl_X)
-        EVT_MENU(menu, wxID_COPY, self._OnCtrl_C)
-        EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V)
-        EVT_MENU(menu, wxID_CLEAR, self._OnClear)
-        EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A)
-
-        # ## WSS: The base control apparently handles
-        # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE
-        # and wxID_CLEAR menu items even if the menu is one
-        # we created.  However, it doesn't do undo properly,
-        # so we're keeping track of previous values ourselves.
-        # Therefore, we have to override the default update for
-        # that item on the menu:
-        EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI)
-        self._contextMenu = menu
-
-        self.PopupMenu(menu, event.GetPosition())
-        menu.Destroy()
-        self._contextMenu = None
-##        dbg(indent=0)
-
-    def _UndoUpdateUI(self, event):
-        if self._prevValue is None or self._prevValue == self._curValue:
-            self._contextMenu.Enable(wxID_UNDO, False)
-        else:
-            self._contextMenu.Enable(wxID_UNDO, True)
-
-
-    def _OnCtrlParametersChanged(self):
-        """
-        Overridable function to allow derived classes to take action as a
-        result of parameter changes prior to possibly changing the value
-        of the control.
-        """
-        pass
-
- ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-# ## TRICKY BIT: to avoid a ton of boiler-plate, and to
-# ## automate the getter/setter generation for each valid
-# ## control parameter so we never forget to add the
-# ## functions when adding parameters, this loop
-# ## programmatically adds them to the class:
-# ## (This makes it easier for Designers like Boa to
-# ## deal with masked controls.)
-#
-# ## To further complicate matters, this is done with an
-# ## extra level of inheritance, so that "general" classes like
-# ## MaskedTextCtrl can have all possible attributes,
-# ## while derived classes, like TimeCtrl and MaskedNumCtrl
-# ## can prevent exposure of those optional attributes of their base
-# ## class that do not make sense for their derivation.  Therefore,
-# ## we define
-# ##    BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin)
-# ## and
-# ##    MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin).
-# ##
-# ## This allows us to then derive:
-# ##    MaskedNumCtrl( BaseMaskedTextCtrl )
-# ##
-# ## and not have to expose all the same accessor functions for the
-# ## derived control when they don't all make sense for it.
-# ##
-class MaskedEditAccessorsMixin:
-
-    # Define the default set of attributes exposed by the most generic masked controls:
-    exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys()
-    exposed_basectrl_params.remove('index')
-    exposed_basectrl_params.remove('extent')
-    exposed_basectrl_params.remove('foregroundColour')   # (base class already has this)
-
-    for param in exposed_basectrl_params:
-        propname = param[0].upper() + param[1:]
-        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-        if param.find('Colour') != -1:
-            # add non-british spellings, for backward-compatibility
-            propname.replace('Colour', 'Color')
-
-            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-
-
-
-class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
-    """
-    This is the primary derivation from MaskedEditMixin.  It provides
-    a general masked text control that can be configured with different
-    masks.  It's actually a "base masked textCtrl", so that the
-    MaskedTextCtrl class can be derived from it, and add those
-    accessor functions to it that are appropriate to the general class,
-    whilst other classes can derive from BaseMaskedTextCtrl, and
-    only define those accessor functions that are appropriate for
-    those derivations.
-    """
-
-    def __init__( self, parent, id=-1, value = '',
-                  pos = wx.DefaultPosition,
-                  size = wx.DefaultSize,
-                  style = wx.TE_PROCESS_TAB,
-                  validator=wx.DefaultValidator,     ## placeholder provided for data-transfer logic
-                  name = 'maskedTextCtrl',
-                  setupEventHandling = True,        ## setup event handling by default
-                  **kwargs):
-
-        wx.TextCtrl.__init__(self, parent, id, value='',
-                            pos=pos, size = size,
-                            style=style, validator=validator,
-                            name=name)
-
-        self.controlInitialized = True
-        MaskedEditMixin.__init__( self, name, **kwargs )
-
-        self._SetInitialValue(value)
-
-        if setupEventHandling:
-            ## Setup event handlers
-            self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
-            self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
-            self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
-            self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
-            self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab.
-            self.Bind(wx.EVT_CHAR, self._OnChar )               ## handle each keypress
-            self.Bind(wx.EVT_TEXT, self._OnTextChange )         ## color control appropriately & keep
-                                                                ## track of previous value for undo
-
-
-    def __repr__(self):
-        return "<BaseMaskedTextCtrl: %s>" % self.GetValue()
-
-
-    def _GetSelection(self):
-        """
-        Allow mixin to get the text selection of this control.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return self.GetSelection()
-
-    def _SetSelection(self, sel_start, sel_to):
-        """
-        Allow mixin to set the text selection of this control.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-####        dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
-        return self.SetSelection( sel_start, sel_to )
-
-    def SetSelection(self, sel_start, sel_to):
-        """
-        This is just for debugging...
-        """
-##        dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
-        wx.TextCtrl.SetSelection(self, sel_start, sel_to)
-
-
-    def _GetInsertionPoint(self):
-        return self.GetInsertionPoint()
-
-    def _SetInsertionPoint(self, pos):
-####        dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals())
-        self.SetInsertionPoint(pos)
-
-    def SetInsertionPoint(self, pos):
-        """
-        This is just for debugging...
-        """
-##        dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals())
-        wx.TextCtrl.SetInsertionPoint(self, pos)
-
-
-    def _GetValue(self):
-        """
-        Allow mixin to get the raw value of the control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return self.GetValue()
-
-    def _SetValue(self, value):
-        """
-        Allow mixin to set the raw value of the control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-##        dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
-        # Record current selection and insertion point, for undo
-        self._prevSelection = self._GetSelection()
-        self._prevInsertionPoint = self._GetInsertionPoint()
-        wx.TextCtrl.SetValue(self, value)
-##        dbg(indent=0)
-
-    def SetValue(self, value):
-        """
-        This function redefines the externally accessible .SetValue to be
-        a smart "paste" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-##        dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1)
-
-        if not self._mask:
-            wx.TextCtrl.SetValue(self, value)    # revert to base control behavior
-            return
-
-        # empty previous contents, replacing entire value:
-        self._SetInsertionPoint(0)
-        self._SetSelection(0, self._masklength)
-        if self._signOk and self._useParens:
-            signpos = value.find('-')
-            if signpos != -1:
-                value = value[:signpos] + '(' + value[signpos+1:].strip() + ')'
-            elif value.find(')') == -1 and len(value) < self._masklength:
-                value += ' '    # add place holder for reserved space for right paren
-
-        if( len(value) < self._masklength                # value shorter than control
-            and (self._isFloat or self._isInt)            # and it's a numeric control
-            and self._ctrl_constraints._alignRight ):   # and it's a right-aligned control
-
-##            dbg('len(value)', len(value), ' < self._masklength', self._masklength)
-            # try to intelligently "pad out" the value to the right size:
-            value = self._template[0:self._masklength - len(value)] + value
-            if self._isFloat and value.find('.') == -1:
-                value = value[1:]
-##            dbg('padded value = "%s"' % value)
-
-        # make SetValue behave the same as if you had typed the value in:
-        try:
-            value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
-            if self._isFloat:
-                self._isNeg = False     # (clear current assumptions)
-                value = self._adjustFloat(value)
-            elif self._isInt:
-                self._isNeg = False     # (clear current assumptions)
-                value = self._adjustInt(value)
-            elif self._isDate and not self.IsValid(value) and self._4digityear:
-                value = self._adjustDate(value, fixcentury=True)
-        except ValueError:
-            # If date, year might be 2 digits vs. 4; try adjusting it:
-            if self._isDate and self._4digityear:
-                dateparts = value.split(' ')
-                dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
-                value = string.join(dateparts, ' ')
-##                dbg('adjusted value: "%s"' % value)
-                value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
-            else:
-##                dbg('exception thrown', indent=0)
-                raise
-
-        self._SetValue(value)   # note: to preserve similar capability, .SetValue()
-                                # does not change IsModified()
-####        dbg('queuing insertion after .SetValue', self._masklength)
-        wx.CallAfter(self._SetInsertionPoint, self._masklength)
-        wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
-##        dbg(indent=0)
-
-
-    def Clear(self):
-        """ Blanks the current control value by replacing it with the default value."""
-##        dbg("MaskedTextCtrl::Clear - value reset to default value (template)")
-        if self._mask:
-            self.ClearValue()
-        else:
-            wx.TextCtrl.Clear(self)    # else revert to base control behavior
-
-
-    def _Refresh(self):
-        """
-        Allow mixin to refresh the base control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-##        dbg('MaskedTextCtrl::_Refresh', indent=1)
-        wx.TextCtrl.Refresh(self)
-##        dbg(indent=0)
-
-
-    def Refresh(self):
-        """
-        This function redefines the externally accessible .Refresh() to
-        validate the contents of the masked control as it refreshes.
-        NOTE: this must be done in the class derived from the base wx control.
-        """
-##        dbg('MaskedTextCtrl::Refresh', indent=1)
-        self._CheckValid()
-        self._Refresh()
-##        dbg(indent=0)
-
-
-    def _IsEditable(self):
-        """
-        Allow mixin to determine if the base control is editable with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return wx.TextCtrl.IsEditable(self)
-
-
-    def Cut(self):
-        """
-        This function redefines the externally accessible .Cut to be
-        a smart "erase" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-        if self._mask:
-            self._Cut()             # call the mixin's Cut method
-        else:
-            wx.TextCtrl.Cut(self)    # else revert to base control behavior
-
-
-    def Paste(self):
-        """
-        This function redefines the externally accessible .Paste to be
-        a smart "paste" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-        if self._mask:
-            self._Paste()                   # call the mixin's Paste method
-        else:
-            wx.TextCtrl.Paste(self, value)   # else revert to base control behavior
-
-
-    def Undo(self):
-        """
-        This function defines the undo operation for the control. (The default
-        undo is 1-deep.)
-        """
-        if self._mask:
-            self._Undo()
-        else:
-            wx.TextCtrl.Undo(self)   # else revert to base control behavior
-
-
-    def IsModified(self):
-        """
-        This function overrides the raw wxTextCtrl method, because the
-        masked edit mixin uses SetValue to change the value, which doesn't
-        modify the state of this attribute.  So, we keep track on each
-        keystroke to see if the value changes, and if so, it's been
-        modified.
-        """
-        return wx.TextCtrl.IsModified(self) or self.modified
-
-
-    def _CalcSize(self, size=None):
-        """
-        Calculate automatic size if allowed; use base mixin function.
-        """
-        return self._calcSize(size)
-
-
-class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
-    """
-    This extra level of inheritance allows us to add the generic set of
-    masked edit parameters only to this class while allowing other
-    classes to derive from the "base" masked text control, and provide
-    a smaller set of valid accessor functions.
-    """
-    pass
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
-## events, we have to do it ourselves when we auto-complete.
-class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
-    def __init__(self, id, selection = 0, object=None):
-        wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id)
-
-        self.__selection = selection
-        self.SetEventObject(object)
-
-    def GetSelection(self):
-        """Retrieve the value of the control at the time
-        this event was generated."""
-        return self.__selection
-
-
-class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
-    """
-    This masked edit control adds the ability to use a masked input
-    on a combobox, and do auto-complete of such values.
-    """
-    def __init__( self, parent, id=-1, value = '',
-                  pos = wx.DefaultPosition,
-                  size = wx.DefaultSize,
-                  choices = [],
-                  style = wx.CB_DROPDOWN,
-                  validator = wx.DefaultValidator,
-                  name = "maskedComboBox",
-                  setupEventHandling = True,        ## setup event handling by default):
-                  **kwargs):
-
-
-        # This is necessary, because wxComboBox currently provides no
-        # method for determining later if this was specified in the
-        # constructor for the control...
-        self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY
-
-        kwargs['choices'] = choices                 ## set up maskededit to work with choice list too
-
-        ## Since combobox completion is case-insensitive, always validate same way
-        if not kwargs.has_key('compareNoCase'):
-            kwargs['compareNoCase'] = True
-
-        MaskedEditMixin.__init__( self, name, **kwargs )
-
-        self._choices = self._ctrl_constraints._choices
-##        dbg('self._choices:', self._choices)
-
-        if self._ctrl_constraints._alignRight:
-            choices = [choice.rjust(self._masklength) for choice in choices]
-        else:
-            choices = [choice.ljust(self._masklength) for choice in choices]
-
-        wx.ComboBox.__init__(self, parent, id, value='',
-                            pos=pos, size = size,
-                            choices=choices, style=style|wx.WANTS_CHARS,
-                            validator=validator,
-                            name=name)
-
-        self.controlInitialized = True
-
-        # Set control font - fixed width by default
-        self._setFont()
-
-        if self._autofit:
-            self.SetClientSize(self._CalcSize())
-
-        if value:
-            # ensure value is width of the mask of the control:
-            if self._ctrl_constraints._alignRight:
-                value = value.rjust(self._masklength)
-            else:
-                value = value.ljust(self._masklength)
-
-        if self.__readonly:
-            self.SetStringSelection(value)
-        else:
-            self._SetInitialValue(value)
-
-
-        self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice)
-        self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice)
-
-        if setupEventHandling:
-            ## Setup event handlers
-            self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
-            self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
-            self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
-            self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
-            self.Bind(wx.EVT_CHAR, self._OnChar )               ## handle each keypress
-            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown )         ## for special processing of up/down keys
-            self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## for processing the rest of the control keys
-                                                                ## (next in evt chain)
-            self.Bind(wx.EVT_TEXT, self._OnTextChange )         ## color control appropriately & keep
-                                                                ## track of previous value for undo
-
-
-
-    def __repr__(self):
-        return "<MaskedComboBox: %s>" % self.GetValue()
-
-
-    def _CalcSize(self, size=None):
-        """
-        Calculate automatic size if allowed; augment base mixin function
-        to account for the selector button.
-        """
-        size = self._calcSize(size)
-        return (size[0]+20, size[1])
-
-
-    def _GetSelection(self):
-        """
-        Allow mixin to get the text selection of this control.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return self.GetMark()
-
-    def _SetSelection(self, sel_start, sel_to):
-        """
-        Allow mixin to set the text selection of this control.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return self.SetMark( sel_start, sel_to )
-
-
-    def _GetInsertionPoint(self):
-        return self.GetInsertionPoint()
-
-    def _SetInsertionPoint(self, pos):
-        self.SetInsertionPoint(pos)
-
-
-    def _GetValue(self):
-        """
-        Allow mixin to get the raw value of the control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return self.GetValue()
-
-    def _SetValue(self, value):
-        """
-        Allow mixin to set the raw value of the control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        # For wxComboBox, ensure that values are properly padded so that
-        # if varying length choices are supplied, they always show up
-        # in the window properly, and will be the appropriate length
-        # to match the mask:
-        if self._ctrl_constraints._alignRight:
-            value = value.rjust(self._masklength)
-        else:
-            value = value.ljust(self._masklength)
-
-        # Record current selection and insertion point, for undo
-        self._prevSelection = self._GetSelection()
-        self._prevInsertionPoint = self._GetInsertionPoint()
-        wx.ComboBox.SetValue(self, value)
-        # text change events don't always fire, so we check validity here
-        # to make certain formatting is applied:
-        self._CheckValid()
-
-    def SetValue(self, value):
-        """
-        This function redefines the externally accessible .SetValue to be
-        a smart "paste" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-        if not self._mask:
-            wx.ComboBox.SetValue(value)   # revert to base control behavior
-            return
-        # else...
-        # empty previous contents, replacing entire value:
-        self._SetInsertionPoint(0)
-        self._SetSelection(0, self._masklength)
-
-        if( len(value) < self._masklength                # value shorter than control
-            and (self._isFloat or self._isInt)            # and it's a numeric control
-            and self._ctrl_constraints._alignRight ):   # and it's a right-aligned control
-            # try to intelligently "pad out" the value to the right size:
-            value = self._template[0:self._masklength - len(value)] + value
-##            dbg('padded value = "%s"' % value)
-
-        # For wxComboBox, ensure that values are properly padded so that
-        # if varying length choices are supplied, they always show up
-        # in the window properly, and will be the appropriate length
-        # to match the mask:
-        elif self._ctrl_constraints._alignRight:
-            value = value.rjust(self._masklength)
-        else:
-            value = value.ljust(self._masklength)
-
-
-        # make SetValue behave the same as if you had typed the value in:
-        try:
-            value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
-            if self._isFloat:
-                self._isNeg = False     # (clear current assumptions)
-                value = self._adjustFloat(value)
-            elif self._isInt:
-                self._isNeg = False     # (clear current assumptions)
-                value = self._adjustInt(value)
-            elif self._isDate and not self.IsValid(value) and self._4digityear:
-                value = self._adjustDate(value, fixcentury=True)
-        except ValueError:
-            # If date, year might be 2 digits vs. 4; try adjusting it:
-            if self._isDate and self._4digityear:
-                dateparts = value.split(' ')
-                dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
-                value = string.join(dateparts, ' ')
-##                dbg('adjusted value: "%s"' % value)
-                value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
-            else:
-                raise
-
-        self._SetValue(value)
-####        dbg('queuing insertion after .SetValue', self._masklength)
-        wx.CallAfter(self._SetInsertionPoint, self._masklength)
-        wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
-
-
-    def _Refresh(self):
-        """
-        Allow mixin to refresh the base control with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        wx.ComboBox.Refresh(self)
-
-    def Refresh(self):
-        """
-        This function redefines the externally accessible .Refresh() to
-        validate the contents of the masked control as it refreshes.
-        NOTE: this must be done in the class derived from the base wx control.
-        """
-        self._CheckValid()
-        self._Refresh()
-
-
-    def _IsEditable(self):
-        """
-        Allow mixin to determine if the base control is editable with this function.
-        REQUIRED by any class derived from MaskedEditMixin.
-        """
-        return not self.__readonly
-
-
-    def Cut(self):
-        """
-        This function redefines the externally accessible .Cut to be
-        a smart "erase" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-        if self._mask:
-            self._Cut()             # call the mixin's Cut method
-        else:
-            wx.ComboBox.Cut(self)    # else revert to base control behavior
-
-
-    def Paste(self):
-        """
-        This function redefines the externally accessible .Paste to be
-        a smart "paste" of the text in question, so as not to corrupt the
-        masked control.  NOTE: this must be done in the class derived
-        from the base wx control.
-        """
-        if self._mask:
-            self._Paste()           # call the mixin's Paste method
-        else:
-            wx.ComboBox.Paste(self)  # else revert to base control behavior
-
-
-    def Undo(self):
-        """
-        This function defines the undo operation for the control. (The default
-        undo is 1-deep.)
-        """
-        if self._mask:
-            self._Undo()
-        else:
-            wx.ComboBox.Undo()       # else revert to base control behavior
-
-
-    def Append( self, choice, clientData=None ):
-        """
-        This function override is necessary so we can keep track of any additions to the list
-        of choices, because wxComboBox doesn't have an accessor for the choice list.
-        The code here is the same as in the SetParameters() mixin function, but is
-        done for the individual value as appended, so the list can be built incrementally
-        without speed penalty.
-        """
-        if self._mask:
-            if type(choice) not in (types.StringType, types.UnicodeType):
-                raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
-            elif not self.IsValid(choice):
-                raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
-
-            if not self._ctrl_constraints._choices:
-                self._ctrl_constraints._compareChoices = []
-                self._ctrl_constraints._choices = []
-                self._hasList = True
-
-            compareChoice = choice.strip()
-
-            if self._ctrl_constraints._compareNoCase:
-                compareChoice = compareChoice.lower()
-
-            if self._ctrl_constraints._alignRight:
-                choice = choice.rjust(self._masklength)
-            else:
-                choice = choice.ljust(self._masklength)
-            if self._ctrl_constraints._fillChar != ' ':
-                choice = choice.replace(' ', self._fillChar)
-##            dbg('updated choice:', choice)
-
-
-            self._ctrl_constraints._compareChoices.append(compareChoice)
-            self._ctrl_constraints._choices.append(choice)
-            self._choices = self._ctrl_constraints._choices     # (for shorthand)
-
-            if( not self.IsValid(choice) and
-               (not self._ctrl_constraints.IsEmpty(choice) or
-                (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ):
-                raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name))
-
-        wx.ComboBox.Append(self, choice, clientData)
-
-
-
-    def Clear( self ):
-        """
-        This function override is necessary so we can keep track of any additions to the list
-        of choices, because wxComboBox doesn't have an accessor for the choice list.
-        """
-        if self._mask:
-            self._choices = []
-            self._ctrl_constraints._autoCompleteIndex = -1
-            if self._ctrl_constraints._choices:
-                self.SetCtrlParameters(choices=[])
-        wx.ComboBox.Clear(self)
-
-
-    def _OnCtrlParametersChanged(self):
-        """
-        Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
-        we can update the base control:
-        """
-        if self.controlInitialized and self._choices != self._ctrl_constraints._choices:
-            wx.ComboBox.Clear(self)
-            self._choices = self._ctrl_constraints._choices
-            for choice in self._choices:
-                wx.ComboBox.Append( self, choice )
-
-
-    def GetMark(self):
-        """
-        This function is a hack to make up for the fact that wxComboBox has no
-        method for returning the selected portion of its edit control.  It
-        works, but has the nasty side effect of generating lots of intermediate
-        events.
-        """
-##        dbg(suspend=1)  # turn off debugging around this function
-##        dbg('MaskedComboBox::GetMark', indent=1)
-        if self.__readonly:
-##            dbg(indent=0)
-            return 0, 0 # no selection possible for editing
-##        sel_start, sel_to = wxComboBox.GetMark(self)        # what I'd *like* to have!
-        sel_start = sel_to = self.GetInsertionPoint()
-##        dbg("current sel_start:", sel_start)
-        value = self.GetValue()
-##        dbg('value: "%s"' % value)
-
-        self._ignoreChange = True               # tell _OnTextChange() to ignore next event (if any)
-
-        wx.ComboBox.Cut(self)
-        newvalue = self.GetValue()
-##        dbg("value after Cut operation:", newvalue)
-
-        if newvalue != value:                   # something was selected; calculate extent
-##            dbg("something selected")
-            sel_to = sel_start + len(value) - len(newvalue)
-            wx.ComboBox.SetValue(self, value)    # restore original value and selection (still ignoring change)
-            wx.ComboBox.SetInsertionPoint(self, sel_start)
-            wx.ComboBox.SetMark(self, sel_start, sel_to)
-
-        self._ignoreChange = False              # tell _OnTextChange() to pay attn again
-
-##        dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
-        return sel_start, sel_to
-
-
-    def SetSelection(self, index):
-        """
-        Necessary for bookkeeping on choice selection, to keep current value
-        current.
-        """
-##        dbg('MaskedComboBox::SetSelection(%d)' % index)
-        if self._mask:
-            self._prevValue = self._curValue
-            self._curValue = self._choices[index]
-            self._ctrl_constraints._autoCompleteIndex = index
-        wx.ComboBox.SetSelection(self, index)
-
-
-    def OnKeyDown(self, event):
-        """
-        This function is necessary because navigation and control key
-        events do not seem to normally be seen by the wxComboBox's
-        EVT_CHAR routine.  (Tabs don't seem to be visible no matter
-        what... {:-( )
-        """
-        if event.GetKeyCode() in self._nav + self._control:
-            self._OnChar(event)
-            return
-        else:
-            event.Skip()    # let mixin default KeyDown behavior occur
-
-
-    def OnSelectChoice(self, event):
-        """
-        This function appears to be necessary, because the processing done
-        on the text of the control somehow interferes with the combobox's
-        selection mechanism for the arrow keys.
-        """
-##        dbg('MaskedComboBox::OnSelectChoice', indent=1)
-
-        if not self._mask:
-            event.Skip()
-            return
-
-        value = self.GetValue().strip()
-
-        if self._ctrl_constraints._compareNoCase:
-            value = value.lower()
-
-        if event.GetKeyCode() == wx.WXK_UP:
-            direction = -1
-        else:
-            direction = 1
-        match_index, partial_match = self._autoComplete(
-                                                direction,
-                                                self._ctrl_constraints._compareChoices,
-                                                value,
-                                                self._ctrl_constraints._compareNoCase,
-                                                current_index = self._ctrl_constraints._autoCompleteIndex)
-        if match_index is not None:
-##            dbg('setting selection to', match_index)
-            # issue appropriate event to outside:
-            self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
-            self._CheckValid()
-            keep_processing = False
-        else:
-            pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-            field = self._FindField(pos)
-            if self.IsEmpty() or not field._hasList:
-##                dbg('selecting 1st value in list')
-                self._OnAutoSelect(self._ctrl_constraints, match_index=0)
-                self._CheckValid()
-                keep_processing = False
-            else:
-                # attempt field-level auto-complete
-##                dbg(indent=0)
-                keep_processing = self._OnAutoCompleteField(event)
-##        dbg('keep processing?', keep_processing, indent=0)
-        return keep_processing
-
-
-    def _OnAutoSelect(self, field, match_index):
-        """
-        Override mixin (empty) autocomplete handler, so that autocompletion causes
-        combobox to update appropriately.
-        """
-##        dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
-##        field._autoCompleteIndex = match_index
-        if field == self._ctrl_constraints:
-            self.SetSelection(match_index)
-##            dbg('issuing combo selection event')
-            self.GetEventHandler().ProcessEvent(
-                MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
-        self._CheckValid()
-##        dbg('field._autoCompleteIndex:', match_index)
-##        dbg('self.GetSelection():', self.GetSelection())
-##        dbg(indent=0)
-
-
-    def _OnReturn(self, event):
-        """
-        For wxComboBox, it seems that if you hit return when the dropdown is
-        dropped, the event that dismisses the dropdown will also blank the
-        control, because of the implementation of wxComboBox.  So here,
-        we look and if the selection is -1, and the value according to
-        (the base control!) is a value in the list, then we schedule a
-        programmatic wxComboBox.SetSelection() call to pick the appropriate
-        item in the list. (and then do the usual OnReturn bit.)
-        """
-##        dbg('MaskedComboBox::OnReturn', indent=1)
-##        dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
-        if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
-            wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
-
-        event.m_keyCode = wx.WXK_TAB
-        event.Skip()
-##        dbg(indent=0)
-
-
-class MaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
-    """
-    This extra level of inheritance allows us to add the generic set of
-    masked edit parameters only to this class while allowing other
-    classes to derive from the "base" masked combobox control, and provide
-    a smaller set of valid accessor functions.
-    """
-    pass
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class IpAddrCtrlAccessorsMixin:
-    # Define IpAddrCtrl's list of attributes having their own
-    # Get/Set functions, exposing only those that make sense for
-    # an IP address control.
-
-    exposed_basectrl_params = (
-        'fields',
-        'retainFieldValidation',
-        'formatcodes',
-        'fillChar',
-        'defaultValue',
-        'description',
-
-        'useFixedWidthFont',
-        'signedForegroundColour',
-        'emptyBackgroundColour',
-        'validBackgroundColour',
-        'invalidBackgroundColour',
-
-        'emptyInvalid',
-        'validFunc',
-        'validRequired',
-        )
-
-    for param in exposed_basectrl_params:
-        propname = param[0].upper() + param[1:]
-        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-        if param.find('Colour') != -1:
-            # add non-british spellings, for backward-compatibility
-            propname.replace('Colour', 'Color')
-
-            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-
-class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
-    """
-    This class is a particular type of MaskedTextCtrl that accepts
-    and understands the semantics of IP addresses, reformats input
-    as you move from field to field, and accepts '.' as a navigation
-    character, so that typing an IP address can be done naturally.
-    """
-
-
-
-    def __init__( self, parent, id=-1, value = '',
-                  pos = wx.DefaultPosition,
-                  size = wx.DefaultSize,
-                  style = wx.TE_PROCESS_TAB,
-                  validator = wx.DefaultValidator,
-                  name = 'IpAddrCtrl',
-                  setupEventHandling = True,        ## setup event handling by default
-                  **kwargs):
-
-        if not kwargs.has_key('mask'):
-           kwargs['mask'] = mask = "###.###.###.###"
-        if not kwargs.has_key('formatcodes'):
-            kwargs['formatcodes'] = 'F_Sr<'
-        if not kwargs.has_key('validRegex'):
-            kwargs['validRegex'] = "(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(  \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}"
-
-
-        BaseMaskedTextCtrl.__init__(
-                self, parent, id=id, value = value,
-                pos=pos, size=size,
-                style = style,
-                validator = validator,
-                name = name,
-                setupEventHandling = setupEventHandling,
-                **kwargs)
-
-
-        # set up individual field parameters as well:
-        field_params = {}
-        field_params['validRegex'] = "(   |  \d| \d |\d  | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))"
-
-        # require "valid" string; this prevents entry of any value > 255, but allows
-        # intermediate constructions; overall control validation requires well-formatted value.
-        field_params['formatcodes'] = 'V'
-
-        if field_params:
-            for i in self._field_indices:
-                self.SetFieldParameters(i, **field_params)
-
-        # This makes '.' act like tab:
-        self._AddNavKey('.', handler=self.OnDot)
-        self._AddNavKey('>', handler=self.OnDot)    # for "shift-."
-
-
-    def OnDot(self, event):
-##        dbg('IpAddrCtrl::OnDot', indent=1)
-        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-        oldvalue = self.GetValue()
-        edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
-        if not event.ShiftDown():
-            if pos > edit_start and pos < edit_end:
-                # clip data in field to the right of pos, if adjusting fields
-                # when not at delimeter; (assumption == they hit '.')
-                newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:]
-                self._SetValue(newvalue)
-                self._SetInsertionPoint(pos)
-##        dbg(indent=0)
-        return self._OnChangeField(event)
-
-
-
-    def GetAddress(self):
-        value = BaseMaskedTextCtrl.GetValue(self)
-        return value.replace(' ','')    # remove spaces from the value
-
-
-    def _OnCtrl_S(self, event):
-##        dbg("IpAddrCtrl::_OnCtrl_S")
-        if self._demo:
-            print "value:", self.GetAddress()
-        return False
-
-    def SetValue(self, value):
-##        dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1)
-        if type(value) not in (types.StringType, types.UnicodeType):
-##            dbg(indent=0)
-            raise ValueError('%s must be a string', str(value))
-
-        bValid = True   # assume True
-        parts = value.split('.')
-        if len(parts) != 4:
-            bValid = False
-        else:
-            for i in range(4):
-                part = parts[i]
-                if not 0 <= len(part) <= 3:
-                    bValid = False
-                    break
-                elif part.strip():  # non-empty part
-                    try:
-                        j = string.atoi(part)
-                        if not 0 <= j <= 255:
-                            bValid = False
-                            break
-                        else:
-                            parts[i] = '%3d' % j
-                    except:
-                        bValid = False
-                        break
-                else:
-                    # allow empty sections for SetValue (will result in "invalid" value,
-                    # but this may be useful for initializing the control:
-                    parts[i] = '   '    # convert empty field to 3-char length
-
-        if not bValid:
-##            dbg(indent=0)
-            raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value))
-        else:
-##            dbg('parts:', parts)
-            value = string.join(parts, '.')
-            BaseMaskedTextCtrl.SetValue(self, value)
-##        dbg(indent=0)
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-## these are helper subroutines:
-
-def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '):
-    """ addseparators = add separator character every three numerals if True
-    """
-    fmt0 = fmtstring.split('.')
-    fmt1 = fmt0[0]
-    fmt2 = fmt0[1]
-    val  = origvalue.split('.')[0].strip()
-    ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2)
-    if neg:
-        ret = '-' + ret[1:]
-    return (ret,len(fmt1))
-
-
-def isDateType( fmtstring ):
-    """ Checks the mask and returns True if it fits an allowed
-        date or datetime format.
-    """
-    dateMasks = ("^##/##/####",
-                 "^##-##-####",
-                 "^##.##.####",
-                 "^####/##/##",
-                 "^####-##-##",
-                 "^####.##.##",
-                 "^##/CCC/####",
-                 "^##.CCC.####",
-                 "^##/##/##$",
-                 "^##/##/## ",
-                 "^##/CCC/##$",
-                 "^##.CCC.## ",)
-    reString  = "|".join(dateMasks)
-    filter = re.compile( reString)
-    if re.match(filter,fmtstring): return True
-    return False
-
-def isTimeType( fmtstring ):
-    """ Checks the mask and returns True if it fits an allowed
-        time format.
-    """
-    reTimeMask = "^##:##(:##)?( (AM|PM))?"
-    filter = re.compile( reTimeMask )
-    if re.match(filter,fmtstring): return True
-    return False
-
-
-def isFloatingPoint( fmtstring):
-    filter = re.compile("[ ]?[#]+\.[#]+\n")
-    if re.match(filter,fmtstring+"\n"): return True
-    return False
-
-
-def isInteger( fmtstring ):
-    filter = re.compile("[#]+\n")
-    if re.match(filter,fmtstring+"\n"): return True
-    return False
-
-
-def getDateParts( dateStr, dateFmt ):
-    if len(dateStr) > 11: clip = dateStr[0:11]
-    else:                 clip = dateStr
-    if clip[-2] not in string.digits:
-        clip = clip[:-1]    # (got part of time; drop it)
-
-    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
-    slices  = clip.split(dateSep)
-    if dateFmt == "MDY":
-        y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts
-    elif dateFmt == "DMY":
-        y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts
-    elif dateFmt == "YMD":
-        y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts
-    else:
-        y,m,d = None, None, None
-    if not y:
-        return None
-    else:
-        return y,m,d
-
-
-def getDateSepChar(dateStr):
-    clip   = dateStr[0:10]
-    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
-    return dateSep
-
-
-def makeDate( year, month, day, dateFmt, dateStr):
-    sep    = getDateSepChar( dateStr)
-    if dateFmt == "MDY":
-        return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts
-    elif dateFmt == "DMY":
-        return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts
-    elif dateFmt == "YMD":
-        return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts
-    else:
-        return none
-
-
-def getYear(dateStr,dateFmt):
-    parts = getDateParts( dateStr, dateFmt)
-    return parts[0]
-
-def getMonth(dateStr,dateFmt):
-    parts = getDateParts( dateStr, dateFmt)
-    return parts[1]
-
-def getDay(dateStr,dateFmt):
-    parts = getDateParts( dateStr, dateFmt)
-    return parts[2]
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-class test(wx.PySimpleApp):
-        def OnInit(self):
-            from wx.lib.rcsizer import RowColSizer
-            self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
-            self.panel = wx.Panel( self.frame, -1)
-            self.sizer = RowColSizer()
-            self.labels = []
-            self.editList  = []
-            rowcount    = 4
-
-            id, id1 = wx.NewId(), wx.NewId()
-            self.command1  = wx.Button( self.panel, id, "&Close" )
-            self.command2  = wx.Button( self.panel, id1, "&AutoFormats" )
-            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
-            self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5)
-            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 )
-##            self.panel.SetDefaultItem(self.command1 )
-            self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2)
-
-            self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" )
-            self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" )
-            self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 )
-            self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 )
-            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 )
-            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 )
-
-
-            label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field.
-Note that all controls have been auto-sized by including F in the format code.
-Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status)."""
-            label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)."
-
-            self.label1 = wx.StaticText( self.panel, -1, label)
-            self.label2 = wx.StaticText( self.panel, -1, "Description")
-            self.label3 = wx.StaticText( self.panel, -1, "Mask Value")
-            self.label4 = wx.StaticText( self.panel, -1, "Format")
-            self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)")
-            self.label6 = wx.StaticText( self.panel, -1, "wxMaskedEdit Ctrl")
-            self.label7 = wx.StaticText( self.panel, -1, label2)
-            self.label7.SetForegroundColour("Blue")
-            self.label1.SetForegroundColour("Blue")
-            self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-
-            self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5)
-
-            # The following list is of the controls for the demo. Feel free to play around with
-            # the options!
-            controls = [
-            #description        mask                    excl format     regexp                              range,list,initial
-           ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''),
-           ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''),
-           ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''),
-           ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''),
-           ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''),
-           ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'),
-           ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''),
-           ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'),
-           ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''),
-           ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '),
-           ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '),
-           ("Month selector",   "XXX",                  "", 'F',        "",                                 (),
-                ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""),
-           ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (),
-                ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "")
-           ]
-
-            for control in controls:
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL)
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL)
-
-                if control in controls[:]:#-2]:
-                    newControl  = MaskedTextCtrl( self.panel, -1, "",
-                                                    mask         = control[1],
-                                                    excludeChars = control[2],
-                                                    formatcodes  = control[3],
-                                                    includeChars = "",
-                                                    validRegex   = control[4],
-                                                    validRange   = control[5],
-                                                    choices      = control[6],
-                                                    defaultValue = control[7],
-                                                    demo         = True)
-                    if control[6]: newControl.SetCtrlParameters(choiceRequired = True)
-                else:
-                    newControl = MaskedComboBox(  self.panel, -1, "",
-                                                    choices = control[7],
-                                                    choiceRequired  = True,
-                                                    mask         = control[1],
-                                                    formatcodes  = control[3],
-                                                    excludeChars = control[2],
-                                                    includeChars = "",
-                                                    validRegex   = control[4],
-                                                    validRange   = control[5],
-                                                    demo         = True)
-                self.editList.append( newControl )
-
-                self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5)
-                rowcount += 1
-
-            self.sizer.AddGrowableCol(4)
-
-            self.panel.SetSizer(self.sizer)
-            self.panel.SetAutoLayout(1)
-
-            self.frame.Show(1)
-            self.MainLoop()
-
-            return True
-
-        def onClick(self, event):
-            self.frame.Close()
-
-        def onClickPage(self, event):
-            self.page2 = test2(self.frame,-1,"")
-            self.page2.Show(True)
-
-        def _onCheck1(self,event):
-            """ Set required value on/off """
-            value = event.IsChecked()
-            if value:
-                for control in self.editList:
-                    control.SetCtrlParameters(emptyInvalid=True)
-                    control.Refresh()
-            else:
-                for control in self.editList:
-                    control.SetCtrlParameters(emptyInvalid=False)
-                    control.Refresh()
-            self.panel.Refresh()
-
-        def _onCheck2(self,event):
-            """ Highlight empty values"""
-            value = event.IsChecked()
-            if value:
-                for control in self.editList:
-                    control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine')
-                    control.Refresh()
-            else:
-                for control in self.editList:
-                    control.SetCtrlParameters( emptyBackgroundColour = 'White')
-                    control.Refresh()
-            self.panel.Refresh()
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class test2(wx.Frame):
-        def __init__(self, parent, id, caption):
-            wx.Frame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
-            from wx.lib.rcsizer import RowColSizer
-            self.panel = wx.Panel( self, -1)
-            self.sizer = RowColSizer()
-            self.labels = []
-            self.texts  = []
-            rowcount    = 4
-
-            label = """\
-All these controls have been created by passing a single parameter, the AutoFormat code.
-The class contains an internal dictionary of types and formats (autoformats).
-To see a great example of validations in action, try entering a bad email address, then tab out."""
-
-            self.label1 = wx.StaticText( self.panel, -1, label)
-            self.label2 = wx.StaticText( self.panel, -1, "Description")
-            self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code")
-            self.label4 = wx.StaticText( self.panel, -1, "wxMaskedEdit Control")
-            self.label1.SetForegroundColour("Blue")
-            self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-            self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-
-            self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
-            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
-
-            id, id1 = wx.NewId(), wx.NewId()
-            self.command1  = wx.Button( self.panel, id, "&Close")
-            self.command2  = wx.Button( self.panel, id1, "&Print Formats")
-            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1)
-            self.panel.SetDefaultItem(self.command1)
-            self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2)
-
-            # The following list is of the controls for the demo. Feel free to play around with
-            # the options!
-            controls = [
-           ("Phone No","USPHONEFULLEXT"),
-           ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"),
-           ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
-           ("Time (with seconds)","TIMEHHMMSS"),
-           ("Military Time\n(without seconds)","24HRTIMEHHMM"),
-           ("Social Sec#","USSOCIALSEC"),
-           ("Credit Card","CREDITCARD"),
-           ("Expiration MM/YY","EXPDATEMMYY"),
-           ("Percentage","PERCENT"),
-           ("Person's Age","AGE"),
-           ("US Zip Code","USZIP"),
-           ("US Zip+4","USZIPPLUS4"),
-           ("Email Address","EMAIL"),
-           ("IP Address", "(derived control IpAddrCtrl)")
-           ]
-
-            for control in controls:
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
-                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
-                if control in controls[:-1]:
-                    self.sizer.Add( MaskedTextCtrl( self.panel, -1, "",
-                                                      autoformat  = control[1],
-                                                      demo        = True),
-                                row=rowcount,col=2,flag=wx.ALL,border=5)
-                else:
-                    self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ),
-                                    row=rowcount,col=2,flag=wx.ALL,border=5)
-                rowcount += 1
-
-            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
-            self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5)
-            self.sizer.AddGrowableCol(3)
-
-            self.panel.SetSizer(self.sizer)
-            self.panel.SetAutoLayout(1)
-
-        def onClick(self, event):
-            self.Close()
-
-        def onClickPrint(self, event):
-            for format in masktags.keys():
-                sep = "+------------------------+"
-                print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex'])
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-if __name__ == "__main__":
-    app = test(False)
-
-i=1
-##
-## Current Issues:
-## ===================================
-##
-## 1. WS: For some reason I don't understand, the control is generating two (2)
-##      EVT_TEXT events for every one (1) .SetValue() of the underlying control.
-##      I've been unsuccessful in determining why or in my efforts to make just one
-##      occur.  So, I've added a hack to save the last seen value from the
-##      control in the EVT_TEXT handler, and if *different*, call event.Skip()
-##      to propagate it down the event chain, and let the application see it.
-##
-## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the
-##      behavior of the underlying control that I can't fix.  The problems are:
-##      a) The background coloring doesn't work in the text field of the control;
-##         instead, there's a only border around it that assumes the correct color.
-##      b) The control will not pass WXK_TAB to the event handler, no matter what
-##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to
-##         indicate that we want these events.  As a result, MaskedComboBox
-##         doesn't do the nice field-tabbing that MaskedTextCtrl does.
-##      c) Auto-complete had to be reimplemented for the control because programmatic
-##         setting of the value of the text field does not set up the auto complete
-##         the way that the control processing keystrokes does.  (But I think I've
-##         implemented a fairly decent approximation.)  Because of this the control
-##         also won't auto-complete on dropdown, and there's no event I can catch
-##         to work around this problem.
-##      d) There is no method provided for getting the selection; the hack I've
-##         implemented has its flaws, not the least of which is that due to the
-##         strategy that I'm using, the paste buffer is always replaced by the
-##         contents of the control's selection when in focus, on each keystroke;
-##         this makes it impossible to paste anything into a MaskedComboBox
-##         at the moment... :-(
-##      e) The other deficient behavior, likely induced by the workaround for (d),
-##         is that you can can't shift-left to select more than one character
-##         at a time.
-##
-##
-## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their
-##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in
-##      wxWidgets, shift-tab won't take you backwards through the fields of
-##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will
-##      work like shift-tab and tab ought to.
-##
-
-## To-Do's:
-## =============================##
-##  1. Add Popup list for auto-completable fields that simulates combobox on individual
-##     fields.  Example: City validates against list of cities, or zip vs zip code list.
-##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal"
-##     control.
-##  3. Fix shift-left selection for MaskedComboBox.
-##  5. Transform notion of "decimal control" to be less "entire control"-centric,
-##     so that monetary symbols can be included and still have the appropriate
-##     semantics.  (Big job, as currently written, but would make control even
-##     more useful for business applications.)
-
-
-## CHANGELOG:
-## ====================
-##  Version 1.5
-##  (Reported) bugs fixed:
-##   1. Crash ensues if you attempt to change the mask of a read-only
-##      MaskedComboBox after initial construction.
-##   2. Changed strategy of defining Get/Set property functions so that
-##      these are now generated dynamically at runtime, rather than as
-##      part of the class definition.  (This makes it possible to have
-##      more general base classes that have many more options for configuration
-##      without requiring that derivations support the same options.)
-##   3. Fixed IsModified for _Paste() and _OnErase().
-##
-##   Enhancements:
-##   1. Fixed "attribute function inheritance," since base control is more
-##      generic than subsequent derivations, not all property functions of a
-##      generic control should be exposed in those derivations.  New strategy
-##      uses base control classes (eg. BaseMaskedTextCtrl) that should be
-##      used to derive new class types, and mixed with their own mixins to
-##      only expose those attributes from the generic masked controls that
-##      make sense for the derivation.  (This makes Boa happier.)
-##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less
-##      "parochial."
-##
-##  Version 1.4
-##  (Reported) bugs fixed:
-##   1. Right-click menu allowed "cut" operation that destroyed mask
-##      (was implemented by base control)
-##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all
-##      got converted to lower case.
-##   3. MaskedComboBox selection didn't deal with spaces in values
-##      properly when autocompleting, and didn't have a concept of "next"
-##      match for handling choice list duplicates.
-##   4. Size of MaskedComboBox was always default.
-##   5. Email address regexp allowed some "non-standard" things, and wasn't
-##      general enough.
-##   6. Couldn't easily reset MaskedComboBox contents programmatically.
-##   7. Couldn't set emptyInvalid during construction.
-##   8. Under some versions of wxPython, readonly comboboxes can apparently
-##      return a GetInsertionPoint() result (655535), causing masked control
-##      to fail.
-##   9. Specifying an empty mask caused the controls to traceback.
-##  10. Can't specify float ranges for validRange.
-##  11. '.' from within a the static portion of a restricted IP address
-##      destroyed the mask from that point rightward; tab when cursor is
-##      before 1st field takes cursor past that field.
-##
-##  Enhancements:
-##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.)
-##  13. Added auto-select option on char input for masked controls with
-##      choice lists.
-##  14. Added '>' formatcode, allowing insert within a given or each field
-##      as appropriate, rather than requiring "overwrite".  This makes single
-##      field controls that just have validation rules (eg. EMAIL) much more
-##      friendly.  The same flag controls left shift when deleting vs just
-##      blanking the value, and for right-insert fields, allows right-insert
-##      at any non-blank (non-sign) position in the field.
-##  15. Added option to use to indicate negative values for numeric controls.
-##  16. Improved OnFocus handling of numeric controls.
-##  17. Enhanced Home/End processing to allow operation on a field level,
-##      using ctrl key.
-##  18. Added individual Get/Set functions for control parameters, for
-##      simplified integration with Boa Constructor.
-##  19. Standardized "Colour" parameter names to match wxPython, with
-##      non-british spellings still supported for backward-compatibility.
-##  20. Added '&' mask specification character for punctuation only (no letters
-##      or digits).
-##  21. Added (in a separate file) wxMaskedCtrl() factory function to provide
-##      unified interface to the masked edit subclasses.
-##
-##
-##  Version 1.3
-##   1. Made it possible to configure grouping, decimal and shift-decimal characters,
-##      to make controls more usable internationally.
-##   2. Added code to smart "adjust" value strings presented to .SetValue()
-##      for right-aligned numeric format controls if they are shorter than
-##      than the control width,  prepending the missing portion, prepending control
-##      template left substring for the missing characters, so that setting
-##      numeric values is easier.
-##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved
-##      for b-c), as this makes more sense.
-##
-##  Version 1.2
-##   1. Fixed .SetValue() to replace the current value, rather than the current
-##      selection. Also changed it to generate ValueError if presented with
-##      either a value which doesn't follow the format or won't fit.  Also made
-##      set value adjust numeric and date controls as if user entered the value.
-##      Expanded doc explaining how SetValue() works.
-##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
-##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
-##   3. Made all date autoformats automatically pick implied "datestyle".
-##   4. Added IsModified override, since base wxTextCtrl never reports modified if
-##      .SetValue used to change the value, which is what the masked edit controls
-##      use internally.
-##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when
-##      using tab to "leave field" and auto-adjust.
-##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes,
-##      and bug in ._Paste() that didn't account for signs in signed masks either.
-##   7. Fixed issues with _adjustPos for right-insert fields causing improper
-##      selection/replacement of values
-##   8. Fixed _OnHome handler to properly handle extending current selection to
-##      beginning of control.
-##   9. Exposed all (valid) autoformats to demo, binding descriptions to
-##      autoformats.
-##  10. Fixed a couple of bugs in email regexp.
-##  11. Made maskchardict an instance var, to make mask chars to be more
-##      amenable to international use.
-##  12. Clarified meaning of '-' formatcode in doc.
-##  13. Fixed a couple of coding bugs being flagged by Python2.1.
-##  14. Fixed several issues with sign positioning, erasure and validity
-##      checking for "numeric" masked controls.
-##  15. Added validation to IpAddrCtrl.SetValue().
-##
-##  Version 1.1
-##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default)
-##      vs. literal font facename, and use wxTELETYPE as the font family
-##      if so specified.
-##   2. Switched to use of dbg module vs. locally defined version.
-##   3. Revamped entire control structure to use Field classes to hold constraint
-##      and formatting data, to make code more hierarchical, allow for more
-##      sophisticated masked edit construction.
-##   4. Better strategy for managing options, and better validation on keywords.
-##   5. Added 'V' format code, which requires that in order for a character
-##      to be accepted, it must result in a string that passes the validRegex.
-##   6. Added 'S' format code which means "select entire field when navigating
-##      to new field."
-##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment)
-##   8. Added '<' format code to allow fields to require explicit cursor movement
-##      to leave field.
-##   9. Added validFunc option to other validation mechanisms, that allows derived
-##      classes to add dynamic validation constraints to the control.
-##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also
-##      fixed failure to obey case conversion codes when pasting.
-##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere...
-##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.'
-##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal,
-##      individual field validation to prevent field values > 255, and require explicit
-##      tab/. to change fields.
-##  14. Added handler for left double-click to select field under cursor.
-##  15. Fixed handling for "Read-only" styles.
-##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor
-##      attribute, for more consistent and controllable coloring.
-##  17. Added retainFieldValidation parameter, allowing top-level constraints
-##      such as "validRequired" to be set independently of field-level equivalent.
-##      (needed in TimeCtrl for bounds constraints.)
-##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed
-##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue,
-##      etc.
-##  19. Fixed maskchar setting for upper/lowercase, to work in all locales.
-##
-##
-##  Version 1.0
-##   1. Decimal point behavior restored for decimal and integer type controls:
-##      decimal point now trucates the portion > 0.
-##   2. Return key now works like the tab character and moves to the next field,
-##      provided no default button is set for the form panel on which the control
-##      resides.
-##   3. Support added in _FindField() for subclasses controls (like timecontrol)
-##      to determine where the current insertion point is within the mask (i.e.
-##      which sub-'field'). See method documentation for more info and examples.
-##   4. Added Field class and support for all constraints to be field-specific
-##      in addition to being globally settable for the control.
-##      Choices for each field are validated for length and pastability into
-##      the field in question, raising ValueError if not appropriate for the control.
-##      Also added selective additional validation based on individual field constraints.
-##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all
-##      auto-complete fields with choice lists, supplying the 1st entry in
-##      the choice list if the field is empty, and cycling through the list in
-##      the appropriate direction if already a match.  WXK_DOWN will also auto-
-##      complete if the field is partially completed and a match can be made.
-##      SHIFT-WXK_UP/DOWN will also take you to the next field after any
-##      auto-completion performed.
-##   5. Added autoCompleteKeycodes=[] parameters for allowing further
-##      customization of the control.  Any keycode supplied as a member
-##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If
-##      requireFieldChoice is set, then a valid value from each non-empty
-##      choice list will be required for the value of the control to validate.
-##   6. Fixed "auto-sizing" to be relative to the font actually used, rather
-##      than making assumptions about character width.
-##   7. Fixed GetMaskParameter(), which was non-functional in previous version.
-##   8. Fixed exceptions raised to provide info on which control had the error.
-##   9. Fixed bug in choice management of MaskedComboBox.
-##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of
-##     the form '# #'.  Modified control code for IpAddrCtrl so that '.'
-##     in the middle of a field clips the rest of that field, similar to
-##     decimal and integer controls.
-##
-##
-##  Version 0.0.7
-##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive.
-##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333).
-##   3. New support for selecting text within the control.(thanks Will Sadkin!)
-##      Shift-End and Shift-Home now select text as you would expect
-##      Control-Shift-End selects to the end of the mask string, even if value not entered.
-##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control.
-##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed-
-##      for some reason I couldn't find the original email but thanks!!!)
-##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome,
-##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc.
-##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!).
-##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...)
-##   7. New mechanism for replacing default behavior for any given key, using
-##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available
-##      for easier subclassing of the control.
-##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs
-##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut"
-##      semantics, erasing the selection, rather than erasing the entire control.
-##   9. Added option for an "default value" (ie. the template) for use when a single fillChar
-##      is not desired in every position.  Added IsDefault() function to mean "does the value
-##      equal the template?" and modified .IsEmpty() to mean "do all of the editable
-##      positions in the template == the fillChar?"
-##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox,
-##      now included.
-##  11. MaskedComboBox now adds the capability to validate from list of valid values.
-##      Example: City validates against list of cities, or zip vs zip code list.
-##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being
-##      passed to the next handler in the event chain, causing updates to the
-##      control to be invisible to the parent code.
-##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within
-##      the control, that auto-reformats as you move between cells.
-##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'.
-##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14}
-##  16. Fixed major bugs in date validation, due to the fact that
-##      wxDateTime.ParseDate is too liberal, and will accept any form that
-##      makes any kind of sense, regardless of the datestyle you specified
-##      for the control.  Unfortunately, the strategy used to fix it only
-##      works for versions of wxPython post 2.3.3.1, as a C++ assert box
-##      seems to show up on an invalid date otherwise, instead of a catchable
-##      exception.
-##  17. Enhanced date adjustment to automatically adjust heuristic based on
-##      current year, making last century/this century determination on
-##      2-digit year based on distance between today's year and value;
-##      if > 50 year separation, assume last century (and don't assume last
-##      century is 20th.)
-##  18. Added autoformats and support for including HHMMSS as well as HHMM for
-##      date times, and added similar time, and militaray time autoformats.
-##  19. Enhanced tabbing logic so that tab takes you to the next field if the
-##      control is a multi-field control.
-##  20. Added stub method called whenever the control "changes fields", that
-##      can be overridden by subclasses (eg. IpAddrCtrl.)
-##  21. Changed a lot of code to be more functionally-oriented so side-effects
-##      aren't as problematic when maintaining code and/or adding features.
-##      Eg: IsValid() now does not have side-effects; it merely reflects the
-##      validity of the value of the control; to determine validity AND recolor
-##      the control, _CheckValid() should be used with a value argument of None.
-##      Similarly, made most reformatting function take an optional candidate value
-##      rather than just using the current value of the control, and only
-##      have them change the value of the control if a candidate is not specified.
-##      In this way, you can do validation *before* changing the control.
-##  22. Changed validRequired to mean "disallow chars that result in invalid
-##      value."  (Old meaning now represented by emptyInvalid.)  (This was
-##      possible once I'd made the changes in (19) above.)
-##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they
-##      can be set/modified/retrieved after construction.  Removed individual
-##      parameter setting functions, in favor of this mechanism, so that
-##      all adjustment of the control based on changing parameter values can
-##      be handled in one place with unified mechanism.
-##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability
-##      to type "grouping char" (ie. ',') and validate as appropriate.
-##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9.
-##  26. Fixed assumption about "decimal or integer" masks so that they're only
-##      made iff there's no validRegex associated with the field.  (This
-##      is so things like zipcodes which look like integers can have more
-##      restrictive validation (ie. must be 5 digits.)
-##  27. Added a ton more doc strings to explain use and derivation requirements
-##      and did regularization of the naming conventions.
-##  28. Fixed a range bug in _adjustKey preventing z from being handled properly.
-##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to
-##      reformat the value and move the next field as appropriate. (shift-'.',
-##      ie. '>' moves to the previous field.
-
-##  Version 0.0.6
-##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending
-##      in '0'.
-##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the
-##      year position, the control will expand the value to four digits, using numerals below
-##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn).
-##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM}
-##   3. revalid parameter renamed validRegex to conform to standard for all validation
-##      parameters (see 2 new ones below).
-##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types.
-##      Allows the developer to specify a valid low/high range of values.
-##   5. New optional init parameter = validList. Used for character types. Allows developer
-##      to send a list of values to the control to be used for specific validation.
-##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams.
-##   6. Date type fields now use wxDateTime's parser to validate the date and time.
-##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing
-##      me toward this solution!
-##   7. Date fields now automatically expand 2-digit years when it can. For example,
-##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year
-##      date is entered it will be expanded in any case when the user tabs out of the
-##      field.
-##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor,
-##      SetSignedForeColor allow accessto override default class coloring behavior.
-##   9. Documentation updated and improved.
-##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better.
-##      Two new options (checkboxes) - test highlight empty and disallow empty.
-##  11. Home and End now work more intuitively, moving to the first and last user-entry
-##      value, respectively.
-##  12. New class function: SetRequired(bool). Sets the control's entry required flag
-##      (i.e. disallow empty values if True).
-##
-##  Version 0.0.5
-##   1. get_plainValue method renamed to GetPlainValue following the wxWidgets
-##      StudlyCaps(tm) standard (thanks Paul Moore).  ;)
-##   2. New format code 'F' causes the control to auto-fit (auto-size) itself
-##      based on the length of the mask template.
-##   3. Class now supports "autoformat" codes. These can be passed to the class
-##      on instantiation using the parameter autoformat="code". If the code is in
-##      the dictionary, it will self set the mask, formatting, and validation string.
-##      I have included a number of samples, but I am hoping that someone out there
-##      can help me to define a whole bunch more.
-##   4. I have added a second page to the demo (as well as a second demo class, test2)
-##      to showcase how autoformats work. The way they self-format and self-size is,
-##      I must say, pretty cool.
-##   5. Comments added and some internal cosmetic revisions re: matching the code
-##      standards for class submission.
-##   6. Regex validation is now done in real time - field turns yellow immediately
-##      and stays yellow until the entered value is valid
-##   7. Cursor now skips over template characters in a more intuitive way (before the
-##      next keypress).
-##   8. Change, Keypress and LostFocus methods added for convenience of subclasses.
-##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR,
-##      and EVT_KILL_FOCUS, respectively.
-##   9. Decimal and numeric handlers have been rewritten and now work more intuitively.
-##
-##  Version 0.0.4
-##   1. New .IsEmpty() method returns True if the control's value is equal to the
-##      blank template string
-##   2. Control now supports a new init parameter: revalid. Pass a regular expression
-##      that the value will have to match when the control loses focus. If invalid,
-##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next).
-##   3. Demo now shows revalid functionality. Try entering a partial value, such as a
-##      partial social security number.
-##   4. New .IsValid() value returns True if the control is empty, or if the value matches
-##      the revalid expression. If not, .IsValid() returns False.
-##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never
-##      presses the decimal point.
-##   6. Cursor now goes to the beginning of the field if the user clicks in an
-##      "empty" field intead of leaving the insertion point in the middle of the
-##      field.
-##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9.
-##   8. New formatcodes init parameter replaces other init params and adds functions.
-##      String passed to control on init controls:
-##        _ Allow spaces
-##        ! Force upper
-##        ^ Force lower
-##        R Show negative #s in red
-##        , Group digits
-##        - Signed numerals
-##        0 Numeric fields get leading zeros
-##   9. Ctrl-X in any field clears the current value.
-##   10. Code refactored and made more modular (esp in OnChar method). Should be more
-##       easy to read and understand.
-##   11. Demo enhanced.
-##   12. Now has _doc_.
-##
-##  Version 0.0.3
-##   1. GetPlainValue() now returns the value without the template characters;
-##      so, for example, a social security number (123-33-1212) would return as
-##      123331212; also removes white spaces from numeric/decimal values, so
-##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value.
-##   2. Press '.' in an integer style masked control and truncate any trailing digits.
-##   3. Code moderately refactored. Internal names improved for clarity. Additional
-##      internal documentation.
-##   4. Home and End keys now supported to move cursor to beginning or end of field.
-##   5. Un-signed integers and decimals now supported.
-##   6. Cosmetic improvements to the demo.
-##   7. Class renamed to MaskedTextCtrl.
-##   8. Can now specify include characters that will override the basic
-##      controls: for example, includeChars = "@." for email addresses
-##   9. Added mask character 'C' -> allow any upper or lowercase character
-##   10. .SetSignColor(str:color) sets the foreground color for negative values
-##       in signed controls (defaults to red)
-##   11. Overview documentation written.
-##
-##  Version 0.0.2
-##   1. Tab now works properly when pressed in last position
-##   2. Decimal types now work (e.g. #####.##)
-##   3. Signed decimal or numeric values supported (i.e. negative numbers)
-##   4. Negative decimal or numeric values now can show in red.
-##   5. Can now specify an "exclude list" with the excludeChars parameter.
-##      See date/time formatted example - you can only enter A or P in the
-##      character mask space (i.e. AM/PM).
-##   6. Backspace now works properly, including clearing data from a selected
-##      region but leaving template characters intact. Also delete key.
-##   7. Left/right arrows now work properly.
-##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3
-##
diff --git a/wxPython/wx/lib/maskednumctrl.py b/wxPython/wx/lib/maskednumctrl.py
deleted file mode 100644 (file)
index 9a3d234..0000000
+++ /dev/null
@@ -1,1613 +0,0 @@
-#----------------------------------------------------------------------------
-# Name:         wxPython.lib.maskednumctrl.py
-# Author:       Will Sadkin
-# Created:      09/06/2003
-# Copyright:   (c) 2003 by Will Sadkin
-# RCS-ID:      $Id$
-# License:     wxWidgets license
-#----------------------------------------------------------------------------
-# NOTE:
-#   This was written to provide a numeric edit control for wxPython that
-#   does things like right-insert (like a calculator), and does grouping, etc.
-#   (ie. the features of MaskedTextCtrl), but allows Get/Set of numeric
-#   values, rather than text.
-#
-#   MaskedNumCtrl permits integer, and floating point values to be set
-#   retrieved or set via .GetValue() and .SetValue() (type chosen based on
-#   fraction width, and provides an EVT_MASKEDNUM() event function for trapping
-#   changes to the control.
-#
-#   It supports negative numbers as well as the naturals, and has the option
-#   of not permitting leading zeros or an empty control; if an empty value is
-#   not allowed, attempting to delete the contents of the control will result
-#   in a (selected) value of zero, thus preserving a legitimate numeric value.
-#   Similarly, replacing the contents of the control with '-' will result in
-#   a selected (absolute) value of -1.
-#
-#   MaskedNumCtrl also supports range limits, with the option of either
-#   enforcing them or simply coloring the text of the control if the limits
-#   are exceeded.
-#
-#   MaskedNumCtrl is intended to support fixed-point numeric entry, and
-#   is derived from BaseMaskedTextCtrl.  As such, it supports a limited range
-#   of values to comply with a fixed-width entry mask.
-#----------------------------------------------------------------------------
-# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Updated for wx namespace
-# 
-# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o wxMaskedEditMixin -> MaskedEditMixin
-# o wxMaskedTextCtrl -> MaskedTextCtrl
-# o wxMaskedNumNumberUpdatedEvent -> MaskedNumNumberUpdatedEvent
-# o wxMaskedNumCtrl -> MaskedNumCtrl
-#
-
-"""<html><body>
-<P>
-<B>MaskedNumCtrl:</B>
-<UL>
-<LI>allows you to get and set integer or floating point numbers as value,</LI>
-<LI>provides bounds support and optional value limiting,</LI>
-<LI>has the right-insert input style that MaskedTextCtrl supports,</LI>
-<LI>provides optional automatic grouping, sign control and format, grouping and decimal
-character selection, etc. etc.</LI>
-</UL>
-<P>
-Being derived from MaskedTextCtrl, the control only allows
-fixed-point  notation.  That is, it has a fixed (though reconfigurable)
-maximum width for the integer portion and optional fixed width
-fractional portion.
-<P>
-Here's the API:
-<DL><PRE>
-    <B>MaskedNumCtrl</B>(
-         parent, id = -1,
-         <B>value</B> = 0,
-         pos = wx.DefaultPosition,
-         size = wx.DefaultSize,
-         style = 0,
-         validator = wx.DefaultValidator,
-         name = "maskednumber",
-         <B>integerWidth</B> = 10,
-         <B>fractionWidth</B> = 0,
-         <B>allowNone</B> = False,
-         <B>allowNegative</B> = True,
-         <B>useParensForNegatives</B> = False,
-         <B>groupDigits</B> = False,
-         <B>groupChar</B> = ',',
-         <B>decimalChar</B> = '.',
-         <B>min</B> = None,
-         <B>max</B> = None,
-         <B>limited</B> = False,
-         <B>selectOnEntry</b> = True,
-         <B>foregroundColour</B> = "Black",
-         <B>signedForegroundColour</B> = "Red",
-         <B>emptyBackgroundColour</B> = "White",
-         <B>validBackgroundColour</B> = "White",
-         <B>invalidBackgroundColour</B> = "Yellow",
-         <B>autoSize</B> = True         
-         )
-</PRE>
-<UL>
-    <DT><B>value</B>
-    <DD>If no initial value is set, the default will be zero, or
-        the minimum value, if specified.  If an illegal string is specified,
-        a ValueError will result. (You can always later set the initial
-        value with SetValue() after instantiation of the control.)
-    <BR>
-    <DL><B>integerWidth</B>
-    <DD>Indicates how many places to the right of any decimal point
-        should be allowed in the control.  This will, perforce, limit
-        the size of the values that can be entered. This number need
-        not include space for grouping characters or the sign, if either
-        of these options are enabled, as the resulting underlying
-        mask is automatically by the control.  The default of 10
-        will allow any 32 bit integer value.  The minimum value
-        for integerWidth is 1.
-    <BR>
-    <DL><B>fractionWidth</B>
-    <DD>Indicates how many decimal places to show for numeric value.
-        If default (0), then the control will display and return only
-        integer or long values.
-    <BR>
-    <DT><B>allowNone</B>
-    <DD>Boolean indicating whether or not the control is allowed to be
-        empty, representing a value of None for the control.
-    <BR>
-    <DT><B>allowNegative</B>
-    <DD>Boolean indicating whether or not control is allowed to hold
-        negative numbers.
-    <BR>
-    <DT><B>useParensForNegatives</B>
-    <DD>If true, this will cause negative numbers to be displayed with ()s
-        rather than -, (although '-' will still trigger a negative number.)
-    <BR>
-    <DT><B>groupDigits</B>
-    <DD>Indicates whether or not grouping characters should be allowed and/or
-        inserted when leaving the control or the decimal character is entered.
-    <BR>
-    <DT><B>groupChar</B>
-    <DD>What grouping character will be used if allowed. (By default ',')
-    <BR>
-    <DT><B>decimalChar</B>
-    <DD>If fractionWidth is > 0, what character will be used to represent
-        the decimal point.  (By default '.')
-    <BR>
-    <DL><B>min</B>
-    <DD>The minimum value that the control should allow.  This can be also be
-        adjusted with SetMin().  If the control is not limited, any value
-        below this bound will result in a background colored with the current
-        invalidBackgroundColour.  If the min specified will not fit into the
-        control, the min setting will be ignored.
-    <BR>
-    <DT><B>max</B>
-    <DD>The maximum value that the control should allow.  This can be
-        adjusted with SetMax().  If the control is not limited, any value
-        above this bound will result in a background colored with the current
-        invalidBackgroundColour.  If the max specified will not fit into the
-        control, the max setting will be ignored.
-    <BR>
-    <DT><B>limited</B>
-    <DD>Boolean indicating whether the control prevents values from
-        exceeding the currently set minimum and maximum values (bounds).
-        If False and bounds are set, out-of-bounds values will
-        result in a background colored with the current invalidBackgroundColour.
-    <BR>
-    <DT><B>selectOnEntry</B>
-    <DD>Boolean indicating whether or not the value in each field of the
-        control should be automatically selected (for replacement) when
-        that field is entered, either by cursor movement or tabbing.
-        This can be desirable when using these controls for rapid data entry.
-    <BR>
-    <DT><B>foregroundColour</B>
-    <DD>Color value used for positive values of the control.
-    <BR>
-    <DT><B>signedForegroundColour</B>
-    <DD>Color value used for negative values of the control.
-    <BR>
-    <DT><B>emptyBackgroundColour</B>
-    <DD>What background color to use when the control is considered
-        "empty." (allow_none must be set to trigger this behavior.)
-    <BR>
-    <DT><B>validBackgroundColour</B>
-    <DD>What background color to use when the control value is
-        considered valid.
-    <BR>
-    <DT><B>invalidBackgroundColour</B>
-    <DD>Color value used for illegal values or values out-of-bounds of the
-        control when the bounds are set but the control is not limited.
-    <BR>
-    <DT><B>autoSize</B>
-    <DD>Boolean indicating whether or not the control should set its own
-        width based on the integer and fraction widths.  True by default.
-        <B><I>Note:</I></B> Setting this to False will produce seemingly odd
-        behavior unless the control is large enough to hold the maximum
-        specified value given the widths and the sign positions; if not,
-        the control will appear to "jump around" as the contents scroll.
-        (ie. autoSize is highly recommended.)
-</UL>
-<BR>
-<BR>
-<DT><B>EVT_MASKEDNUM(win, id, func)</B>
-<DD>Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when
-the value changes. Notice that this event will always be sent when the
-control's contents changes - whether this is due to user input or
-comes from the program itself (for example, if SetValue() is called.)
-<BR>
-<BR>
-<DT><B>SetValue(int|long|float|string)</B>
-<DD>Sets the value of the control to the value specified, if
-possible.  The resulting actual value of the control may be
-altered to conform to the format of the control, changed
-to conform with the bounds set on the control if limited,
-or colored if not limited but the value is out-of-bounds.
-A ValueError exception will be raised if an invalid value
-is specified.
-<BR>
-<DT><B>GetValue()</B>
-<DD>Retrieves the numeric value from the control.  The value
-retrieved will be either be returned as a long if the
-fractionWidth is 0, or a float otherwise.
-<BR>
-<BR>
-<DT><B>SetParameters(**kwargs)</B>
-<DD>Allows simultaneous setting of various attributes
-of the control after construction.  Keyword arguments
-allowed are the same parameters as supported in the constructor.
-<BR>
-<BR>
-<DT><B>SetIntegerWidth(value)</B>
-<DD>Resets the width of the integer portion of the control.  The
-value must be >= 1, or an AttributeError exception will result.
-This value should account for any grouping characters that might
-be inserted (if grouping is enabled), but does not need to account
-for the sign, as that is handled separately by the control.
-<DT><B>GetIntegerWidth()</B>
-<DD>Returns the current width of the integer portion of the control,
-not including any reserved sign position.
-<BR>
-<BR>
-<DT><B>SetFractionWidth(value)</B>
-<DD>Resets the width of the fractional portion of the control.  The
-value must be >= 0, or an AttributeError exception will result.  If
-0, the current value of the control will be truncated to an integer
-value.
-<DT><B>GetFractionWidth()</B>
-<DD>Returns the current width of the fractional portion of the control.
-<BR>
-<BR>
-<DT><B>SetMin(min=None)</B>
-<DD>Resets the minimum value of the control.  If a value of <I>None</I>
-is provided, then the control will have no explicit minimum value.
-If the value specified is greater than the current maximum value,
-then the function returns False and the minimum will not change from
-its current setting.  On success, the function returns True.
-<DT><DD>
-If successful and the current value is lower than the new lower
-bound, if the control is limited, the value will be automatically
-adjusted to the new minimum value; if not limited, the value in the
-control will be colored as invalid.
-<DT><DD>
-If min > the max value allowed by the width of the control,
-the function will return False, and the min will not be set.
-<BR>
-<DT><B>GetMin()</B>
-<DD>Gets the current lower bound value for the control.
-It will return None if no lower bound is currently specified.
-<BR>
-<BR>
-<DT><B>SetMax(max=None)</B>
-<DD>Resets the maximum value of the control. If a value of <I>None</I>
-is provided, then the control will have no explicit maximum value.
-If the value specified is less than the current minimum value, then
-the function returns False and the maximum will not change from its
-current setting. On success, the function returns True.
-<DT><DD>
-If successful and the current value is greater than the new upper
-bound, if the control is limited the value will be automatically
-adjusted to this maximum value; if not limited, the value in the
-control will be colored as invalid.
-<DT><DD>
-If max > the max value allowed by the width of the control,
-the function will return False, and the max will not be set.
-<BR>
-<DT><B>GetMax()</B>
-<DD>Gets the current upper bound value for the control.
-It will return None if no upper bound is currently specified.
-<BR>
-<BR>
-<DT><B>SetBounds(min=None,max=None)</B>
-<DD>This function is a convenience function for setting the min and max
-values at the same time.  The function only applies the maximum bound
-if setting the minimum bound is successful, and returns True
-only if both operations succeed.  <B><I>Note:</I></B> leaving out an argument
-will remove the corresponding bound.
-<DT><B>GetBounds()</B>
-<DD>This function returns a two-tuple (min,max), indicating the
-current bounds of the control.  Each value can be None if
-that bound is not set.
-<BR>
-<BR>
-<DT><B>IsInBounds(value=None)</B>
-<DD>Returns <I>True</I> if no value is specified and the current value
-of the control falls within the current bounds.  This function can also
-be called with a value to see if that value would fall within the current
-bounds of the given control.
-<BR>
-<BR>
-<DT><B>SetLimited(bool)</B>
-<DD>If called with a value of True, this function will cause the control
-to limit the value to fall within the bounds currently specified.
-If the control's value currently exceeds the bounds, it will then
-be limited accordingly.
-If called with a value of False, this function will disable value
-limiting, but coloring of out-of-bounds values will still take
-place if bounds have been set for the control.
-<DT><B>GetLimited()</B>
-<DT><B>IsLimited()</B>
-<DD>Returns <I>True</I> if the control is currently limiting the
-value to fall within the current bounds.
-<BR>
-<BR>
-<DT><B>SetAllowNone(bool)</B>
-<DD>If called with a value of True, this function will cause the control
-to allow the value to be empty, representing a value of None.
-If called with a value of False, this function will prevent the value
-from being None.  If the value of the control is currently None,
-ie. the control is empty, then the value will be changed to that
-of the lower bound of the control, or 0 if no lower bound is set.
-<DT><B>GetAllowNone()</B>
-<DT><B>IsNoneAllowed()</B>
-<DD>Returns <I>True</I> if the control currently allows its
-value to be None.
-<BR>
-<BR>
-<DT><B>SetAllowNegative(bool)</B>
-<DD>If called with a value of True, this function will cause the
-control to allow the value to be negative (and reserve space for
-displaying the sign. If called with a value of False, and the
-value of the control is currently negative, the value of the
-control will be converted to the absolute value, and then
-limited appropriately based on the existing bounds of the control
-(if any).
-<DT><B>GetAllowNegative()</B>
-<DT><B>IsNegativeAllowed()</B>
-<DD>Returns <I>True</I> if the control currently permits values
-to be negative.
-<BR>
-<BR>
-<DT><B>SetGroupDigits(bool)</B>
-<DD>If called with a value of True, this will make the control
-automatically add and manage grouping characters to the presented
-value in integer portion of the control.
-<DT><B>GetGroupDigits()</B>
-<DT><B>IsGroupingAllowed()</B>
-<DD>Returns <I>True</I> if the control is currently set to group digits.
-<BR>
-<BR>
-<DT><B>SetGroupChar()</B>
-<DD>Sets the grouping character for the integer portion of the
-control.  (The default grouping character this is ','.
-<DT><B>GetGroupChar()</B>
-<DD>Returns the current grouping character for the control.
-<BR>
-<BR>
-<DT><B>SetSelectOnEntry()</B>
-<DD>If called with a value of <I>True</I>, this will make the control
-automatically select the contents of each field as it is entered
-within the control.  (The default is True.)
-<DT><B>GetSelectOnEntry()</B>
-<DD>Returns <I>True</I> if the control currently auto selects
-the field values on entry.
-<BR>
-<BR>
-<DT><B>SetAutoSize(bool)</B>
-<DD>Resets the autoSize attribute of the control.
-<DT><B>GetAutoSize()</B>
-<DD>Returns the current state of the autoSize attribute for the control.
-<BR>
-<BR>
-</DL>
-</body></html>
-"""
-
-import  copy
-import  string
-import  types
-
-import  wx
-
-from sys import maxint
-MAXINT = maxint     # (constants should be in upper case)
-MININT = -maxint-1
-
-from wx.tools.dbg import Logger
-from wx.lib.maskededit import MaskedEditMixin, BaseMaskedTextCtrl, Field
-dbg = Logger()
-dbg(enable=0)
-
-#----------------------------------------------------------------------------
-
-wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wx.NewEventType()
-EVT_MASKEDNUM = wx.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED, 1)
-
-#----------------------------------------------------------------------------
-
-class MaskedNumNumberUpdatedEvent(wx.PyCommandEvent):
-    def __init__(self, id, value = 0, object=None):
-        wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id)
-
-        self.__value = value
-        self.SetEventObject(object)
-
-    def GetValue(self):
-        """Retrieve the value of the control at the time
-        this event was generated."""
-        return self.__value
-
-
-#----------------------------------------------------------------------------
-class MaskedNumCtrlAccessorsMixin:
-    # Define wxMaskedNumCtrl's list of attributes having their own
-    # Get/Set functions, ignoring those that make no sense for
-    # an numeric control.
-    exposed_basectrl_params = (
-         'decimalChar',
-         'shiftDecimalChar',
-         'groupChar',
-         'useParensForNegatives',
-         'defaultValue',
-         'description',
-
-         'useFixedWidthFont',
-         'autoSize',
-         'signedForegroundColour',
-         'emptyBackgroundColour',
-         'validBackgroundColour',
-         'invalidBackgroundColour',
-
-         'emptyInvalid',
-         'validFunc',
-         'validRequired',
-        )
-    for param in exposed_basectrl_params:
-        propname = param[0].upper() + param[1:]
-        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-        if param.find('Colour') != -1:
-            # add non-british spellings, for backward-compatibility
-            propname.replace('Colour', 'Color')
-
-            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-
-
-#----------------------------------------------------------------------------
-class MaskedNumCtrl(BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin):
-
-
-    valid_ctrl_params = {
-        'integerWidth': 10,                 # by default allow all 32-bit integers
-        'fractionWidth': 0,                 # by default, use integers
-        'decimalChar': '.',                 # by default, use '.' for decimal point
-        'allowNegative': True,              # by default, allow negative numbers
-        'useParensForNegatives': False,     # by default, use '-' to indicate negatives
-        'groupDigits': True,                # by default, don't insert grouping
-        'groupChar': ',',                   # by default, use ',' for grouping
-        'min': None,                        # by default, no bounds set
-        'max': None,
-        'limited': False,                   # by default, no limiting even if bounds set
-        'allowNone': False,                 # by default, don't allow empty value
-        'selectOnEntry': True,              # by default, select the value of each field on entry
-        'foregroundColour': "Black",
-        'signedForegroundColour': "Red",
-        'emptyBackgroundColour': "White",
-        'validBackgroundColour': "White",
-        'invalidBackgroundColour': "Yellow",
-        'useFixedWidthFont': True,          # by default, use a fixed-width font
-        'autoSize': True,                   # by default, set the width of the control based on the mask        
-        }
-
-
-    def __init__ (
-                self, parent, id=-1, value = 0,
-                pos = wx.DefaultPosition, size = wx.DefaultSize,
-                style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator,
-                name = "maskednum",
-                **kwargs ):
-
-        dbg('MaskedNumCtrl::__init__', indent=1)
-
-        # Set defaults for control:
-        dbg('setting defaults:')
-        for key, param_value in MaskedNumCtrl.valid_ctrl_params.items():
-            # This is done this way to make setattr behave consistently with
-            # "private attribute" name mangling
-            setattr(self, '_' + key, copy.copy(param_value))
-
-        # Assign defaults for all attributes:
-        init_args = copy.deepcopy(MaskedNumCtrl.valid_ctrl_params)
-        dbg('kwargs:', kwargs)
-        for key, param_value in kwargs.items():
-            key = key.replace('Color', 'Colour')
-            if key not in MaskedNumCtrl.valid_ctrl_params.keys():
-                raise AttributeError('invalid keyword argument "%s"' % key)
-            else:
-                init_args[key] = param_value
-        dbg('init_args:', indent=1)
-        for key, param_value in init_args.items():
-            dbg('%s:' % key, param_value)
-        dbg(indent=0)
-
-        # Process initial fields for the control, as part of construction:
-        if type(init_args['integerWidth']) != types.IntType:
-            raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth']))
-        elif init_args['integerWidth'] < 1:
-            raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth']))
-
-        fields = {}
-
-        if init_args.has_key('fractionWidth'):
-            if type(init_args['fractionWidth']) != types.IntType:
-                raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth))
-            elif init_args['fractionWidth'] < 0:
-                raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth']))
-            self._fractionWidth = init_args['fractionWidth']
-
-        if self._fractionWidth:
-            fracmask = '.' + '#{%d}' % self._fractionWidth
-            dbg('fracmask:', fracmask)
-            fields[1] = Field(defaultValue='0'*self._fractionWidth)
-        else:
-            fracmask = ''
-
-        self._integerWidth = init_args['integerWidth']
-        if init_args['groupDigits']:
-            self._groupSpace = (self._integerWidth - 1) / 3
-        else:
-            self._groupSpace = 0
-        intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
-        if self._fractionWidth:
-            emptyInvalid = False
-        else:
-            emptyInvalid = True
-        fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
-        dbg('intmask:', intmask)
-
-        # don't bother to reprocess these arguments:
-        del init_args['integerWidth']
-        del init_args['fractionWidth']
-
-        self._autoSize = init_args['autoSize']
-        if self._autoSize:
-            formatcodes = 'FR<'
-        else:
-            formatcodes = 'R<'
-
-
-        mask = intmask+fracmask
-
-        # initial value of state vars
-        self._oldvalue = 0
-        self._integerEnd = 0
-        self._typedSign = False
-
-        # Construct the base control:
-        BaseMaskedTextCtrl.__init__(
-                self, parent, id, '',
-                pos, size, style, validator, name,
-                mask = mask,
-                formatcodes = formatcodes,
-                fields = fields,
-                validFunc=self.IsInBounds,
-                setupEventHandling = False)
-
-        self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )        ## defeat automatic full selection
-        self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )   ## run internal validator
-        self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick
-        self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu
-        self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab.
-        self.Bind(wx.EVT_CHAR, self._OnChar )              ## handle each keypress
-        self.Bind(wx.EVT_TEXT, self.OnTextChange )      ## color control appropriately & keep
-                                                           ## track of previous value for undo
-
-        # Establish any additional parameters, with appropriate error checking
-        self.SetParameters(**init_args)
-
-        # Set the value requested (if possible)
-##        wxCallAfter(self.SetValue, value)
-        self.SetValue(value)
-
-        # Ensure proper coloring:
-        self.Refresh()
-        dbg('finished MaskedNumCtrl::__init__', indent=0)
-
-
-    def SetParameters(self, **kwargs):
-        """
-        This routine is used to initialize and reconfigure the control:
-        """
-        dbg('MaskedNumCtrl::SetParameters', indent=1)
-        maskededit_kwargs = {}
-        reset_fraction_width = False
-
-
-        if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth)
-            or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth)
-            or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits)
-            or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ):
-
-            fields = {}
-
-            if kwargs.has_key('fractionWidth'):
-                if type(kwargs['fractionWidth']) != types.IntType:
-                    raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth']))
-                elif kwargs['fractionWidth'] < 0:
-                    raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth']))
-                else:
-                    if self._fractionWidth != kwargs['fractionWidth']:
-                        self._fractionWidth = kwargs['fractionWidth']
-
-            if self._fractionWidth:
-                fracmask = '.' + '#{%d}' % self._fractionWidth
-                fields[1] = Field(defaultValue='0'*self._fractionWidth)
-                emptyInvalid = False
-            else:
-                emptyInvalid = True
-                fracmask = ''
-            dbg('fracmask:', fracmask)
-
-            if kwargs.has_key('integerWidth'):
-                if type(kwargs['integerWidth']) != types.IntType:
-                    dbg(indent=0)
-                    raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth']))
-                elif kwargs['integerWidth'] < 0:
-                    dbg(indent=0)
-                    raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth']))
-                else:
-                    self._integerWidth = kwargs['integerWidth']
-
-            if kwargs.has_key('groupDigits'):
-                self._groupDigits = kwargs['groupDigits']
-
-            if self._groupDigits:
-                self._groupSpace = (self._integerWidth - 1) / 3
-            else:
-                self._groupSpace = 0
-
-            intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
-            dbg('intmask:', intmask)
-            fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
-            maskededit_kwargs['fields'] = fields
-
-            # don't bother to reprocess these arguments:
-            if kwargs.has_key('integerWidth'):
-                del kwargs['integerWidth']
-            if kwargs.has_key('fractionWidth'):
-                del kwargs['fractionWidth']
-
-            maskededit_kwargs['mask'] = intmask+fracmask
-
-        if kwargs.has_key('groupChar'):
-            old_groupchar = self._groupChar     # save so we can reformat properly
-            dbg("old_groupchar: '%s'" % old_groupchar)
-            maskededit_kwargs['groupChar'] = kwargs['groupChar']
-        if kwargs.has_key('decimalChar'):
-            old_decimalchar = self._decimalChar
-            dbg("old_decimalchar: '%s'" % old_decimalchar)
-            maskededit_kwargs['decimalChar'] = kwargs['decimalChar']
-
-        # for all other parameters, assign keyword args as appropriate:
-        for key, param_value in kwargs.items():
-            key = key.replace('Color', 'Colour')
-            if key not in MaskedNumCtrl.valid_ctrl_params.keys():
-                raise AttributeError('invalid keyword argument "%s"' % key)
-            elif key not in MaskedEditMixin.valid_ctrl_params.keys():
-                setattr(self, '_' + key, param_value)
-            elif key in ('mask', 'autoformat'): # disallow explicit setting of mask
-                raise AttributeError('invalid keyword argument "%s"' % key)
-            else:
-                maskededit_kwargs[key] = param_value
-        dbg('kwargs:', kwargs)
-
-        # reprocess existing format codes to ensure proper resulting format:
-        formatcodes = self.GetCtrlParameter('formatcodes')        
-        if kwargs.has_key('allowNegative'):
-            if kwargs['allowNegative'] and '-' not in formatcodes:
-                formatcodes += '-'
-                maskededit_kwargs['formatcodes'] = formatcodes
-            elif not kwargs['allowNegative'] and '-' in formatcodes:
-                formatcodes = formatcodes.replace('-','')
-                maskededit_kwargs['formatcodes'] = formatcodes
-
-        if kwargs.has_key('groupDigits'):
-            if kwargs['groupDigits'] and ',' not in formatcodes:
-                formatcodes += ','
-                maskededit_kwargs['formatcodes'] = formatcodes
-            elif not kwargs['groupDigits'] and ',' in formatcodes:
-                formatcodes = formatcodes.replace(',','')
-                maskededit_kwargs['formatcodes'] = formatcodes
-
-        if kwargs.has_key('selectOnEntry'):
-            self._selectOnEntry = kwargs['selectOnEntry']
-            dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes)
-            if kwargs['selectOnEntry'] and 'S' not in formatcodes:
-                formatcodes += 'S'
-                maskededit_kwargs['formatcodes'] = formatcodes
-            elif not kwargs['selectOnEntry'] and 'S' in formatcodes:
-                formatcodes = formatcodes.replace('S','')
-                maskededit_kwargs['formatcodes'] = formatcodes
-
-        if kwargs.has_key('autoSize'):
-            self._autoSize = kwargs['autoSize']
-            if kwargs['autoSize'] and 'F' not in formatcodes:
-                formatcodes += 'F'
-                maskededit_kwargs['formatcodes'] = formatcodes
-            elif not kwargs['autoSize'] and 'F' in formatcodes:
-                formatcodes = formatcodes.replace('F', '')
-                maskededit_kwargs['formatcodes'] = formatcodes
-
-
-        if 'r' in formatcodes and self._fractionWidth:
-            # top-level mask should only be right insert if no fractional
-            # part will be shown; ie. if reconfiguring control, remove
-            # previous "global" setting.
-            formatcodes = formatcodes.replace('r', '')
-            maskededit_kwargs['formatcodes'] = formatcodes
-
-
-        if kwargs.has_key('limited'):
-            if kwargs['limited'] and not self._limited:
-                maskededit_kwargs['validRequired'] = True
-            elif not kwargs['limited'] and self._limited:
-                maskededit_kwargs['validRequired'] = False
-            self._limited = kwargs['limited']
-
-        dbg('maskededit_kwargs:', maskededit_kwargs)
-        if maskededit_kwargs.keys():
-            self.SetCtrlParameters(**maskededit_kwargs)
-
-        # Record end of integer and place cursor there:
-        integerEnd = self._fields[0]._extent[1]
-        self.SetInsertionPoint(0)
-        self.SetInsertionPoint(integerEnd)
-        self.SetSelection(integerEnd, integerEnd)
-
-        # Go ensure all the format codes necessary are present:
-        orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes')
-        if 'r' not in intformat:
-            intformat += 'r'
-        if '>' not in intformat:
-            intformat += '>'
-        if intformat != orig_intformat:
-            if self._fractionWidth:
-                self.SetFieldParameters(0, formatcodes=intformat)
-            else:
-                self.SetCtrlParameters(formatcodes=intformat)
-
-        # Set min and max as appropriate:
-        if kwargs.has_key('min'):
-            min = kwargs['min']
-            if( self._max is None
-                or min is None
-                or (self._max is not None and self._max >= min) ):
-                dbg('examining min')
-                if min is not None:
-                    try:
-                        textmin = self._toGUI(min, apply_limits = False)
-                    except ValueError:
-                        dbg('min will not fit into control; ignoring', indent=0)
-                        raise
-                dbg('accepted min')
-                self._min = min
-            else:
-                dbg('ignoring min')
-
-
-        if kwargs.has_key('max'):
-            max = kwargs['max']
-            if( self._min is None
-                or max is None
-                or (self._min is not None and self._min <= max) ):
-                dbg('examining max')
-                if max is not None:
-                    try:
-                        textmax = self._toGUI(max, apply_limits = False)
-                    except ValueError:
-                        dbg('max will not fit into control; ignoring', indent=0)
-                        raise
-                dbg('accepted max')
-                self._max = max
-            else:
-                dbg('ignoring max')
-
-        if kwargs.has_key('allowNegative'):
-            self._allowNegative = kwargs['allowNegative']
-
-        # Ensure current value of control obeys any new restrictions imposed:
-        text = self._GetValue()
-        dbg('text value: "%s"' % text)
-        if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1:
-            text = text.replace(old_groupchar, self._groupChar)
-        if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1:
-            text = text.replace(old_decimalchar, self._decimalChar)
-        if text != self._GetValue():
-            wx.TextCtrl.SetValue(self, text)
-
-        value = self.GetValue()
-
-        dbg('self._allowNegative?', self._allowNegative)
-        if not self._allowNegative and self._isNeg:
-            value = abs(value)
-            dbg('abs(value):', value)
-            self._isNeg = False
-
-        elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '':
-            if self._min > 0:
-                value = self._min
-            else:
-                value = 0
-
-        sel_start, sel_to = self.GetSelection()
-        if self.IsLimited() and self._min is not None and value < self._min:
-            dbg('Set to min value:', self._min)
-            self._SetValue(self._toGUI(self._min))
-
-        elif self.IsLimited() and self._max is not None and value > self._max:
-            dbg('Setting to max value:', self._max)
-            self._SetValue(self._toGUI(self._max))
-        else:
-            # reformat current value as appropriate to possibly new conditions
-            dbg('Reformatting value:', value)
-            sel_start, sel_to = self.GetSelection()
-            self._SetValue(self._toGUI(value))
-        self.Refresh() # recolor as appropriate
-        dbg('finished MaskedNumCtrl::SetParameters', indent=0)
-
-
-
-    def _GetNumValue(self, value):
-        """
-        This function attempts to "clean up" a text value, providing a regularized
-        convertable string, via atol() or atof(), for any well-formed numeric text value.
-        """
-        return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip()
-
-
-    def GetFraction(self, candidate=None):
-        """
-        Returns the fractional portion of the value as a float.  If there is no
-        fractional portion, the value returned will be 0.0.
-        """
-        if not self._fractionWidth:
-            return 0.0
-        else:
-            fracstart, fracend = self._fields[1]._extent
-            if candidate is None:
-                value = self._toGUI(BaseMaskedTextCtrl.GetValue(self))
-            else:
-                value = self._toGUI(candidate)
-            fracstring = value[fracstart:fracend].strip()
-            if not value:
-                return 0.0
-            else:
-                return string.atof(fracstring)
-
-    def _OnChangeSign(self, event):
-        dbg('MaskedNumCtrl::_OnChangeSign', indent=1)
-        self._typedSign = True
-        MaskedEditMixin._OnChangeSign(self, event)
-        dbg(indent=0)
-
-
-    def _disallowValue(self):
-        dbg('MaskedNumCtrl::_disallowValue')
-        # limited and -1 is out of bounds
-        if self._typedSign:
-            self._isNeg = False
-        if not wx.Validator_IsSilent():
-            wx.Bell()
-        sel_start, sel_to = self._GetSelection()
-        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
-        wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position
-        wx.CallAfter(self.SetSelection, sel_start, sel_to)
-
-    def _SetValue(self, value):
-        """
-        This routine supersedes the base masked control _SetValue().  It is
-        needed to ensure that the value of the control is always representable/convertable
-        to a numeric return value (via GetValue().)  This routine also handles
-        automatic adjustment and grouping of the value without explicit intervention
-        by the user.
-        """
-
-        dbg('MaskedNumCtrl::_SetValue("%s")' % value, indent=1)
-
-        if( (self._fractionWidth and value.find(self._decimalChar) == -1) or
-            (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) :
-            value = self._toGUI(value)
-
-        numvalue = self._GetNumValue(value)
-        dbg('cleansed value: "%s"' % numvalue)
-        replacement = None
-
-        if numvalue == "":
-            if self._allowNone:
-                dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value)
-                BaseMaskedTextCtrl._SetValue(self, value)
-                self.Refresh()
-                return
-            elif self._min > 0 and self.IsLimited():
-                replacement = self._min
-            else:
-                replacement = 0
-            dbg('empty value; setting replacement:', replacement)
-
-        if replacement is None:
-            # Go get the integer portion about to be set and verify its validity
-            intstart, intend = self._fields[0]._extent
-            dbg('intstart, intend:', intstart, intend)
-            dbg('raw integer:"%s"' % value[intstart:intend])
-            int = self._GetNumValue(value[intstart:intend])
-            numval = self._fromGUI(value)
-
-            dbg('integer: "%s"' % int)
-            try:
-                fracval = self.GetFraction(value)
-            except ValueError, e:
-                dbg('Exception:', e, 'must be out of bounds; disallow value')
-                self._disallowValue()
-                dbg(indent=0)
-                return
-
-            if fracval == 0.0:
-                dbg('self._isNeg?', self._isNeg)
-                if int == '-' and self._oldvalue < 0 and not self._typedSign:
-                    dbg('just a negative sign; old value < 0; setting replacement of 0')
-                    replacement = 0
-                    self._isNeg = False
-                elif int[:2] == '-0' and self._fractionWidth == 0:
-                    if self._oldvalue < 0:
-                        dbg('-0; setting replacement of 0')
-                        replacement = 0
-                        self._isNeg = False
-                    elif not self._limited or (self._min < -1 and self._max >= -1):
-                        dbg('-0; setting replacement of -1')
-                        replacement = -1
-                        self._isNeg = True
-                    else:
-                        # limited and -1 is out of bounds
-                        self._disallowValue()
-                        dbg(indent=0)
-                        return
-
-                elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0:
-                    if not self._limited or (self._min < -1 and self._max >= -1):
-                        dbg('just a negative sign; setting replacement of -1')
-                        replacement = -1
-                    else:
-                        # limited and -1 is out of bounds
-                        self._disallowValue()
-                        dbg(indent=0)
-                        return
-
-                elif( self._typedSign
-                      and int.find('-') != -1
-                      and self._limited
-                      and not self._min <= numval <= self._max):
-                    # changed sign resulting in value that's now out-of-bounds;
-                    # disallow
-                    self._disallowValue()
-                    dbg(indent=0)
-                    return
-
-            if replacement is None:
-                if int and int != '-':
-                    try:
-                        string.atol(int)
-                    except ValueError:
-                        # integer requested is not legal.  This can happen if the user
-                        # is attempting to insert a digit in the middle of the control
-                        # resulting in something like "   3   45". Disallow such actions:
-                        dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int)
-                        if not wx.Validator_IsSilent():
-                            wx.Bell()
-                        sel_start, sel_to = self._GetSelection()
-                        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
-                        wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position
-                        wx.CallAfter(self.SetSelection, sel_start, sel_to)
-                        dbg(indent=0)
-                        return
-
-                    if int[0] == '0' and len(int) > 1:
-                        dbg('numvalue: "%s"' % numvalue.replace(' ', ''))
-                        if self._fractionWidth:
-                            value = self._toGUI(string.atof(numvalue))
-                        else:
-                            value = self._toGUI(string.atol(numvalue))
-                        dbg('modified value: "%s"' % value)
-
-        self._typedSign = False     # reset state var
-
-        if replacement is not None:
-            # Value presented wasn't a legal number, but control should do something
-            # reasonable instead:
-            dbg('setting replacement value:', replacement)
-            self._SetValue(self._toGUI(replacement))
-            sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement)))   # find where it put the 1, so we can select it            
-            sel_to = sel_start + len(str(abs(replacement)))
-            dbg('queuing selection of (%d, %d)' %(sel_start, sel_to))
-            wx.CallAfter(self.SetInsertionPoint, sel_start)
-            wx.CallAfter(self.SetSelection, sel_start, sel_to)
-            dbg(indent=0)
-            return
-
-        # Otherwise, apply appropriate formatting to value:
-
-        # Because we're intercepting the value and adjusting it
-        # before a sign change is detected, we need to do this here:
-        if '-' in value or '(' in value:
-            self._isNeg = True
-        else:
-            self._isNeg = False
-
-        dbg('value:"%s"' % value, 'self._useParens:', self._useParens)
-        if self._fractionWidth:
-            adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar))
-        else:
-            adjvalue = self._adjustInt(self._GetNumValue(value))
-        dbg('adjusted value: "%s"' % adjvalue)
-
-
-        sel_start, sel_to = self._GetSelection()     # record current insertion point
-        dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue)
-        BaseMaskedTextCtrl._SetValue(self, adjvalue)
-        # After all actions so far scheduled, check that resulting cursor
-        # position is appropriate, and move if not:
-        wx.CallAfter(self._CheckInsertionPoint)
-
-        dbg('finished MaskedNumCtrl::_SetValue', indent=0)
-
-    def _CheckInsertionPoint(self):
-        # If current insertion point is before the end of the integer and
-        # its before the 1st digit, place it just after the sign position:
-        dbg('MaskedNumCtrl::CheckInsertionPoint', indent=1)
-        sel_start, sel_to = self._GetSelection()
-        text = self._GetValue()
-        if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('):
-            text, signpos, right_signpos = self._getSignedValue()
-            dbg('setting selection(%d, %d)' % (signpos+1, signpos+1))
-            self.SetInsertionPoint(signpos+1)
-            self.SetSelection(signpos+1, signpos+1)
-        dbg(indent=0)
-
-
-    def _OnErase( self, event ):
-        """
-        This overrides the base control _OnErase, so that erasing around
-        grouping characters auto selects the digit before or after the
-        grouping character, so that the erasure does the right thing.
-        """
-        dbg('MaskedNumCtrl::_OnErase', indent=1)
-
-        #if grouping digits, make sure deletes next to group char always
-        # delete next digit to appropriate side:
-        if self._groupDigits:
-            key = event.GetKeyCode()
-            value = BaseMaskedTextCtrl.GetValue(self)
-            sel_start, sel_to = self._GetSelection()
-
-            if key == wx.WXK_BACK:
-                # if 1st selected char is group char, select to previous digit
-                if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar:
-                    self.SetInsertionPoint(sel_start-1)
-                    self.SetSelection(sel_start-1, sel_to)
-
-                # elif previous char is group char, select to previous digit
-                elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar:
-                    self.SetInsertionPoint(sel_start-2)
-                    self.SetSelection(sel_start-2, sel_to)
-
-            elif key == wx.WXK_DELETE:
-                if( sel_to < len(self._mask) - 2 + (1 *self._useParens)
-                    and sel_start == sel_to
-                    and value[sel_to] == self._groupChar ):
-                    self.SetInsertionPoint(sel_start)
-                    self.SetSelection(sel_start, sel_to+2)
-
-                elif( sel_to < len(self._mask) - 2 + (1 *self._useParens)
-                           and value[sel_start:sel_to] == self._groupChar ):
-                    self.SetInsertionPoint(sel_start)
-                    self.SetSelection(sel_start, sel_to+1)
-
-        BaseMaskedTextCtrl._OnErase(self, event)
-        dbg(indent=0)
-
-
-    def OnTextChange( self, event ):
-        """
-        Handles an event indicating that the text control's value
-        has changed, and issue EVT_MaskedNum event.
-        NOTE: using wxTextCtrl.SetValue() to change the control's
-        contents from within a EVT_CHAR handler can cause double
-        text events.  So we check for actual changes to the text
-        before passing the events on.
-        """
-        dbg('MaskedNumCtrl::OnTextChange', indent=1)
-        if not BaseMaskedTextCtrl._OnTextChange(self, event):
-            dbg(indent=0)
-            return
-
-        # else... legal value
-
-        value = self.GetValue()
-        if value != self._oldvalue:
-            try:
-                self.GetEventHandler().ProcessEvent(
-                    MaskedNumNumberUpdatedEvent( self.GetId(), self.GetValue(), self ) )
-            except ValueError:
-                dbg(indent=0)
-                return
-            # let normal processing of the text continue
-            event.Skip()
-        self._oldvalue = value # record for next event
-        dbg(indent=0)
-
-    def _GetValue(self):
-        """
-        Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the
-        control with this function.
-        """
-        return wx.TextCtrl.GetValue(self)
-
-
-    def GetValue(self):
-        """
-        Returns the current numeric value of the control.
-        """
-        return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) )
-
-    def SetValue(self, value):
-        """
-        Sets the value of the control to the value specified.
-        The resulting actual value of the control may be altered to
-        conform with the bounds set on the control if limited,
-        or colored if not limited but the value is out-of-bounds.
-        A ValueError exception will be raised if an invalid value
-        is specified.
-        """
-        BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) )
-
-
-    def SetIntegerWidth(self, value):
-        self.SetParameters(integerWidth=value)
-    def GetIntegerWidth(self):
-        return self._integerWidth
-
-    def SetFractionWidth(self, value):
-        self.SetParameters(fractionWidth=value)
-    def GetFractionWidth(self):
-        return self._fractionWidth
-
-
-
-    def SetMin(self, min=None):
-        """
-        Sets the minimum value of the control.  If a value of None
-        is provided, then the control will have no explicit minimum value.
-        If the value specified is greater than the current maximum value,
-        then the function returns False and the minimum will not change from
-        its current setting.  On success, the function returns True.
-
-        If successful and the current value is lower than the new lower
-        bound, if the control is limited, the value will be automatically
-        adjusted to the new minimum value; if not limited, the value in the
-        control will be colored as invalid.
-
-        If min > the max value allowed by the width of the control,
-        the function will return False, and the min will not be set.
-        """
-        dbg('MaskedNumCtrl::SetMin(%s)' % repr(min), indent=1)
-        if( self._max is None
-            or min is None
-            or (self._max is not None and self._max >= min) ):
-            try:
-                self.SetParameters(min=min)
-                bRet = True
-            except ValueError:
-                bRet = False
-        else:
-            bRet = False
-        dbg(indent=0)
-        return bRet
-
-    def GetMin(self):
-        """
-        Gets the lower bound value of the control.  It will return
-        None if not specified.
-        """
-        return self._min
-
-
-    def SetMax(self, max=None):
-        """
-        Sets the maximum value of the control. If a value of None
-        is provided, then the control will have no explicit maximum value.
-        If the value specified is less than the current minimum value, then
-        the function returns False and the maximum will not change from its
-        current setting. On success, the function returns True.
-
-        If successful and the current value is greater than the new upper
-        bound, if the control is limited the value will be automatically
-        adjusted to this maximum value; if not limited, the value in the
-        control will be colored as invalid.
-
-        If max > the max value allowed by the width of the control,
-        the function will return False, and the max will not be set.
-        """
-        if( self._min is None
-            or max is None
-            or (self._min is not None and self._min <= max) ):
-            try:
-                self.SetParameters(max=max)
-                bRet = True
-            except ValueError:
-                bRet = False
-        else:
-            bRet = False
-
-        return bRet
-
-
-    def GetMax(self):
-        """
-        Gets the maximum value of the control.  It will return the current
-        maximum integer, or None if not specified.
-        """
-        return self._max
-
-
-    def SetBounds(self, min=None, max=None):
-        """
-        This function is a convenience function for setting the min and max
-        values at the same time.  The function only applies the maximum bound
-        if setting the minimum bound is successful, and returns True
-        only if both operations succeed.
-        NOTE: leaving out an argument will remove the corresponding bound.
-        """
-        ret = self.SetMin(min)
-        return ret and self.SetMax(max)
-
-
-    def GetBounds(self):
-        """
-        This function returns a two-tuple (min,max), indicating the
-        current bounds of the control.  Each value can be None if
-        that bound is not set.
-        """
-        return (self._min, self._max)
-
-
-    def SetLimited(self, limited):
-        """
-        If called with a value of True, this function will cause the control
-        to limit the value to fall within the bounds currently specified.
-        If the control's value currently exceeds the bounds, it will then
-        be limited accordingly.
-
-        If called with a value of False, this function will disable value
-        limiting, but coloring of out-of-bounds values will still take
-        place if bounds have been set for the control.
-        """
-        self.SetParameters(limited = limited)
-
-
-    def IsLimited(self):
-        """
-        Returns True if the control is currently limiting the
-        value to fall within the current bounds.
-        """
-        return self._limited
-
-    def GetLimited(self):
-        """ (For regularization of property accessors) """
-        return self.IsLimited
-
-
-    def IsInBounds(self, value=None):
-        """
-        Returns True if no value is specified and the current value
-        of the control falls within the current bounds.  This function can
-        also be called with a value to see if that value would fall within
-        the current bounds of the given control.
-        """
-        dbg('IsInBounds(%s)' % repr(value), indent=1)
-        if value is None:
-            value = self.GetValue()
-        else:
-            try:
-                value = self._GetNumValue(self._toGUI(value))
-            except ValueError, e:
-                dbg('error getting NumValue(self._toGUI(value)):', e, indent=0)
-                return False
-            if value.strip() == '':
-                value = None
-            elif self._fractionWidth:
-                value = float(value)
-            else:
-                value = long(value)
-
-        min = self.GetMin()
-        max = self.GetMax()
-        if min is None: min = value
-        if max is None: max = value
-
-        # if bounds set, and value is None, return False
-        if value == None and (min is not None or max is not None):
-            dbg('finished IsInBounds', indent=0)
-            return 0
-        else:
-            dbg('finished IsInBounds', indent=0)
-            return min <= value <= max
-
-
-    def SetAllowNone(self, allow_none):
-        """
-        Change the behavior of the validation code, allowing control
-        to have a value of None or not, as appropriate.  If the value
-        of the control is currently None, and allow_none is False, the
-        value of the control will be set to the minimum value of the
-        control, or 0 if no lower bound is set.
-        """
-        self._allowNone = allow_none
-        if not allow_none and self.GetValue() is None:
-            min = self.GetMin()
-            if min is not None: self.SetValue(min)
-            else:               self.SetValue(0)
-
-
-    def IsNoneAllowed(self):
-        return self._allowNone
-    def GetAllowNone(self):
-        """ (For regularization of property accessors) """
-        return self.IsNoneAllowed()
-
-    def SetAllowNegative(self, value):
-        self.SetParameters(allowNegative=value)
-    def IsNegativeAllowed(self):
-        return self._allowNegative
-    def GetAllowNegative(self):
-        """ (For regularization of property accessors) """
-        return self.IsNegativeAllowed()
-
-    def SetGroupDigits(self, value):
-        self.SetParameters(groupDigits=value)
-    def IsGroupingAllowed(self):
-        return self._groupDigits
-    def GetGroupDigits(self):
-        """ (For regularization of property accessors) """
-        return self.IsGroupingAllowed()
-
-    def SetGroupChar(self, value):
-        self.SetParameters(groupChar=value)
-    def GetGroupChar(self):
-        return self._groupChar
-
-    def SetDecimalChar(self, value):
-        self.SetParameters(decimalChar=value)
-    def GetDecimalChar(self):
-        return self._decimalChar
-
-    def SetSelectOnEntry(self, value):
-        self.SetParameters(selectOnEntry=value)
-    def GetSelectOnEntry(self):
-        return self._selectOnEntry
-
-    def SetAutoSize(self, value):
-        self.SetParameters(autoSize=value)
-    def GetAutoSize(self):
-        return self._autoSize
-
-
-    # (Other parameter accessors are inherited from base class)
-
-
-    def _toGUI( self, value, apply_limits = True ):
-        """
-        Conversion function used to set the value of the control; does
-        type and bounds checking and raises ValueError if argument is
-        not a valid value.
-        """
-        dbg('MaskedNumCtrl::_toGUI(%s)' % repr(value), indent=1)
-        if value is None and self.IsNoneAllowed():
-            dbg(indent=0)
-            return self._template
-
-        elif type(value) in (types.StringType, types.UnicodeType):
-            value = self._GetNumValue(value)
-            dbg('cleansed num value: "%s"' % value)
-            if value == "":
-                if self.IsNoneAllowed():
-                    dbg(indent=0)
-                    return self._template
-                else:
-                    dbg('exception raised:', e, indent=0)
-                    raise ValueError ('wxMaskedNumCtrl requires numeric value, passed %s'% repr(value) )
-            # else...
-            try:
-                if self._fractionWidth or value.find('.') != -1:
-                    value = float(value)
-                else:
-                    value = long(value)
-            except Exception, e:
-                dbg('exception raised:', e, indent=0)
-                raise ValueError ('MaskedNumCtrl requires numeric value, passed %s'% repr(value) )
-
-        elif type(value) not in (types.IntType, types.LongType, types.FloatType):
-            dbg(indent=0)
-            raise ValueError (
-                'MaskedNumCtrl requires numeric value, passed %s'% repr(value) )
-
-        if not self._allowNegative and value < 0:
-            raise ValueError (
-                'control configured to disallow negative values, passed %s'% repr(value) )
-
-        if self.IsLimited() and apply_limits:
-            min = self.GetMin()
-            max = self.GetMax()
-            if not min is None and value < min:
-                dbg(indent=0)
-                raise ValueError (
-                    'value %d is below minimum value of control'% value )
-            if not max is None and value > max:
-                dbg(indent=0)
-                raise ValueError (
-                    'value %d exceeds value of control'% value )
-
-        adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk)
-        dbg('len(%s):' % self._mask, len(self._mask))
-        dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace)
-        dbg('adjustwidth:', adjustwidth)
-        if self._fractionWidth == 0:
-            s = str(long(value)).rjust(self._integerWidth)
-        else:
-            format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth)
-            s = format % float(value)
-        dbg('s:"%s"' % s, 'len(s):', len(s))
-        if len(s) > (adjustwidth - self._groupSpace):
-            dbg(indent=0)
-            raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
-        elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace):
-            dbg(indent=0)
-            raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
-
-        s = s.rjust(adjustwidth).replace('.', self._decimalChar)
-        if self._signOk and self._useParens:
-            if s.find('-') != -1:
-                s = s.replace('-', '(') + ')'
-            else:
-                s += ' '
-        dbg('returned: "%s"' % s, indent=0)
-        return s
-
-
-    def _fromGUI( self, value ):
-        """
-        Conversion function used in getting the value of the control.
-        """
-        dbg(suspend=0)
-        dbg('MaskedNumCtrl::_fromGUI(%s)' % value, indent=1)
-        # One or more of the underlying text control implementations
-        # issue an intermediate EVT_TEXT when replacing the control's
-        # value, where the intermediate value is an empty string.
-        # So, to ensure consistency and to prevent spurious ValueErrors,
-        # we make the following test, and react accordingly:
-        #
-        if value.strip() == '':
-            if not self.IsNoneAllowed():
-                dbg('empty value; not allowed,returning 0', indent = 0)
-                if self._fractionWidth:
-                    return 0.0
-                else:
-                    return 0
-            else:
-                dbg('empty value; returning None', indent = 0)
-                return None
-        else:
-            value = self._GetNumValue(value)
-            dbg('Num value: "%s"' % value)
-            if self._fractionWidth:
-                try:
-                    dbg(indent=0)
-                    return float( value )
-                except ValueError:
-                    dbg("couldn't convert to float; returning None")
-                    return None
-                else:
-                    raise
-            else:
-                try:
-                    dbg(indent=0)
-                    return int( value )
-                except ValueError:
-                    try:
-                       dbg(indent=0)
-                       return long( value )
-                    except ValueError:
-                       dbg("couldn't convert to long; returning None")
-                       return None
-
-                    else:
-                       raise
-                else:
-                    dbg('exception occurred; returning None')
-                    return None
-
-
-    def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
-        """
-        Preprocessor for base control paste; if value needs to be right-justified
-        to fit in control, do so prior to paste:
-        """
-        dbg('MaskedNumCtrl::_Paste (value = "%s")' % value)
-        if value is None:
-            paste_text = self._getClipboardContents()
-        else:
-            paste_text = value
-
-        # treat paste as "replace number", if appropriate:
-        sel_start, sel_to = self._GetSelection()
-        if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent:
-            paste_text = self._toGUI(paste_text)
-            self._SetSelection(0, len(self._mask))
-
-        return MaskedEditMixin._Paste(self,
-                                        paste_text,
-                                        raise_on_invalid=raise_on_invalid,
-                                        just_return_value=just_return_value)
-
-
-
-#===========================================================================
-
-if __name__ == '__main__':
-
-    import traceback
-
-    class myDialog(wx.Dialog):
-        def __init__(self, parent, id, title,
-            pos = wx.DefaultPosition, size = wx.DefaultSize,
-            style = wx.DEFAULT_DIALOG_STYLE ):
-            wx.Dialog.__init__(self, parent, id, title, pos, size, style)
-
-            self.int_ctrl = MaskedNumCtrl(self, wx.NewId(), size=(55,20))
-            self.OK = wx.Button( self, wx.ID_OK, "OK")
-            self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel")
-
-            vs = wx.BoxSizer( wx.VERTICAL )
-            vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
-            hs = wx.BoxSizer( wx.HORIZONTAL )
-            hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
-            hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
-            vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
-
-            self.SetAutoLayout( True )
-            self.SetSizer( vs )
-            vs.Fit( self )
-            vs.SetSizeHints( self )
-            self.Bind(EVT_MASKEDNUM, self.OnChange, self.int_ctrl)
-
-        def OnChange(self, event):
-            print 'value now', event.GetValue()
-
-    class TestApp(wx.App):
-        def OnInit(self):
-            try:
-                self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100)  )
-                self.panel = wx.Panel(self.frame, -1)
-                button = wx.Button(self.panel, -1, "Push Me", (20, 20))
-                self.Bind(wx.EVT_BUTTON, self.OnClick, button)
-            except:
-                traceback.print_exc()
-                return False
-            return True
-
-        def OnClick(self, event):
-            dlg = myDialog(self.panel, -1, "test MaskedNumCtrl")
-            dlg.int_ctrl.SetValue(501)
-            dlg.int_ctrl.SetInsertionPoint(1)
-            dlg.int_ctrl.SetSelection(1,2)
-            rc = dlg.ShowModal()
-            print 'final value', dlg.int_ctrl.GetValue()
-            del dlg
-            self.frame.Destroy()
-
-        def Show(self):
-            self.frame.Show(True)
-
-    try:
-        app = TestApp(0)
-        app.Show()
-        app.MainLoop()
-    except:
-        traceback.print_exc()
-
-i=0
-## To-Do's:
-## =============================##
-##   1. Add support for printf-style format specification.
-##   2. Add option for repositioning on 'illegal' insertion point.
-##
-## Version 1.1
-##   1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions.
-##   2. Added autoSize parameter, to allow manual sizing of the control.
-##   3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of
-##      nonsensical parameter methods from the control, so it will work
-##      properly with Boa.
-##   4. Fixed allowNone bug found by user sameerc1@grandecom.net
-
diff --git a/wxPython/wx/lib/timectrl.py b/wxPython/wx/lib/timectrl.py
deleted file mode 100644 (file)
index af42f08..0000000
+++ /dev/null
@@ -1,1315 +0,0 @@
-#----------------------------------------------------------------------------
-# Name:         timectrl.py
-# Author:       Will Sadkin
-# Created:      09/19/2002
-# Copyright:    (c) 2002 by Will Sadkin, 2002
-# RCS-ID:       $Id$
-# License:      wxWindows license
-#----------------------------------------------------------------------------
-# NOTE:
-#   This was written way it is because of the lack of masked edit controls
-#   in wxWindows/wxPython.  I would also have preferred to derive this
-#   control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl
-#   component of that control is inaccessible through the interface exposed in
-#   wxPython.
-#
-#   TimeCtrl does not use validators, because it does careful manipulation
-#   of the cursor in the text window on each keystroke, and validation is
-#   cursor-position specific, so the control intercepts the key codes before the
-#   validator would fire.
-#
-#   TimeCtrl now also supports .SetValue() with either strings or wxDateTime
-#   values, as well as range limits, with the option of either enforcing them
-#   or simply coloring the text of the control if the limits are exceeded.
-#
-#   Note: this class now makes heavy use of wxDateTime for parsing and
-#   regularization, but it always does so with ephemeral instances of
-#   wxDateTime, as the C++/Python validity of these instances seems to not
-#   persist.  Because "today" can be a day for which an hour can "not exist"
-#   or be counted twice (1 day each per year, for DST adjustments), the date
-#   portion of all wxDateTimes used/returned have their date portion set to
-#   Jan 1, 1970 (the "epoch.")
-#----------------------------------------------------------------------------
-# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Updated for V2.5 compatability
-# o wx.SpinCtl has some issues that cause the control to
-#   lock up. Noted in other places using it too, it's not this module
-#   that's at fault.
-# 
-# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o wxMaskedTextCtrl -> MaskedTextCtrl
-# o wxTimeCtrl -> TimeCtrl
-#
-
-"""<html><body>
-<P>
-<B>TimeCtrl</B> provides a multi-cell control that allows manipulation of a time
-value.  It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime
-to get/set values from the control.
-<P>
-Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act
-like a spin control.  TimeCtrl also allows for an actual spin button to be attached
-to the control, so that it acts like the up/down arrow keys.
-<P>
-The <B>!</B> or <B>c</B> key sets the value of the control to the current time.
-<P>
-Here's the API for TimeCtrl:
-<DL><PRE>
-    <B>TimeCtrl</B>(
-         parent, id = -1,
-         <B>value</B> = '12:00:00 AM',
-         pos = wx.DefaultPosition,
-         size = wx.DefaultSize,
-         <B>style</B> = wxTE_PROCESS_TAB,
-         <B>validator</B> = wx.DefaultValidator,
-         name = "time",
-         <B>format</B> = 'HHMMSS',         
-         <B>fmt24hr</B> = False,
-         <B>displaySeconds</B> = True,
-         <B>spinButton</B> = None,
-         <B>min</B> = None,
-         <B>max</B> = None,
-         <B>limited</B> = None,
-         <B>oob_color</B> = "Yellow"
-)
-</PRE>
-<UL>
-    <DT><B>value</B>
-    <DD>If no initial value is set, the default will be midnight; if an illegal string
-    is specified, a ValueError will result.  (You can always later set the initial time
-    with SetValue() after instantiation of the control.)
-    <DL><B>size</B>
-    <DD>The size of the control will be automatically adjusted for 12/24 hour format
-    if wx.DefaultSize is specified.
-    <DT><B>style</B>
-    <DD>By default, TimeCtrl will process TAB events, by allowing tab to the
-    different cells within the control.
-    <DT><B>validator</B>
-    <DD>By default, TimeCtrl just uses the default (empty) validator, as all
-    of its validation for entry control is handled internally.  However, a validator
-    can be supplied to provide data transfer capability to the control.
-    <BR>
-    <DT><B>format</B>
-    <DD>This parameter can be used instead of the fmt24hr and displaySeconds
-    parameters, respectively; it provides a shorthand way to specify the time
-    format you want.  Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and
-    '24HHMM'.  If the format is specified, the other two  arguments will be ignored.    
-    <BR>
-    <DT><B>fmt24hr</B>
-    <DD>If True, control will display time in 24 hour time format; if False, it will
-    use 12 hour AM/PM format.  SetValue() will adjust values accordingly for the
-    control, based on the format specified.  (This value is ignored if the <i>format</i>
-    parameter is specified.)
-    <BR>
-    <DT><B>displaySeconds</B>
-    <DD>If True, control will include a seconds field; if False, it will
-    just show hours and minutes. (This value is ignored if the <i>format</i>
-    parameter is specified.)
-    <BR>    
-    <DT><B>spinButton</B>
-    <DD>If specified, this button's events will be bound to the behavior of the
-    TimeCtrl, working like up/down cursor key events.  (See BindSpinButton.)
-    <BR>
-    <DT><B>min</B>
-    <DD>Defines the lower bound for "valid" selections in the control.
-    By default, TimeCtrl doesn't have bounds.  You must set both upper and lower
-    bounds to make the control pay attention to them, (as only one bound makes no sense
-    with times.) "Valid" times will fall between the min and max "pie wedge" of the
-    clock.
-    <DT><B>max</B>
-    <DD>Defines the upper bound for "valid" selections in the control.
-    "Valid" times will fall between the min and max "pie wedge" of the
-    clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
-    means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
-    <DT><B>limited</B>
-    <DD>If True, the control will not permit entry of values that fall outside the
-    set bounds.
-    <BR>
-    <DT><B>oob_color</B>
-    <DD>Sets the background color used to indicate out-of-bounds values for the control
-    when the control is not limited.  This is set to "Yellow" by default.
-    </DL>
-</UL>
-<BR>
-<BR>
-<BR>
-<DT><B>EVT_TIMEUPDATE(win, id, func)</B>
-<DD>func is fired whenever the value of the control changes.
-<BR>
-<BR>
-<DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
-<DD>Sets the value of the control to a particular time, given a valid
-value; raises ValueError on invalid value.
-<EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
-was successfully imported by the class module.
-<BR>
-<DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
-<DD>Retrieves the value of the time from the control.  By default this is
-returned as a string, unless one of the other arguments is set; args are
-searched in the order listed; only one value will be returned.
-<BR>
-<DT><B>GetWxDateTime(value=None)</B>
-<DD>When called without arguments, retrieves the value of the control, and applies
-it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
-The date portion will always be set to Jan 1, 1970. This form is the same
-as GetValue(as_wxDateTime=True).  GetWxDateTime can also be called with any of the
-other valid time formats settable with SetValue, to regularize it to a single
-wxDateTime form.  The function will raise ValueError on an unconvertable argument.
-<BR>
-<DT><B>GetMxDateTime()</B>
-<DD>Retrieves the value of the control and applies it to the DateTime.Time()
-constructor,and returns the resulting value.  (The date portion will always be
-set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
-compatibility with previous release.)
-<BR>
-<BR>
-<DT><B>BindSpinButton(SpinBtton)</B>
-<DD>Binds an externally created spin button to the control, so that up/down spin
-events change the active cell or selection in the control (in addition to the
-up/down cursor keys.)  (This is primarily to allow you to create a "standard"
-interface to time controls, as seen in Windows.)
-<BR>
-<BR>
-<DT><B>SetMin(min=None)</B>
-<DD>Sets the expected minimum value, or lower bound, of the control.
-(The lower bound will only be enforced if the control is
-configured to limit its values to the set bounds.)
-If a value of <I>None</I> is provided, then the control will have
-explicit lower bound.  If the value specified is greater than
-the current lower bound, then the function returns False and the
-lower bound will not change from its current setting.  On success,
-the function returns True.  Even if set, if there is no corresponding
-upper bound, the control will behave as if it is unbounded.
-<DT><DD>If successful and the current value is outside the
-new bounds, if the control is limited the value will be
-automatically adjusted to the nearest bound; if not limited,
-the background of the control will be colored with the current
-out-of-bounds color.
-<BR>
-<DT><B>GetMin(as_string=False)</B>
-<DD>Gets the current lower bound value for the control, returning
-None, if not set, or a wxDateTime, unless the as_string parameter
-is set to True, at which point it will return the string
-representation of the lower bound.
-<BR>
-<BR>
-<DT><B>SetMax(max=None)</B>
-<DD>Sets the expected maximum value, or upper bound, of the control.
-(The upper bound will only be enforced if the control is
-configured to limit its values to the set bounds.)
-If a value of <I>None</I> is provided, then the control will
-have no explicit upper bound.  If the value specified is less
-than the current lower bound, then the function returns False and
-the maximum will not change from its current setting. On success,
-the function returns True.  Even if set, if there is no corresponding
-lower bound, the control will behave as if it is unbounded.
-<DT><DD>If successful and the current value is outside the
-new bounds, if the control is limited the value will be
-automatically adjusted to the nearest bound; if not limited,
-the background of the control will be colored with the current
-out-of-bounds color.
-<BR>
-<DT><B>GetMax(as_string = False)</B>
-<DD>Gets the current upper bound value for the control, returning
-None, if not set, or a wxDateTime, unless the as_string parameter
-is set to True, at which point it will return the string
-representation of the lower bound.
-
-<BR>
-<BR>
-<DT><B>SetBounds(min=None,max=None)</B>
-<DD>This function is a convenience function for setting the min and max
-values at the same time.  The function only applies the maximum bound
-if setting the minimum bound is successful, and returns True
-only if both operations succeed.  <B><I>Note: leaving out an argument
-will remove the corresponding bound, and result in the behavior of
-an unbounded control.</I></B>
-<BR>
-<DT><B>GetBounds(as_string = False)</B>
-<DD>This function returns a two-tuple (min,max), indicating the
-current bounds of the control.  Each value can be None if
-that bound is not set.  The values will otherwise be wxDateTimes
-unless the as_string argument is set to True, at which point they
-will be returned as string representations of the bounds.
-<BR>
-<BR>
-<DT><B>IsInBounds(value=None)</B>
-<DD>Returns <I>True</I> if no value is specified and the current value
-of the control falls within the current bounds.  This function can also
-be called with a value to see if that value would fall within the current
-bounds of the given control.  It will raise ValueError if the value
-specified is not a wxDateTime, mxDateTime (if available) or parsable string.
-<BR>
-<BR>
-<DT><B>IsValid(value)</B>
-<DD>Returns <I>True</I>if specified value is a legal time value and
-falls within the current bounds of the given control.
-<BR>
-<BR>
-<DT><B>SetLimited(bool)</B>
-<DD>If called with a value of True, this function will cause the control
-to limit the value to fall within the bounds currently specified.
-(Provided both bounds have been set.)
-If the control's value currently exceeds the bounds, it will then
-be set to the nearest bound.
-If called with a value of False, this function will disable value
-limiting, but coloring of out-of-bounds values will still take
-place if bounds have been set for the control.
-<DT><B>IsLimited()</B>
-<DD>Returns <I>True</I> if the control is currently limiting the
-value to fall within the current bounds.
-<BR>
-</DL>
-</body></html>
-"""
-
-import  copy
-import  string
-import  types
-
-import  wx
-
-from wx.tools.dbg import Logger
-from wx.lib.maskededit import BaseMaskedTextCtrl, Field
-
-dbg = Logger()
-dbg(enable=0)
-
-try:
-    from mx import DateTime
-    accept_mx = True
-except ImportError:
-    accept_mx = False
-
-# This class of event fires whenever the value of the time changes in the control:
-wxEVT_TIMEVAL_UPDATED = wx.NewEventType()
-EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1)
-
-class TimeUpdatedEvent(wx.PyCommandEvent):
-    def __init__(self, id, value ='12:00:00 AM'):
-        wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id)
-        self.value = value
-    def GetValue(self):
-        """Retrieve the value of the time control at the time this event was generated"""
-        return self.value
-
-class TimeCtrlAccessorsMixin:
-    # Define TimeCtrl's list of attributes having their own
-    # Get/Set functions, ignoring those that make no sense for
-    # an numeric control.
-    exposed_basectrl_params = (
-         'defaultValue',
-         'description',
-
-         'useFixedWidthFont',
-         'emptyBackgroundColour',
-         'validBackgroundColour',
-         'invalidBackgroundColour',
-
-         'validFunc',
-         'validRequired',
-        )
-    for param in exposed_basectrl_params:
-        propname = param[0].upper() + param[1:]
-        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-
-        if param.find('Colour') != -1:
-            # add non-british spellings, for backward-compatibility
-            propname.replace('Colour', 'Color')
-
-            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
-            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
-class TimeCtrl(BaseMaskedTextCtrl):
-
-    valid_ctrl_params = {
-        'format' :  'HHMMSS',       # default format code
-        'displaySeconds' : True,    # by default, shows seconds
-        'min': None,                # by default, no bounds set
-        'max': None,
-        'limited': False,           # by default, no limiting even if bounds set
-        'useFixedWidthFont': True,  # by default, use a fixed-width font
-        'oob_color': "Yellow"       # by default, the default MaskedTextCtrl "invalid" color
-        }
-
-    def __init__ (
-                self, parent, id=-1, value = '12:00:00 AM',
-                pos = wx.DefaultPosition, size = wx.DefaultSize,
-                fmt24hr=False,
-                spinButton = None,
-                style = wx.TE_PROCESS_TAB,
-                validator = wx.DefaultValidator,
-                name = "time",
-                **kwargs ):
-
-        # set defaults for control:
-        dbg('setting defaults:')
-        for key, param_value in TimeCtrl.valid_ctrl_params.items():
-            # This is done this way to make setattr behave consistently with
-            # "private attribute" name mangling
-            setattr(self, "_TimeCtrl__" + key, copy.copy(param_value))
-
-        # create locals from current defaults, so we can override if
-        # specified in kwargs, and handle uniformly:
-        min = self.__min
-        max = self.__max
-        limited = self.__limited
-        self.__posCurrent = 0
-        # handle deprecated keword argument name:
-        if kwargs.has_key('display_seconds'):
-            kwargs['displaySeconds'] = kwargs['display_seconds']
-            del kwargs['display_seconds']
-        if not kwargs.has_key('displaySeconds'):
-            kwargs['displaySeconds'] = True
-
-        # (handle positional arg (from original release) differently from rest of kwargs:)
-        self.__fmt24hr = False
-        if not kwargs.has_key('format'):
-            if fmt24hr:
-                if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
-                    kwargs['format'] = '24HHMMSS'
-                    del kwargs['displaySeconds']
-                else:
-                    kwargs['format'] = '24HHMM'
-            else:
-                if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
-                    kwargs['format'] = 'HHMMSS'
-                    del kwargs['displaySeconds']
-                else:
-                    kwargs['format'] = 'HHMM'
-
-        if not kwargs.has_key('useFixedWidthFont'):
-            # allow control over font selection:
-            kwargs['useFixedWidthFont'] = self.__useFixedWidthFont
-
-        maskededit_kwargs = self.SetParameters(**kwargs)
-
-        # allow for explicit size specification:
-        if size != wx.DefaultSize:
-            # override (and remove) "autofit" autoformat code in standard time formats:
-            maskededit_kwargs['formatcodes'] = 'T!'
-
-        # This allows range validation if set
-        maskededit_kwargs['validFunc'] = self.IsInBounds
-
-        # This allows range limits to affect insertion into control or not
-        # dynamically without affecting individual field constraint validation
-        maskededit_kwargs['retainFieldValidation'] = True
-
-        # Now we can initialize the base control:
-        BaseMaskedTextCtrl.__init__(
-                self, parent, id=id,
-                pos=pos, size=size,
-                style = style,
-                validator = validator,
-                name = name,
-                setupEventHandling = False,
-                **maskededit_kwargs)
-
-
-        # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
-        self._SetKeyHandler(':', self._OnChangeField)
-
-
-        # This makes the up/down keys act like spin button controls:
-        self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp)
-        self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown)
-
-
-        # This allows ! and c/C to set the control to the current time:
-        self._SetKeyHandler('!', self.__OnSetToNow)
-        self._SetKeyHandler('c', self.__OnSetToNow)
-        self._SetKeyHandler('C', self.__OnSetToNow)
-
-
-        # Set up event handling ourselves, so we can insert special
-        # processing on the ":' key to remove the "shift" attribute
-        # *before* the default handlers have been installed, so
-        # that : takes you forward, not back, and so we can issue
-        # EVT_TIMEUPDATE events on changes:
-
-        self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection
-        self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator
-        self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field
-        self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick
-        self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab.
-        self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event,
-                                                            ## then call BaseMaskedTextCtrl._OnChar with
-                                                            ## the possibly modified event.
-        self.Bind(wx.EVT_TEXT, self.__OnTextChange, self )  ## color control appropriately and EVT_TIMEUPDATE events
-
-
-        # Validate initial value and set if appropriate
-        try:
-            self.SetBounds(min, max)
-            self.SetLimited(limited)
-            self.SetValue(value)
-        except:
-            self.SetValue('12:00:00 AM')
-
-        if spinButton:
-            self.BindSpinButton(spinButton)     # bind spin button up/down events to this control
-
-
-    def SetParameters(self, **kwargs):
-        dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1)
-        maskededit_kwargs = {}
-        reset_format = False
-
-        if kwargs.has_key('display_seconds'):
-            kwargs['displaySeconds'] = kwargs['display_seconds']
-            del kwargs['display_seconds']
-        if kwargs.has_key('format') and kwargs.has_key('displaySeconds'):
-            del kwargs['displaySeconds']    # always apply format if specified
-
-        # assign keyword args as appropriate:
-        for key, param_value in kwargs.items():
-            if key not in TimeCtrl.valid_ctrl_params.keys():
-                raise AttributeError('invalid keyword argument "%s"' % key)
-
-            if key == 'format':
-                # handle both local or generic 'maskededit' autoformat codes:
-                if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS':
-                    self.__displaySeconds = True
-                    self.__fmt24hr = False
-                elif param_value == 'HHMM' or param_value == 'TIMEHHMM':
-                    self.__displaySeconds = False
-                    self.__fmt24hr = False
-                elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS':
-                    self.__displaySeconds = True
-                    self.__fmt24hr = True
-                elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM':
-                    self.__displaySeconds = False
-                    self.__fmt24hr = True
-                else:
-                    raise AttributeError('"%s" is not a valid format' % param_value)
-                reset_format = True
-
-            elif key in ("displaySeconds",  "display_seconds") and not kwargs.has_key('format'):
-                self.__displaySeconds = param_value
-                reset_format = True
-
-            elif key == "min":      min = param_value
-            elif key == "max":      max = param_value
-            elif key == "limited":  limited = param_value
-
-            elif key == "useFixedWidthFont":
-                maskededit_kwargs[key] = param_value
-
-            elif key == "oob_color":
-                maskededit_kwargs['invalidBackgroundColor'] = param_value
-
-        if reset_format:
-            if self.__fmt24hr:
-                if self.__displaySeconds:  maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS'
-                else:                      maskededit_kwargs['autoformat'] = '24HRTIMEHHMM'
-
-                # Set hour field to zero-pad, right-insert, require explicit field change,
-                # select entire field on entry, and require a resultant valid entry
-                # to allow character entry:
-                hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True)
-            else:
-                if self.__displaySeconds:  maskededit_kwargs['autoformat'] = 'TIMEHHMMSS'
-                else:                      maskededit_kwargs['autoformat'] = 'TIMEHHMM'
-
-                # Set hour field to allow spaces (at start), right-insert,
-                # require explicit field change, select entire field on entry,
-                # and require a resultant valid entry to allow character entry:
-                hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True)
-                ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True)
-
-            # Field 1 is always a zero-padded right-insert minute field,
-            # similarly configured as above:
-            minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True)
-
-            fields = [ hourfield, minutefield ]
-            if self.__displaySeconds:
-                fields.append(copy.copy(minutefield))    # second field has same constraints as field 1
-
-            if not self.__fmt24hr:
-                fields.append(ampmfield)
-
-            # set fields argument:
-            maskededit_kwargs['fields'] = fields
-
-            # This allows range validation if set
-            maskededit_kwargs['validFunc'] = self.IsInBounds
-
-            # This allows range limits to affect insertion into control or not
-            # dynamically without affecting individual field constraint validation
-            maskededit_kwargs['retainFieldValidation'] = True
-
-        if hasattr(self, 'controlInitialized') and self.controlInitialized:
-            self.SetCtrlParameters(**maskededit_kwargs)   # set appropriate parameters
-
-            # Validate initial value and set if appropriate
-            try:
-                self.SetBounds(min, max)
-                self.SetLimited(limited)
-                self.SetValue(value)
-            except:
-                self.SetValue('12:00:00 AM')
-            dbg(indent=0)
-            return {}   # no arguments to return
-        else:
-            dbg(indent=0)
-            return maskededit_kwargs
-
-
-    def BindSpinButton(self, sb):
-        """
-        This function binds an externally created spin button to the control, so that
-        up/down events from the button automatically change the control.
-        """
-        dbg('TimeCtrl::BindSpinButton')
-        self.__spinButton = sb
-        if self.__spinButton:
-            # bind event handlers to spin ctrl
-            self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton)
-            self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton)
-
-
-    def __repr__(self):
-        return "<TimeCtrl: %s>" % self.GetValue()
-
-
-    def SetValue(self, value):
-        """
-        Validating SetValue function for time values:
-        This function will do dynamic type checking on the value argument,
-        and convert wxDateTime, mxDateTime, or 12/24 format time string
-        into the appropriate format string for the control.
-        """
-        dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1)
-        try:
-            strtime = self._toGUI(self.__validateValue(value))
-        except:
-            dbg('validation failed', indent=0)
-            raise
-
-        dbg('strtime:', strtime)
-        self._SetValue(strtime)
-        dbg(indent=0)
-
-    def GetValue(self,
-                 as_wxDateTime = False,
-                 as_mxDateTime = False,
-                 as_wxTimeSpan = False,
-                 as_mxDateTimeDelta = False):
-
-
-        if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta:
-            value = self.GetWxDateTime()
-            if as_wxDateTime:
-                pass
-            elif as_mxDateTime:
-                value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond())
-            elif as_wxTimeSpan:
-                value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
-            elif as_mxDateTimeDelta:
-                value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
-        else:
-            value = BaseMaskedTextCtrl.GetValue(self)
-        return value
-
-
-    def SetWxDateTime(self, wxdt):
-        """
-        Because SetValue can take a wxDateTime, this is now just an alias.
-        """
-        self.SetValue(wxdt)
-
-
-    def GetWxDateTime(self, value=None):
-        """
-        This function is the conversion engine for TimeCtrl; it takes
-        one of the following types:
-            time string
-            wxDateTime
-            wxTimeSpan
-            mxDateTime
-            mxDateTimeDelta
-        and converts it to a wxDateTime that always has Jan 1, 1970 as its date
-        portion, so that range comparisons around values can work using
-        wxDateTime's built-in comparison function.  If a value is not
-        provided to convert, the string value of the control will be used.
-        If the value is not one of the accepted types, a ValueError will be
-        raised.
-        """
-        global accept_mx
-        dbg(suspend=1)
-        dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
-        if value is None:
-            dbg('getting control value')
-            value = self.GetValue()
-            dbg('value = "%s"' % value)
-
-        if type(value) == types.UnicodeType:
-            value = str(value)  # convert to regular string
-
-        valid = True    # assume true
-        if type(value) == types.StringType:
-
-            # Construct constant wxDateTime, then try to parse the string:
-            wxdt = wx.DateTimeFromDMY(1, 0, 1970)
-            dbg('attempting conversion')
-            value = value.strip()    # (parser doesn't like leading spaces)
-            checkTime    = wxdt.ParseTime(value)
-            valid = checkTime == len(value)     # entire string parsed?
-            dbg('checkTime == len(value)?', valid)
-
-            if not valid:
-                dbg(indent=0, suspend=0)
-                raise ValueError('cannot convert string "%s" to valid time' % value)
-
-        else:
-            if isinstance(value, wx.DateTime):
-                hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
-            elif isinstance(value, wx.TimeSpan):
-                totalseconds = value.GetSeconds()
-                hour = totalseconds / 3600
-                minute = totalseconds / 60 - (hour * 60)
-                second = totalseconds - ((hour * 3600) + (minute * 60))
-
-            elif accept_mx and isinstance(value, DateTime.DateTimeType):
-                hour, minute, second = value.hour, value.minute, value.second
-            elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType):
-                hour, minute, second = value.hour, value.minute, value.second
-            else:
-                # Not a valid function argument
-                if accept_mx:
-                    error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value)
-                else:
-                    error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value)
-                dbg(indent=0, suspend=0)
-                raise ValueError(error)
-
-            wxdt = wx.DateTimeFromDMY(1, 0, 1970)
-            wxdt.SetHour(hour)
-            wxdt.SetMinute(minute)
-            wxdt.SetSecond(second)
-
-        dbg('wxdt:', wxdt, indent=0, suspend=0)
-        return wxdt
-
-
-    def SetMxDateTime(self, mxdt):
-        """
-        Because SetValue can take an mxDateTime, (if DateTime is importable),
-        this is now just an alias.
-        """
-        self.SetValue(value)
-
-
-    def GetMxDateTime(self, value=None):
-        if value is None:
-            t = self.GetValue(as_mxDateTime=True)
-        else:
-            # Convert string 1st to wxDateTime, then use components, since
-            # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
-            wxdt = self.GetWxDateTime(value)
-            hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()
-            t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second)
-        return t
-
-
-    def SetMin(self, min=None):
-        """
-        Sets the minimum value of the control.  If a value of None
-        is provided, then the control will have no explicit minimum value.
-        If the value specified is greater than the current maximum value,
-        then the function returns 0 and the minimum will not change from
-        its current setting.  On success, the function returns 1.
-
-        If successful and the current value is lower than the new lower
-        bound, if the control is limited, the value will be automatically
-        adjusted to the new minimum value; if not limited, the value in the
-        control will be colored as invalid.
-        """
-        dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1)
-        if min is not None:
-            try:
-                min = self.GetWxDateTime(min)
-                self.__min = self._toGUI(min)
-            except:
-                dbg('exception occurred', indent=0)
-                return False
-        else:
-            self.__min = min
-
-        if self.IsLimited() and not self.IsInBounds():
-            self.SetLimited(self.__limited) # force limited value:
-        else:
-            self._CheckValid()
-        ret = True
-        dbg('ret:', ret, indent=0)
-        return ret
-
-
-    def GetMin(self, as_string = False):
-        """
-        Gets the minimum value of the control.
-        If None, it will return None.  Otherwise it will return
-        the current minimum bound on the control, as a wxDateTime
-        by default, or as a string if as_string argument is True.
-        """
-        dbg(suspend=1)
-        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
-        if self.__min is None:
-            dbg('(min == None)')
-            ret = self.__min
-        elif as_string:
-            ret = self.__min
-            dbg('ret:', ret)
-        else:
-            try:
-                ret = self.GetWxDateTime(self.__min)
-            except:
-                dbg(suspend=0)
-                dbg('exception occurred', indent=0)
-            dbg('ret:', repr(ret))
-        dbg(indent=0, suspend=0)
-        return ret
-
-
-    def SetMax(self, max=None):
-        """
-        Sets the maximum value of the control. If a value of None
-        is provided, then the control will have no explicit maximum value.
-        If the value specified is less than the current minimum value, then
-        the function returns False and the maximum will not change from its
-        current setting. On success, the function returns True.
-
-        If successful and the current value is greater than the new upper
-        bound, if the control is limited the value will be automatically
-        adjusted to this maximum value; if not limited, the value in the
-        control will be colored as invalid.
-        """
-        dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1)
-        if max is not None:
-            try:
-                max = self.GetWxDateTime(max)
-                self.__max = self._toGUI(max)
-            except:
-                dbg('exception occurred', indent=0)
-                return False
-        else:
-            self.__max = max
-        dbg('max:', repr(self.__max))
-        if self.IsLimited() and not self.IsInBounds():
-            self.SetLimited(self.__limited) # force limited value:
-        else:
-            self._CheckValid()
-        ret = True
-        dbg('ret:', ret, indent=0)
-        return ret
-
-
-    def GetMax(self, as_string = False):
-        """
-        Gets the minimum value of the control.
-        If None, it will return None.  Otherwise it will return
-        the current minimum bound on the control, as a wxDateTime
-        by default, or as a string if as_string argument is True.
-        """
-        dbg(suspend=1)
-        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
-        if self.__max is None:
-            dbg('(max == None)')
-            ret = self.__max
-        elif as_string:
-            ret = self.__max
-            dbg('ret:', ret)
-        else:
-            try:
-                ret = self.GetWxDateTime(self.__max)
-            except:
-                dbg(suspend=0)
-                dbg('exception occurred', indent=0)
-                raise
-            dbg('ret:', repr(ret))
-        dbg(indent=0, suspend=0)
-        return ret
-
-
-    def SetBounds(self, min=None, max=None):
-        """
-        This function is a convenience function for setting the min and max
-        values at the same time.  The function only applies the maximum bound
-        if setting the minimum bound is successful, and returns True
-        only if both operations succeed.
-        NOTE: leaving out an argument will remove the corresponding bound.
-        """
-        ret = self.SetMin(min)
-        return ret and self.SetMax(max)
-
-
-    def GetBounds(self, as_string = False):
-        """
-        This function returns a two-tuple (min,max), indicating the
-        current bounds of the control.  Each value can be None if
-        that bound is not set.
-        """
-        return (self.GetMin(as_string), self.GetMax(as_string))
-
-
-    def SetLimited(self, limited):
-        """
-        If called with a value of True, this function will cause the control
-        to limit the value to fall within the bounds currently specified.
-        If the control's value currently exceeds the bounds, it will then
-        be limited accordingly.
-
-        If called with a value of 0, this function will disable value
-        limiting, but coloring of out-of-bounds values will still take
-        place if bounds have been set for the control.
-        """
-        dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1)
-        self.__limited = limited
-
-        if not limited:
-            self.SetMaskParameters(validRequired = False)
-            self._CheckValid()
-            dbg(indent=0)
-            return
-
-        dbg('requiring valid value')
-        self.SetMaskParameters(validRequired = True)
-
-        min = self.GetMin()
-        max = self.GetMax()
-        if min is None or max is None:
-            dbg('both bounds not set; no further action taken')
-            return  # can't limit without 2 bounds
-
-        elif not self.IsInBounds():
-            # set value to the nearest bound:
-            try:
-                value = self.GetWxDateTime()
-            except:
-                dbg('exception occurred', indent=0)
-                raise
-
-            if min <= max:   # valid range doesn't span midnight
-                dbg('min <= max')
-                # which makes the "nearest bound" computation trickier...
-
-                # determine how long the "invalid" pie wedge is, and cut
-                # this interval in half for comparison purposes:
-
-                # Note: relies on min and max and value date portions
-                # always being the same.
-                interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max
-
-                half_interval = wx.TimeSpan(
-                                    0,      # hours
-                                    0,      # minutes
-                                    interval.GetSeconds() / 2,  # seconds
-                                    0)      # msec
-
-                if value < min: # min is on next day, so use value on
-                    # "next day" for "nearest" interval calculation:
-                    cmp_value = value + wx.TimeSpan(24, 0, 0, 0)
-                else:   # "before midnight; ok
-                    cmp_value = value
-
-                if (cmp_value - max) > half_interval:
-                    dbg('forcing value to min (%s)' % min.FormatTime())
-                    self.SetValue(min)
-                else:
-                    dbg('forcing value to max (%s)' % max.FormatTime())
-                    self.SetValue(max)
-            else:
-                dbg('max < min')
-                # therefore  max < value < min guaranteed to be true,
-                # so "nearest bound" calculation is much easier:
-                if (value - max) >= (min - value):
-                    # current value closer to min; pick that edge of pie wedge
-                    dbg('forcing value to min (%s)' % min.FormatTime())
-                    self.SetValue(min)
-                else:
-                    dbg('forcing value to max (%s)' % max.FormatTime())
-                    self.SetValue(max)
-
-        dbg(indent=0)
-
-
-
-    def IsLimited(self):
-        """
-        Returns True if the control is currently limiting the
-        value to fall within any current bounds.  Note: can
-        be set even if there are no current bounds.
-        """
-        return self.__limited
-
-
-    def IsInBounds(self, value=None):
-        """
-        Returns True if no value is specified and the current value
-        of the control falls within the current bounds.  As the clock
-        is a "circle", both minimum and maximum bounds must be set for
-        a value to ever be considered "out of bounds".  This function can
-        also be called with a value to see if that value would fall within
-        the current bounds of the given control.
-        """
-        if value is not None:
-            try:
-                value = self.GetWxDateTime(value)   # try to regularize passed value
-            except ValueError:
-                dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0)
-                raise
-
-        dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1)
-        if self.__min is None or self.__max is None:
-            dbg(indent=0)
-            return True
-
-        elif value is None:
-            try:
-                value = self.GetWxDateTime()
-            except:
-                dbg('exception occurred', indent=0)
-
-        dbg('value:', value.FormatTime())
-
-        # Get wxDateTime representations of bounds:
-        min = self.GetMin()
-        max = self.GetMax()
-
-        midnight = wx.DateTimeFromDMY(1, 0, 1970)
-        if min <= max:   # they don't span midnight
-            ret = min <= value <= max
-
-        else:
-            # have to break into 2 tests; to be in bounds
-            # either "min" <= value (<= midnight of *next day*)
-            # or midnight <= value <= "max"
-            ret = min <= value or (midnight <= value <= max)
-        dbg('in bounds?', ret, indent=0)
-        return ret
-
-
-    def IsValid( self, value ):
-        """
-        Can be used to determine if a given value would be a legal and
-        in-bounds value for the control.
-        """
-        try:
-            self.__validateValue(value)
-            return True
-        except ValueError:
-            return False
-
-    def SetFormat(self, format):
-        self.SetParameters(format=format)
-
-    def GetFormat(self):
-        if self.__displaySeconds:
-            if self.__fmt24hr: return '24HHMMSS'
-            else:              return 'HHMMSS'
-        else:
-            if self.__fmt24hr: return '24HHMM'
-            else:              return 'HHMM'
-
-#-------------------------------------------------------------------------------------------------------------
-# these are private functions and overrides:
-
-
-    def __OnTextChange(self, event=None):
-        dbg('TimeCtrl::OnTextChange', indent=1)
-
-        # Allow Maskedtext base control to color as appropriate,
-        # and Skip the EVT_TEXT event (if appropriate.)
-        ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
-        ## call is generating two (2) EVT_TEXT events. (!)
-        ## The the only mechanism I can find to mask this problem is to
-        ## keep track of last value seen, and declare a valid EVT_TEXT
-        ## event iff the value has actually changed.  The masked edit
-        ## OnTextChange routine does this, and returns True on a valid event,
-        ## False otherwise.
-        if not BaseMaskedTextCtrl._OnTextChange(self, event):
-            return
-
-        dbg('firing TimeUpdatedEvent...')
-        evt = TimeUpdatedEvent(self.GetId(), self.GetValue())
-        evt.SetEventObject(self)
-        self.GetEventHandler().ProcessEvent(evt)
-        dbg(indent=0)
-
-
-    def SetInsertionPoint(self, pos):
-        """
-        Records the specified position and associated cell before calling base class' function.
-        This is necessary to handle the optional spin button, because the insertion
-        point is lost when the focus shifts to the spin button.
-        """
-        dbg('TimeCtrl::SetInsertionPoint', pos, indent=1)
-        BaseMaskedTextCtrl.SetInsertionPoint(self, pos)                 # (causes EVT_TEXT event to fire)
-        self.__posCurrent = self.GetInsertionPoint()
-        dbg(indent=0)
-
-
-    def SetSelection(self, sel_start, sel_to):
-        dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1)
-
-        # Adjust selection range to legal extent if not already
-        if sel_start < 0:
-            sel_start = 0
-
-        if self.__posCurrent != sel_start:                      # force selection and insertion point to match
-            self.SetInsertionPoint(sel_start)
-        cell_start, cell_end = self._FindField(sel_start)._extent
-        if not cell_start <= sel_to <= cell_end:
-            sel_to = cell_end
-
-        self.__bSelection = sel_start != sel_to
-        BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
-        dbg(indent=0)
-
-
-    def __OnSpin(self, key):
-        """
-        This is the function that gets called in response to up/down arrow or
-        bound spin button events.
-        """
-        self.__IncrementValue(key, self.__posCurrent)   # changes the value
-
-        # Ensure adjusted control regains focus and has adjusted portion
-        # selected:
-        self.SetFocus()
-        start, end = self._FindField(self.__posCurrent)._extent
-        self.SetInsertionPoint(start)
-        self.SetSelection(start, end)
-        dbg('current position:', self.__posCurrent)
-
-
-    def __OnSpinUp(self, event):
-        """
-        Event handler for any bound spin button on EVT_SPIN_UP;
-        causes control to behave as if up arrow was pressed.
-        """
-        dbg('TimeCtrl::OnSpinUp', indent=1)
-        self.__OnSpin(wx.WXK_UP)
-        keep_processing = False
-        dbg(indent=0)
-        return keep_processing
-
-
-    def __OnSpinDown(self, event):
-        """
-        Event handler for any bound spin button on EVT_SPIN_DOWN;
-        causes control to behave as if down arrow was pressed.
-        """
-        dbg('TimeCtrl::OnSpinDown', indent=1)
-        self.__OnSpin(wx.WXK_DOWN)
-        keep_processing = False
-        dbg(indent=0)
-        return keep_processing
-
-
-    def __OnChar(self, event):
-        """
-        Handler to explicitly look for ':' keyevents, and if found,
-        clear the m_shiftDown field, so it will behave as forward tab.
-        It then calls the base control's _OnChar routine with the modified
-        event instance.
-        """
-        dbg('TimeCtrl::OnChar', indent=1)
-        keycode = event.GetKeyCode()
-        dbg('keycode:', keycode)
-        if keycode == ord(':'):
-            dbg('colon seen! removing shift attribute')
-            event.m_shiftDown = False
-        BaseMaskedTextCtrl._OnChar(self, event )              ## handle each keypress
-        dbg(indent=0)
-
-
-    def __OnSetToNow(self, event):
-        """
-        This is the key handler for '!' and 'c'; this allows the user to
-        quickly set the value of the control to the current time.
-        """
-        self.SetValue(wx.DateTime_Now().FormatTime())
-        keep_processing = False
-        return keep_processing
-
-
-    def __LimitSelection(self, event):
-        """
-        Event handler for motion events; this handler
-        changes limits the selection to the new cell boundaries.
-        """
-        dbg('TimeCtrl::LimitSelection', indent=1)
-        pos = self.GetInsertionPoint()
-        self.__posCurrent = pos
-        sel_start, sel_to = self.GetSelection()
-        selection = sel_start != sel_to
-        if selection:
-            # only allow selection to end of current cell:
-            start, end = self._FindField(sel_start)._extent
-            if sel_to < pos:   sel_to = start
-            elif sel_to > pos: sel_to = end
-
-        dbg('new pos =', self.__posCurrent, 'select to ', sel_to)
-        self.SetInsertionPoint(self.__posCurrent)
-        self.SetSelection(self.__posCurrent, sel_to)
-        if event: event.Skip()
-        dbg(indent=0)
-
-
-    def __IncrementValue(self, key, pos):
-        dbg('TimeCtrl::IncrementValue', key, pos, indent=1)
-        text = self.GetValue()
-        field = self._FindField(pos)
-        dbg('field: ', field._index)
-        start, end = field._extent
-        slice = text[start:end]
-        if key == wx.WXK_UP: increment = 1
-        else:             increment = -1
-
-        if slice in ('A', 'P'):
-            if slice == 'A': newslice = 'P'
-            elif slice == 'P': newslice = 'A'
-            newvalue = text[:start] + newslice + text[end:]
-
-        elif field._index == 0:
-            # adjusting this field is trickier, as its value can affect the
-            # am/pm setting.  So, we use wxDateTime to generate a new value for us:
-            # (Use a fixed date not subject to DST variations:)
-            converter = wx.DateTimeFromDMY(1, 0, 1970)
-            dbg('text: "%s"' % text)
-            converter.ParseTime(text.strip())
-            currenthour = converter.GetHour()
-            dbg('current hour:', currenthour)
-            newhour = (currenthour + increment) % 24
-            dbg('newhour:', newhour)
-            converter.SetHour(newhour)
-            dbg('converter.GetHour():', converter.GetHour())
-            newvalue = converter     # take advantage of auto-conversion for am/pm in .SetValue()
-
-        else:   # minute or second field; handled the same way:
-            newslice = "%02d" % ((int(slice) + increment) % 60)
-            newvalue = text[:start] + newslice + text[end:]
-
-        try:
-            self.SetValue(newvalue)
-
-        except ValueError:  # must not be in bounds:
-            if not wx.Validator_IsSilent():
-                wx.Bell()
-        dbg(indent=0)
-
-
-    def _toGUI( self, wxdt ):
-        """
-        This function takes a wxdt as an unambiguous representation of a time, and
-        converts it to a string appropriate for the format of the control.
-        """
-        if self.__fmt24hr:
-            if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S')
-            else:                     strval = wxdt.Format('%H:%M')
-        else:
-            if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p')
-            else:                     strval = wxdt.Format('%I:%M %p')
-
-        return strval
-
-
-    def __validateValue( self, value ):
-        """
-        This function converts the value to a wxDateTime if not already one,
-        does bounds checking and raises ValueError if argument is
-        not a valid value for the control as currently specified.
-        It is used by both the SetValue() and the IsValid() methods.
-        """
-        dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1)
-        if not value:
-            dbg(indent=0)
-            raise ValueError('%s not a valid time value' % repr(value))
-
-        valid = True    # assume true
-        try:
-            value = self.GetWxDateTime(value)   # regularize form; can generate ValueError if problem doing so
-        except:
-            dbg('exception occurred', indent=0)
-            raise
-
-        if self.IsLimited() and not self.IsInBounds(value):
-            dbg(indent=0)
-            raise ValueError (
-                'value %s is not within the bounds of the control' % str(value) )
-        dbg(indent=0)
-        return value
-
-#----------------------------------------------------------------------------
-# Test jig for TimeCtrl:
-
-if __name__ == '__main__':
-    import traceback
-
-    class TestPanel(wx.Panel):
-        def __init__(self, parent, id,
-                     pos = wx.DefaultPosition, size = wx.DefaultSize,
-                     fmt24hr = 0, test_mx = 0,
-                     style = wx.TAB_TRAVERSAL ):
-
-            wx.Panel.__init__(self, parent, id, pos, size, style)
-
-            self.test_mx = test_mx
-
-            self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr)
-            sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 )
-            self.tc.BindSpinButton(sb)
-
-            sizer = wx.BoxSizer( wx.HORIZONTAL )
-            sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 )
-            sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 )
-
-            self.SetAutoLayout( True )
-            self.SetSizer( sizer )
-            sizer.Fit( self )
-            sizer.SetSizeHints( self )
-
-            self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc)
-
-        def OnTimeChange(self, event):
-            dbg('OnTimeChange: value = ', event.GetValue())
-            wxdt = self.tc.GetWxDateTime()
-            dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
-            if self.test_mx:
-                mxdt = self.tc.GetMxDateTime()
-                dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
-
-
-    class MyApp(wx.App):
-        def OnInit(self):
-            import sys
-            fmt24hr = '24' in sys.argv
-            test_mx = 'mx' in sys.argv
-            try:
-                frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
-                panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
-                frame.Show(True)
-            except:
-                traceback.print_exc()
-                return False
-            return True
-
-    try:
-        app = MyApp(0)
-        app.MainLoop()
-    except:
-        traceback.print_exc()
-i=0
-## Version 1.2
-##   1. Changed parameter name display_seconds to displaySeconds, to follow
-##      other masked edit conventions.
-##   2. Added format parameter, to remove need to use both fmt24hr and displaySeconds.
-##   3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of
-##      nonsensical parameter methods from the control, so it will work
-##      properly with Boa.
index c022bf40e399900d4d8fe9ff161afa3a642b0218..197cd9db65f3df1f28d3a14258d4a6b3f0873d41 100644 (file)
@@ -2,9 +2,21 @@
 ## backwards compatibility.  Some names will also have a 'wx' added on if
 ## that is how they used to be named in the old wxPython package.
 
-import wx.lib.maskedctrl
+import wx.lib.masked.ctrl
 
-__doc__ =  wx.lib.maskedctrl.__doc__
+__doc__ =  wx.lib.masked.ctrl.__doc__
 
-controlTypes = wx.lib.maskedctrl.controlTypes
-wxMaskedCtrl = wx.lib.maskedctrl.MaskedCtrl
+MASKEDTEXT  = wx.lib.masked.ctrl.TEXT
+MASKEDCOMBO = wx.lib.masked.ctrl.COMBO
+IPADDR      = wx.lib.masked.ctrl.IPADDR
+TIME        = wx.lib.masked.ctrl.TIME
+NUMBER      = wx.lib.masked.ctrl.NUMBER
+
+class controlTypes:
+    MASKEDTEXT  = wx.lib.masked.ctrl.TEXT
+    MASKEDCOMBO = wx.lib.masked.ctrl.COMBO
+    IPADDR      = wx.lib.masked.ctrl.IPADDR
+    TIME        = wx.lib.masked.ctrl.TIME
+    NUMBER      = wx.lib.masked.ctrl.NUMBER
+
+wxMaskedCtrl = wx.lib.masked.Ctrl
index 42e4331cd570dac89e962956e3433c75765b3f30..7c4da9d1c10f81d77006506c27878ea3dc5b7fc5 100644 (file)
@@ -1,16 +1,15 @@
 ## This file imports items from the wx package into the wxPython package for
 ## backwards compatibility.  Some names will also have a 'wx' added on if
 ## that is how they used to be named in the old wxPython package.
+import wx.lib.masked
+import wx.lib.masked.maskededit
 
-import wx.lib.maskededit
+__doc__ =  wx.lib.masked.maskededit.__doc__
 
-__doc__ =  wx.lib.maskededit.__doc__
+from wx.lib.masked.maskededit import *
 
-Field = wx.lib.maskededit.Field
-test = wx.lib.maskededit.test
-test2 = wx.lib.maskededit.test2
-wxIpAddrCtrl = wx.lib.maskededit.IpAddrCtrl
-wxMaskedComboBox = wx.lib.maskededit.MaskedComboBox
-wxMaskedComboBoxSelectEvent = wx.lib.maskededit.MaskedComboBoxSelectEvent
-wxMaskedEditMixin = wx.lib.maskededit.MaskedEditMixin
-wxMaskedTextCtrl = wx.lib.maskededit.MaskedTextCtrl
+wxMaskedEditMixin = wx.lib.masked.MaskedEditMixin
+wxMaskedTextCtrl = wx.lib.masked.TextCtrl
+wxMaskedComboBox = wx.lib.masked.ComboBox
+wxMaskedComboBoxSelectEvent = wx.lib.masked.MaskedComboBoxSelectEvent
+wxIpAddrCtrl = wx.lib.masked.IpAddrCtrl
index 99e09bc188209251b340e0176bbdf7dc8ba60d48..69c9032e62783f9e2f4846c7819fae609564ec18 100644 (file)
@@ -2,10 +2,10 @@
 ## backwards compatibility.  Some names will also have a 'wx' added on if
 ## that is how they used to be named in the old wxPython package.
 
-import wx.lib.maskednumctrl
+import wx.lib.masked.numctrl
 
-__doc__ =  wx.lib.maskednumctrl.__doc__
+__doc__ =  wx.lib.masked.numctrl.__doc__
 
-EVT_MASKEDNUM = wx.lib.maskednumctrl.EVT_MASKEDNUM
-wxMaskedNumCtrl = wx.lib.maskednumctrl.MaskedNumCtrl
-wxMaskedNumNumberUpdatedEvent = wx.lib.maskednumctrl.MaskedNumNumberUpdatedEvent
+EVT_MASKEDNUM = wx.lib.masked.numctrl.EVT_NUM
+wxMaskedNumCtrl = wx.lib.masked.numctrl.NumCtrl
+wxMaskedNumNumberUpdatedEvent = wx.lib.masked.numctrl.NumberUpdatedEvent
index da46bcf97a600516524f0d4bccf9c009d04f4ecd..d71c4cebd66839185097deba5d8fb2fbd130ec68 100644 (file)
@@ -2,10 +2,10 @@
 ## backwards compatibility.  Some names will also have a 'wx' added on if
 ## that is how they used to be named in the old wxPython package.
 
-import wx.lib.timectrl
+import wx.lib.masked.timectrl
 
-__doc__ =  wx.lib.timectrl.__doc__
+__doc__ =  wx.lib.masked.timectrl.__doc__
 
-EVT_TIMEUPDATE = wx.lib.timectrl.EVT_TIMEUPDATE
-TimeUpdatedEvent = wx.lib.timectrl.TimeUpdatedEvent
-wxTimeCtrl = wx.lib.timectrl.TimeCtrl
+EVT_TIMEUPDATE = wx.lib.masked.timectrl.EVT_TIMEUPDATE
+TimeUpdatedEvent = wx.lib.masked.timectrl.TimeUpdatedEvent
+wxTimeCtrl = wx.lib.masked.timectrl.TimeCtrl