diff --git a/wxPython/CHANGES.txt b/wxPython/CHANGES.txt
index 391bf0986a..707b28b44f 100644
--- a/wxPython/CHANGES.txt
+++ b/wxPython/CHANGES.txt
@@ -46,6 +46,10 @@ Toolbars on wxMac can now have controls on them.
Added wxPython.lib.analogclock module based on samples that were
passed back and forth on wxPython-users a while back.
+Added masked edit controls (wxPython.lib.maskededit) by Jeff Childers
+and Will Sadkin. Updated wxTimeCtrl to use MaskedEdit.
+
+
diff --git a/wxPython/demo/Main.py b/wxPython/demo/Main.py
index 2c3d1e20eb..941b18093a 100644
--- a/wxPython/demo/Main.py
+++ b/wxPython/demo/Main.py
@@ -31,6 +31,7 @@ _treeList = [
'NewNamespace',
'PopupMenu',
'AnalogClockWindow',
+ 'MaskedEditControls',
]),
# managed windows == things with a (optional) caption you can close
@@ -111,6 +112,7 @@ _treeList = [
'FancyText',
'FileBrowseButton',
'GenericButtons',
+ 'MaskedEditControls',
'PyShell',
'PyCrust',
'SplitTree',
diff --git a/wxPython/demo/MaskedEditControls.py b/wxPython/demo/MaskedEditControls.py
new file mode 100644
index 0000000000..30de96215c
--- /dev/null
+++ b/wxPython/demo/MaskedEditControls.py
@@ -0,0 +1,527 @@
+from wxPython.wx import *
+from wxPython.lib.maskededit import Field, wxMaskedTextCtrl, wxMaskedComboBox, wxIpAddrCtrl, states, months
+from wxPython.lib.maskededit import __doc__ as overviewdoc
+from wxPython.lib.scrolledpanel import wxScrolledPanel
+import string
+
+class demoMixin:
+ """
+ Centralized routines common to demo pages, to remove repetition.
+ """
+ def labelGeneralTable(self, sizer):
+ description = wxStaticText( self, -1, "Description", )
+ mask = wxStaticText( self, -1, "Mask Value" )
+ formatcode = wxStaticText( self, -1, "Format" )
+ regex = wxStaticText( self, -1, "Regexp\nValidator(opt.)" )
+ ctrl = wxStaticText( self, -1, "wxMaskedEdit Ctrl" )
+
+ description.SetFont( wxFont(9, wxSWISS, wxNORMAL, wxBOLD))
+ mask.SetFont( wxFont(9, wxSWISS, wxNORMAL, wxBOLD))
+ formatcode.SetFont( wxFont(9, wxSWISS, wxNORMAL, wxBOLD) )
+ regex.SetFont( wxFont(9, wxSWISS, wxNORMAL, wxBOLD))
+ ctrl.SetFont( wxFont(9, wxSWISS, wxNORMAL, wxBOLD))
+
+ sizer.Add(description)
+ sizer.Add(mask)
+ sizer.Add(formatcode)
+ sizer.Add(regex)
+ sizer.Add(ctrl)
+
+
+ def layoutGeneralTable(self, controls, sizer):
+ for control in controls:
+ sizer.Add( wxStaticText( self, -1, control[0]) )
+ sizer.Add( wxStaticText( self, -1, control[1]) )
+ sizer.Add( wxStaticText( self, -1, control[3]) )
+ sizer.Add( wxStaticText( self, -1, control[4]) )
+
+ if control in controls:
+ newControl = wxMaskedTextCtrl( self, -1, "",
+ mask = control[1],
+ excludeChars = control[2],
+ formatcodes = control[3],
+ includeChars = "",
+ validRegex = control[4],
+ validRange = control[5],
+ choices = control[6],
+ choiceRequired = True,
+ defaultValue = control[7],
+ demo = True,
+ name = control[0])
+ self.editList.append(newControl)
+ sizer.Add(newControl)
+
+
+ def changeControlParams(self, event, parameter, checked_value, notchecked_value):
+ if event.Checked(): value = checked_value
+ else: value = notchecked_value
+ kwargs = {parameter: value}
+ for control in self.editList:
+ control.SetMaskParameters(**kwargs)
+ control.Refresh()
+ self.Refresh()
+
+
+
+#----------------------------------------------------------------------------
+class demoPage1(wxScrolledPanel, demoMixin):
+ def __init__(self, parent, log):
+ wxScrolledPanel.__init__(self, parent, -1)
+ self.sizer = wxBoxSizer( wxVERTICAL )
+ self.editList = []
+
+ label = wxStaticText( self, -1, """\
+Here are some basic wxMaskedTextCtrls 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 code.
+
+Try entering nonsensical or partial values in validated fields to see what
+happens. Note that the State and Last Name fields are list-limited (valid
+last names are: Smith, Jones, Williams). Signs on numbers can be toggled
+with the minus key.
+""")
+ label.SetForegroundColour( "Blue" )
+ header = wxBoxSizer( wxHORIZONTAL )
+ header.Add( label, 0, flag=wxALIGN_LEFT|wxALL, border = 5 )
+
+ highlight = wxCheckBox( self, -1, "Highlight Empty" )
+ disallow = wxCheckBox( self, -1, "Disallow Empty" )
+ showFill = wxCheckBox( self, -1, "change fillChar" )
+
+ vbox = wxBoxSizer( wxVERTICAL )
+ vbox.Add( highlight, 0, wxALIGN_LEFT|wxALL, 5 )
+ vbox.Add( disallow, 0, wxALIGN_LEFT|wxALL, 5 )
+ vbox.Add( showFill, 0, wxALIGN_LEFT|wxALL, 5 )
+ header.AddSpacer(15, 0)
+ header.Add(vbox, 0, flag=wxALIGN_LEFT|wxALL, border=5 )
+
+ EVT_CHECKBOX( self, highlight.GetId(), self.onHighlightEmpty )
+ EVT_CHECKBOX( self, disallow.GetId(), self.onDisallowEmpty )
+ EVT_CHECKBOX( self, showFill.GetId(), self.onShowFill )
+
+ grid = wxFlexGridSizer( 0, 5, vgap=10, hgap=10 )
+ self.labelGeneralTable(grid)
+
+ # 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^-', "^\(\d{3}\) \d{3}-\d{4}", '','',''),
+ ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", '','',''),
+ ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', '','',''),
+ ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', '',('Smith','Jones','Williams'),''),
+ ("Zip plus 4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})", '','',''),
+ ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", '','',''),
+ ("Invoice Total", "#{9}.##", "", 'F-_,', "", '','',''),
+ ]
+
+ self.layoutGeneralTable(controls, grid)
+ self.sizer.Add( header, 0, flag=wxALIGN_LEFT|wxALL, border=5 )
+ self.sizer.Add( grid, 0, flag= wxALIGN_LEFT|wxLEFT, border=5 )
+ self.SetSizer(self.sizer)
+ self.SetupScrolling()
+ self.SetAutoLayout(1)
+
+
+ def onDisallowEmpty( self, event ):
+ """ Set emptyInvalid parameter on/off """
+ self.changeControlParams( event, "emptyInvalid", True, False )
+
+ def onHighlightEmpty( self, event ):
+ """ Highlight empty values"""
+ self.changeControlParams( event, "emptyBackgroundColor", "Blue", "White" )
+
+ def onShowFill( self, event ):
+ """ Set fillChar parameter to '?' or ' ' """
+ self.changeControlParams( event, "fillChar", '?', ' ' )
+
+
+class demoPage2(wxScrolledPanel, demoMixin):
+ def __init__(self, parent, log):
+ self.log = log
+ wxScrolledPanel.__init__(self, parent, -1)
+ self.sizer = wxBoxSizer( wxVERTICAL )
+ self.editList = []
+
+ label = wxStaticText( self, -1, """\
+Here wxMaskedTextCtrls that have default values. The states
+control has a list of valid values, and the unsigned integer
+has a legal range specified.
+""")
+ label.SetForegroundColour( "Blue" )
+ requireValid = wxCheckBox( self, -1, "Require Valid Value" )
+ EVT_CHECKBOX( self, requireValid.GetId(), self.onRequireValid )
+
+ header = wxBoxSizer( wxHORIZONTAL )
+ header.Add( label, 0, flag=wxALIGN_LEFT|wxALL, border = 5)
+ header.AddSpacer(75, 0)
+ header.Add( requireValid, 0, flag=wxALIGN_LEFT|wxALL, border=10 )
+
+ grid = wxFlexGridSizer( 0, 5, vgap=10, hgap=10 )
+ self.labelGeneralTable( grid )
+
+ controls = [
+ #description mask excl format regexp range,list,initial
+ ("U.S. State (2 char)", "AA", "", 'F!_', "[A-Z]{2}", '',states, states[0]),
+ ("Integer (signed)", "#{6}", "", 'F-_R', "", '','', '0 '),
+ ("Integer (unsigned)\n(1-399)","######", "", 'F_', "", (1,399),'', '1 '),
+ ("Float (signed)", "#{6}.#{9}", "", 'F-_R', "", '','', '000000.000000000'),
+ ("Date (MDY) + Time", "##/##/#### ##:##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DF!',"", '','', wxDateTime_Now().Format("%m/%d/%Y %I:%M:%S %p")),
+ ]
+ self.layoutGeneralTable( controls, grid )
+
+ self.sizer.Add( header, 0, flag=wxALIGN_LEFT|wxALL, border=5 )
+ self.sizer.Add( grid, 0, flag=wxALIGN_LEFT|wxALL, border=5 )
+
+ self.SetSizer( self.sizer )
+ self.SetAutoLayout( 1 )
+ self.SetupScrolling()
+
+ def onRequireValid( self, event ):
+ """ Set validRequired parameter on/off """
+ self.changeControlParams( event, "validRequired", True, False )
+
+
+
+class demoPage3( wxScrolledPanel, demoMixin ):
+ def __init__( self, parent, log ):
+ self.log = log
+ wxScrolledPanel.__init__( self, parent, -1 )
+ self.sizer = wxBoxSizer( wxVERTICAL )
+
+ label = wxStaticText( self, -1, """\
+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.""")
+
+ label.SetForegroundColour( "Blue" )
+ self.sizer.Add( label, 0, wxALIGN_LEFT|wxALL, 5 )
+
+ description = wxStaticText( self, -1, "Description")
+ autofmt = wxStaticText( self, -1, "AutoFormat Code")
+ ctrl = wxStaticText( self, -1, "wxMaskedEdit Control")
+
+ description.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+ autofmt.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+ ctrl.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+
+ grid = wxFlexGridSizer( 0, 3, vgap=10, hgap=5 )
+ grid.Add( description, 0, wxALIGN_LEFT )
+ grid.Add( autofmt, 0, wxALIGN_LEFT )
+ grid.Add( ctrl, 0, wxALIGN_LEFT )
+
+ controls = [
+ #description autoformat code
+ ("Phone No w/opt. ext","USPHONEFULLEXT"),
+ ("Phone No only", "USPHONEFULL"),
+ ("US Date + Time","USDATETIMEMMDDYYYY/HHMMSS"),
+ ("US Date + Time\n(without seconds)","USDATETIMEMMDDYYYY/HHMM"),
+ ("US Date + Military Time","USDATEMILTIMEMMDDYYYY/HHMMSS"),
+ ("US Date + Military Time\n(without seconds)","USDATEMILTIMEMMDDYYYY/HHMM"),
+ ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
+ ("Time", "TIMEHHMMSS"),
+ ("Time\n(without seconds)", "TIMEHHMM"),
+ ("Military Time", "MILTIMEHHMMSS"),
+ ("Military Time\n(without seconds)", "MILTIMEHHMM"),
+ ("European Date", "EUDATEYYYYMMDD/"),
+ ("Social Sec#","USSOCIALSEC"),
+ ("Credit Card","CREDITCARD"),
+ ("Expiration MM/YY","EXPDATEMMYY"),
+ ("Percentage","PERCENT"),
+ ("Person's Age","AGE"),
+ ("US State", "USSTATE"),
+ ("US Zip Code","USZIP"),
+ ("US Zip+4","USZIPPLUS4"),
+ ("Age", "AGE"),
+ ("Email Address","EMAIL"),
+ ]
+ for control in controls:
+ grid.Add( wxStaticText( self, -1, control[0]), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, control[1]), 0, wxALIGN_LEFT )
+ grid.Add( wxMaskedTextCtrl( self, -1, "",
+ autoformat = control[1],
+ demo = True,
+ name = control[1]),
+ 0, wxALIGN_LEFT )
+
+ self.sizer.Add( grid, 0, wxALIGN_LEFT|wxALL, border=5 )
+ self.SetSizer( self.sizer )
+ self.SetAutoLayout( 1 )
+ self.SetupScrolling()
+
+
+class demoPage4( wxScrolledPanel, demoMixin ):
+ def __init__( self, parent, log ):
+ self.log = log
+ wxScrolledPanel.__init__( self, parent, -1 )
+ self.sizer = wxBoxSizer( wxVERTICAL )
+
+ label = wxStaticText( self, -1, """\
+These controls have field-specific choice lists and allow autocompletion.
+
+Down arrow or Page Down in an uncompleted field with an auto-completable field will attempt
+to auto-complete a field if it has a choice list.
+Page Down and Shift-Down arrow will also auto-complete, or cycle through the complete list.
+Page Up and Shift-Up arrow will similarly cycle backwards through the list.
+""")
+
+ label.SetForegroundColour( "Blue" )
+ self.sizer.Add( label, 0, wxALIGN_LEFT|wxALL, 5 )
+
+ description = wxStaticText( self, -1, "Description" )
+ autofmt = wxStaticText( self, -1, "AutoFormat Code" )
+ fields = wxStaticText( self, -1, "Field Objects" )
+ ctrl = wxStaticText( self, -1, "wxMaskedEdit Control" )
+
+ description.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+ autofmt.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+ fields.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+ ctrl.SetFont( wxFont( 9, wxSWISS, wxNORMAL, wxBOLD ) )
+
+ grid = wxFlexGridSizer( 0, 4, vgap=10, hgap=10 )
+ grid.Add( description, 0, wxALIGN_LEFT )
+ grid.Add( autofmt, 0, wxALIGN_LEFT )
+ grid.Add( fields, 0, wxALIGN_LEFT )
+ grid.Add( ctrl, 0, wxALIGN_LEFT )
+
+ autoformat = "USPHONEFULLEXT"
+ fieldsDict = {0: Field(choices=["617","781","508","978","413"], choiceRequired=True)}
+ fieldsLabel = """\
+{0: Field(choices=[
+ "617","781",
+ "508","978","413"],
+ choiceRequired=True)}"""
+ grid.Add( wxStaticText( self, -1, "Restricted Area Code"), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, autoformat), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, fieldsLabel), 0, wxALIGN_LEFT )
+ grid.Add( wxMaskedTextCtrl( self, -1, "",
+ autoformat = autoformat,
+ fields = fieldsDict,
+ demo = True,
+ name = autoformat),
+ 0, wxALIGN_LEFT )
+
+ autoformat = "EXPDATEMMYY"
+ fieldsDict = {1: Field(choices=["03", "04", "05"], choiceRequired=True)}
+ fieldsLabel = """\
+{1: Field(choices=[
+ "03", "04", "05"],
+ choiceRequired=True)}"""
+ exp = wxMaskedTextCtrl( self, -1, "",
+ autoformat = autoformat,
+ fields = fieldsDict,
+ demo = True,
+ name = autoformat)
+
+ grid.Add( wxStaticText( self, -1, "Restricted Expiration"), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, autoformat), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, fieldsLabel), 0, wxALIGN_LEFT )
+ grid.Add( exp, 0, wxALIGN_LEFT )
+
+ fieldsDict = {0: Field(choices=["02134","02155"], choiceRequired=True),
+ 1: Field(choices=["1234", "5678"], choiceRequired=False)}
+ fieldsLabel = """\
+{0: Field(choices=["02134","02155"],
+ choiceRequired=True),
+ 1: Field(choices=["1234", "5678"],
+ choiceRequired=False)}"""
+ autoformat = "USZIPPLUS4"
+ zip = wxMaskedTextCtrl( self, -1, "",
+ autoformat = autoformat,
+ fields = fieldsDict,
+ demo = True,
+ name = autoformat)
+
+ grid.Add( wxStaticText( self, -1, "Restricted Zip + 4"), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, autoformat), 0, wxALIGN_LEFT )
+ grid.Add( wxStaticText( self, -1, fieldsLabel), 0, wxALIGN_LEFT )
+ grid.Add( zip, 0, wxALIGN_LEFT )
+
+ self.sizer.Add( grid, 0, wxALIGN_LEFT|wxALL, border=5 )
+ self.SetSizer( self.sizer )
+ self.SetAutoLayout(1)
+ self.SetupScrolling()
+
+
+class demoPage5( wxScrolledPanel, demoMixin ):
+ def __init__( self, parent, log ):
+ self.log = log
+ wxScrolledPanel.__init__( self, parent, -1 )
+ self.sizer = wxBoxSizer( wxVERTICAL )
+ label = wxStaticText( self, -1, """\
+These are examples of wxMaskedComboBox and wxIpAddrCtrl, and a more
+useful configuration of a wxMaskedTextCtrl for floating point input.
+""")
+ label.SetForegroundColour( "Blue" )
+ self.sizer.Add( label, 0, wxALIGN_LEFT|wxALL, 5 )
+
+ 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: Field(choices=numerators, choiceRequired=False),
+ 1: Field(choices=denominators, choiceRequired=True)}
+ choices = []
+ for n in numerators:
+ for d in denominators:
+ if n != d:
+ choices.append( '%s/%s' % (n,d) )
+
+
+ text1 = wxStaticText( self, -1, """\
+A masked ComboBox for fraction selection.
+Choices for each side of the fraction can be
+selected with PageUp/Down:""")
+
+ fraction = wxMaskedComboBox( self, -1, "",
+ choices = choices,
+ choiceRequired = True,
+ mask = "#/##",
+ formatcodes = "F_",
+ validRegex = "^\d\/\d\d?",
+ fields = fieldsDict )
+
+
+ text2 = wxStaticText( self, -1, """
+A masked ComboBox to validate
+text from a list of numeric codes:""")
+
+ choices = ["91", "136", "305", "4579"]
+ code = wxMaskedComboBox( self, -1, choices[0],
+ choices = choices,
+ choiceRequired = True,
+ formatcodes = "F_r",
+ mask = "####")
+
+
+ text3 = wxStaticText( self, -1, """\
+A masked state selector; only "legal" values
+can be entered:""")
+
+ state = wxMaskedComboBox( self, -1, states[0],
+ choices = states,
+ autoformat="USSTATE")
+
+ text4 = wxStaticText( self, -1, "An empty IP Address entry control:")
+ ip_addr1 = wxIpAddrCtrl( self, -1, style = wxTE_PROCESS_TAB )
+
+
+ text5 = wxStaticText( self, -1, "An IP Address control with a restricted mask:")
+ ip_addr2 = wxIpAddrCtrl( self, -1, mask=" 10. 1.109.###" )
+
+
+ text6 = wxStaticText( self, -1, """\
+An IP Address control with restricted choices
+of form: 10. (1|2) . (129..255) . (0..255)""")
+ ip_addr3 = wxIpAddrCtrl( self, -1, mask=" 10. #.###.###")
+ ip_addr3.SetFieldParameters(0, validRegex="1|2" ) # requires entry to match or not allowed
+
+
+ # This allows any value in penultimate field, but colors anything outside of the range invalid:
+ ip_addr3.SetFieldParameters(1, validRange=(129,255), validRequired=False )
+
+ text7 = wxStaticText( self, -1, """\
+A floating point entry control
+with right-insert for ordinal:""")
+ floatctrl = wxMaskedTextCtrl(self, -1, mask="#{9}.#{2}", formatcodes="F_-R")
+ floatctrl.SetFieldParameters(0, formatcodes='r,<', validRequired=True) # right-insert, allow commas, require explicit cursor movement to change fields
+ floatctrl.SetFieldParameters(1, defaultValue='00') # don't allow blank fraction
+
+ grid = wxFlexGridSizer( 0, 2, vgap=20, hgap = 5 )
+ grid.Add( text1, 0, wxALIGN_LEFT )
+ grid.Add( fraction, 0, wxALIGN_LEFT )
+ grid.Add( text2, 0, wxALIGN_LEFT )
+ grid.Add( code, 0, wxALIGN_LEFT )
+ grid.Add( text3, 0, wxALIGN_LEFT )
+ grid.Add( state, 0, wxALIGN_LEFT )
+ grid.Add( text4, 0, wxALIGN_LEFT )
+ grid.Add( ip_addr1, 0, wxALIGN_LEFT )
+ grid.Add( text5, 0, wxALIGN_LEFT )
+ grid.Add( ip_addr2, 0, wxALIGN_LEFT )
+ grid.Add( text6, 0, wxALIGN_LEFT )
+ grid.Add( ip_addr3, 0, wxALIGN_LEFT )
+ grid.Add( text7, 0, wxALIGN_LEFT )
+ grid.Add( floatctrl, 0, wxALIGN_LEFT )
+
+ self.sizer.Add( grid, 0, wxALIGN_LEFT|wxALL, border=5 )
+ self.SetSizer( self.sizer )
+ self.SetAutoLayout(1)
+ self.SetupScrolling()
+
+ EVT_COMBOBOX( self, fraction.GetId(), self.OnComboChange )
+ EVT_COMBOBOX( self, code.GetId(), self.OnComboChange )
+ EVT_COMBOBOX( self, state.GetId(), self.OnComboChange )
+ EVT_TEXT( self, fraction.GetId(), self.OnComboChange )
+ EVT_TEXT( self, code.GetId(), self.OnComboChange )
+ EVT_TEXT( self, state.GetId(), self.OnComboChange )
+
+ EVT_TEXT( self, ip_addr1.GetId(), self.OnIpAddrChange )
+ EVT_TEXT( self, ip_addr2.GetId(), self.OnIpAddrChange )
+ EVT_TEXT( self, ip_addr3.GetId(), self.OnIpAddrChange )
+ EVT_TEXT( self, floatctrl.GetId(), self.OnTextChange )
+
+
+ def OnComboChange( self, event ):
+ ctl = self.FindWindowById( event.GetId() )
+ if not ctl.IsValid():
+ self.log.write('current value not a valid choice')
+
+
+ def OnIpAddrChange( self, event ):
+ ip_addr = self.FindWindowById( event.GetId() )
+ if ip_addr.IsValid():
+ self.log.write('new addr = %s\n' % ip_addr.GetAddress() )
+
+ def OnTextChange( self, event ):
+ ctl = self.FindWindowById( event.GetId() )
+ if ctl.IsValid():
+ self.log.write('new value = %s\n' % ctl.GetValue() )
+
+# ---------------------------------------------------------------------
+class TestMaskedTextCtrls(wxNotebook):
+ def __init__(self, parent, id, log):
+ wxNotebook.__init__(self, parent, id)
+ self.log = log
+
+ win = demoPage1(self, log)
+ self.AddPage(win, "General examples")
+
+ win = demoPage2(self, log)
+ self.AddPage(win, "Using default values")
+
+ win = demoPage3(self, log)
+ self.AddPage(win, 'Auto-formatted controls')
+
+ win = demoPage4(self, log)
+ self.AddPage(win, 'Using auto-complete fields')
+
+ win = demoPage5(self, log)
+ self.AddPage(win, 'Other masked controls')
+
+
+#----------------------------------------------------------------------------
+
+def runTest(frame, nb, log):
+ testWin = TestMaskedTextCtrls(nb, -1, log)
+ return testWin
+
+def RunStandalone():
+ app = wxPySimpleApp()
+ frame = wxFrame(None, -1, "Test wxMaskedTextCtrl", size=(640, 480))
+ win = TestMaskedTextCtrls(frame, -1, sys.stdout)
+ frame.Show(True)
+ app.MainLoop()
+#----------------------------------------------------------------------------
+if __name__ == "__main__":
+ RunStandalone()
+
+
+overview = """
+
+""" + overviewdoc + """
+
+"""
+
+if __name__ == "__main__":
+ import sys,os
+ import run
+ run.main(['', os.path.basename(sys.argv[0])])
diff --git a/wxPython/demo/wxTimeCtrl.py b/wxPython/demo/wxTimeCtrl.py
index 09465642dd..e6a5aeed2f 100644
--- a/wxPython/demo/wxTimeCtrl.py
+++ b/wxPython/demo/wxTimeCtrl.py
@@ -1,68 +1,114 @@
from wxPython.wx import *
from wxPython.lib.timectrl import *
+from wxPython.lib.timectrl import __doc__ as overviewdoc
+from wxPython.lib.scrolledpanel import wxScrolledPanel
#----------------------------------------------------------------------
-class TestPanel( wxPanel ):
+class TestPanel( wxScrolledPanel ):
def __init__( self, parent, log ):
- wxPanel.__init__( self, parent, -1 )
+ wxScrolledPanel.__init__( self, parent, -1 )
self.log = log
- panel = wxPanel( self, -1 )
- grid = wxFlexGridSizer( 0, 2, 20, 0 )
- text1 = wxStaticText( panel, 10, "A 12-hour format wxTimeCtrl:")
- self.time12 = wxTimeCtrl( panel, 20, name="12 hour control" )
- spin1 = wxSpinButton( panel, 30, wxDefaultPosition, wxSize(-1,20), 0 )
+ text1 = wxStaticText( self, -1, "12-hour format:")
+ self.time12 = wxTimeCtrl( self, -1, name="12 hour control" )
+ spin1 = wxSpinButton( self, -1, wxDefaultPosition, wxSize(-1,20), 0 )
self.time12.BindSpinButton( spin1 )
- grid.AddWindow( text1, 0, wxALIGN_RIGHT, 5 )
+ text2 = wxStaticText( self, -1, "24-hour format:")
+ spin2 = wxSpinButton( self, -1, wxDefaultPosition, wxSize(-1,20), 0 )
+ self.time24 = wxTimeCtrl( self, -1, name="24 hour control", fmt24hr=True, spinButton = spin2 )
+
+ text3 = wxStaticText( self, -1, "No seconds\nor spin button:")
+ self.spinless_ctrl = wxTimeCtrl( self, -1, name="spinless control", display_seconds = False )
+
+ grid = wxFlexGridSizer( 0, 2, 10, 5 )
+ grid.Add( text1, 0, wxALIGN_RIGHT )
hbox1 = wxBoxSizer( wxHORIZONTAL )
- hbox1.AddWindow( self.time12, 0, wxALIGN_CENTRE, 5 )
- hbox1.AddWindow( spin1, 0, wxALIGN_CENTRE, 5 )
- grid.AddSizer( hbox1, 0, wxLEFT, 5 )
+ hbox1.Add( self.time12, 0, wxALIGN_CENTRE )
+ hbox1.Add( spin1, 0, wxALIGN_CENTRE )
+ grid.Add( hbox1, 0, wxLEFT )
-
- text2 = wxStaticText( panel, 40, "A 24-hour format wxTimeCtrl:")
- self.time24 = wxTimeCtrl( panel, 50, fmt24hr=True, name="24 hour control" )
- spin2 = wxSpinButton( panel, 60, wxDefaultPosition, wxSize(-1,20), 0 )
- self.time24.BindSpinButton( spin2 )
-
- grid.AddWindow( text2, 0, wxALIGN_RIGHT|wxTOP|wxBOTTOM, 5 )
+ grid.Add( text2, 0, wxALIGN_RIGHT|wxTOP|wxBOTTOM )
hbox2 = wxBoxSizer( wxHORIZONTAL )
- hbox2.AddWindow( self.time24, 0, wxALIGN_CENTRE, 5 )
- hbox2.AddWindow( spin2, 0, wxALIGN_CENTRE, 5 )
- grid.AddSizer( hbox2, 0, wxLEFT, 5 )
+ hbox2.Add( self.time24, 0, wxALIGN_CENTRE )
+ hbox2.Add( spin2, 0, wxALIGN_CENTRE )
+ grid.Add( hbox2, 0, wxLEFT )
+
+ grid.Add( text3, 0, wxALIGN_RIGHT|wxTOP|wxBOTTOM )
+ grid.Add( self.spinless_ctrl, 0, wxLEFT )
- text3 = wxStaticText( panel, 70, "A wxTimeCtrl without a spin button:")
- self.spinless_ctrl = wxTimeCtrl( panel, 80, name="spinless control" )
-
- grid.AddWindow( text3, 0, wxALIGN_RIGHT|wxTOP|wxBOTTOM, 5 )
- grid.AddWindow( self.spinless_ctrl, 0, wxLEFT, 5 )
-
-
- buttonChange = wxButton( panel, 100, "Change Controls")
- self.radio12to24 = wxRadioButton( panel, 110, "Copy 12-hour time to 24-hour control", wxDefaultPosition, wxDefaultSize, wxRB_GROUP )
- self.radio24to12 = wxRadioButton( panel, 120, "Copy 24-hour time to 12-hour control")
- self.radioWx = wxRadioButton( panel, 130, "Set controls to 'now' using wxDateTime")
- self.radioMx = wxRadioButton( panel, 140, "Set controls to 'now' using mxDateTime")
+ buttonChange = wxButton( self, -1, "Change Controls")
+ self.radio12to24 = wxRadioButton( self, -1, "Copy 12-hour time to 24-hour control", wxDefaultPosition, wxDefaultSize, wxRB_GROUP )
+ self.radio24to12 = wxRadioButton( self, -1, "Copy 24-hour time to 12-hour control")
+ self.radioWx = wxRadioButton( self, -1, "Set controls to 'now' using wxDateTime")
+ self.radioMx = wxRadioButton( self, -1, "Set controls to 'now' using mxDateTime")
radio_vbox = wxBoxSizer( wxVERTICAL )
- radio_vbox.AddWindow( self.radio12to24, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
- radio_vbox.AddWindow( self.radio24to12, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
- radio_vbox.AddWindow( self.radioWx, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
- radio_vbox.AddWindow( self.radioMx, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
+ radio_vbox.Add( self.radio12to24, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
+ radio_vbox.Add( self.radio24to12, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
+ radio_vbox.Add( self.radioWx, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
+ radio_vbox.Add( self.radioMx, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5 )
- box_label = wxStaticBox( panel, 90, "Change Controls through API" )
+ box_label = wxStaticBox( self, -1, "Change Controls through API" )
buttonbox = wxStaticBoxSizer( box_label, wxHORIZONTAL )
- buttonbox.AddWindow( buttonChange, 0, wxALIGN_CENTRE|wxALL, 5 )
- buttonbox.AddSizer( radio_vbox, 0, wxALIGN_CENTRE|wxALL, 5 )
+ buttonbox.Add( buttonChange, 0, wxALIGN_CENTRE|wxALL, 5 )
+ buttonbox.Add( radio_vbox, 0, wxALIGN_CENTRE|wxALL, 5 )
+
+ hbox = wxBoxSizer( wxHORIZONTAL )
+ hbox.Add( grid, 0, wxALIGN_LEFT|wxALL, 15 )
+ hbox.Add( buttonbox, 0, wxALIGN_RIGHT|wxBOTTOM, 20 )
+
+
+ box_label = wxStaticBox( self, -1, "Bounds Control" )
+ boundsbox = wxStaticBoxSizer( box_label, wxHORIZONTAL )
+ self.set_bounds = wxCheckBox( self, -1, "Set time bounds:" )
+
+ minlabel = wxStaticText( self, -1, "minimum time:" )
+ self.min = wxTimeCtrl( self, -1, name="min", display_seconds = False )
+ self.min.Enable( False )
+
+ maxlabel = wxStaticText( self, -1, "maximum time:" )
+ self.max = wxTimeCtrl( self, -1, name="max", display_seconds = False )
+ self.max.Enable( False )
+
+ self.limit_check = wxCheckBox( self, -1, "Limit control" )
+
+ label = wxStaticText( self, -1, "Resulting time control:" )
+ self.target_ctrl = wxTimeCtrl( self, -1, name="new" )
+
+ grid2 = wxFlexGridSizer( 0, 2, 0, 0 )
+ grid2.Add( 20, 0, 0, wxALIGN_LEFT|wxALL, 5 )
+ grid2.Add( 20, 0, 0, wxALIGN_LEFT|wxALL, 5 )
+
+ grid2.Add( self.set_bounds, 0, wxALIGN_LEFT|wxALL, 5 )
+ grid3 = wxFlexGridSizer( 0, 2, 5, 5 )
+ grid3.Add(minlabel, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL )
+ grid3.Add( self.min, 0, wxALIGN_LEFT )
+ grid3.Add(maxlabel, 0, wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL )
+ grid3.Add( self.max, 0, wxALIGN_LEFT )
+ grid2.Add(grid3, 0, wxALIGN_LEFT )
+
+ grid2.Add( self.limit_check, 0, wxALIGN_LEFT|wxALL, 5 )
+ grid2.Add( 20, 0, 0, wxALIGN_LEFT|wxALL, 5 )
+
+ grid2.Add( 20, 0, 0, wxALIGN_LEFT|wxALL, 5 )
+ grid2.Add( 20, 0, 0, wxALIGN_LEFT|wxALL, 5 )
+ grid2.Add( label, 0, wxALIGN_LEFT|wxALIGN_CENTER_VERTICAL|wxALL, 5 )
+ grid2.Add( self.target_ctrl, 0, wxALIGN_LEFT|wxALL, 5 )
+ boundsbox.Add(grid2, 0, wxALIGN_CENTER|wxEXPAND|wxALL, 5)
+
+ vbox = wxBoxSizer( wxVERTICAL )
+ vbox.AddSpacer(20, 20)
+ vbox.Add( hbox, 0, wxALIGN_LEFT|wxALL, 5)
+ vbox.Add( boundsbox, 0, wxALIGN_LEFT|wxALL, 5 )
+
outer_box = wxBoxSizer( wxVERTICAL )
- outer_box.AddSizer( grid, 0, wxALIGN_CENTRE|wxBOTTOM, 20 )
- outer_box.AddSizer( buttonbox, 0, wxALIGN_CENTRE|wxALL, 5 )
+ outer_box.Add( vbox, 0, wxALIGN_LEFT|wxALL, 5)
# Turn on mxDateTime option only if we can import the module:
@@ -72,23 +118,31 @@ class TestPanel( wxPanel ):
self.radioMx.Enable( False )
- panel.SetAutoLayout( True )
- panel.SetSizer( outer_box )
- outer_box.Fit( panel )
- panel.Move( (50,50) )
- self.panel = panel
-
+ self.SetAutoLayout( True )
+ self.SetSizer( outer_box )
+ outer_box.Fit( self )
+ self.SetupScrolling()
+ EVT_BUTTON( self, buttonChange.GetId(), self.OnButtonClick )
EVT_TIMEUPDATE( self, self.time12.GetId(), self.OnTimeChange )
EVT_TIMEUPDATE( self, self.time24.GetId(), self.OnTimeChange )
EVT_TIMEUPDATE( self, self.spinless_ctrl.GetId(), self.OnTimeChange )
- EVT_BUTTON( self, buttonChange.GetId(), self.OnButtonClick )
+
+ EVT_CHECKBOX( self, self.set_bounds.GetId(), self.OnBoundsCheck )
+ EVT_CHECKBOX( self, self.limit_check.GetId(), self.SetTargetMinMax )
+ EVT_TIMEUPDATE( self, self.min.GetId(), self.SetTargetMinMax )
+ EVT_TIMEUPDATE( self, self.max.GetId(), self.SetTargetMinMax )
+ EVT_TIMEUPDATE( self, self.target_ctrl.GetId(), self.OnTimeChange )
+
def OnTimeChange( self, event ):
- timectrl = self.panel.FindWindowById( event.GetId() )
- self.log.write('%s time = %s\n' % ( timectrl.GetName(), timectrl.GetValue() ) )
+ timectrl = self.FindWindowById( event.GetId() )
+ ib_str = [ " (out of bounds)", "" ]
+
+ self.log.write('%s time = %s%s\n' % ( timectrl.GetName(), timectrl.GetValue(), ib_str[ timectrl.IsInBounds() ] ) )
+
def OnButtonClick( self, event ):
if self.radio12to24.GetValue():
@@ -99,16 +153,53 @@ class TestPanel( wxPanel ):
elif self.radioWx.GetValue():
now = wxDateTime_Now()
- self.time12.SetWxDateTime( now )
- self.time24.SetWxDateTime( now )
- self.spinless_ctrl.SetWxDateTime( now )
+ self.time12.SetValue( now )
+ # (demonstrates that G/SetValue returns/takes a wxDateTime)
+ self.time24.SetValue( self.time12.GetValue(as_wxDateTime=True) )
+
+ # (demonstrates that G/SetValue returns/takes a wxTimeSpan)
+ self.spinless_ctrl.SetValue( self.time12.GetValue(as_wxTimeSpan=True) )
elif self.radioMx.GetValue():
from mx import DateTime
now = DateTime.now()
- self.time12.SetMxDateTime( now )
- self.time24.SetMxDateTime( now )
- self.spinless_ctrl.SetMxDateTime( now )
+ self.time12.SetValue( now )
+
+ # (demonstrates that G/SetValue returns/takes a DateTime)
+ self.time24.SetValue( self.time12.GetValue(as_mxDateTime=True) )
+
+ # (demonstrates that G/SetValue returns/takes a DateTimeDelta)
+ self.spinless_ctrl.SetValue( self.time12.GetValue(as_mxDateTimeDelta=True) )
+
+
+ def OnBoundsCheck( self, event ):
+ self.min.Enable( self.set_bounds.GetValue() )
+ self.max.Enable( self.set_bounds.GetValue() )
+ self.SetTargetMinMax()
+
+
+ def SetTargetMinMax( self, event=None ):
+ min = max = None
+
+ if self.set_bounds.GetValue():
+ min = self.min.GetWxDateTime()
+ max = self.max.GetWxDateTime()
+ else:
+ min, max = None, None
+
+ cur_min, cur_max = self.target_ctrl.GetBounds()
+
+ if min != cur_min: self.target_ctrl.SetMin( min )
+ if max != cur_max: self.target_ctrl.SetMax( max )
+
+ self.target_ctrl.SetLimited( self.limit_check.GetValue() )
+
+ if min != cur_min or max != cur_max:
+ new_min, new_max = self.target_ctrl.GetBounds()
+ if new_min and new_max:
+ self.log.write( "current min, max: (%s, %s)\n" % ( new_min.FormatTime(), new_max.FormatTime() ) )
+ else:
+ self.log.write( "current min, max: (None, None)\n" )
#----------------------------------------------------------------------
@@ -118,93 +209,7 @@ def runTest( frame, nb, log ):
#----------------------------------------------------------------------
-overview = """
-
-wxTimeCtrl 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.
-
-Left/right/tab keys to switch cells within a wxTimeCtrl, and the up/down arrows act
-like a spin control. wxTimeCtrl also allows for an actual spin button to be attached
-to the control, so that it acts like the up/down arrow keys.
-
-The ! or c key sets the value of the control to now.
-
-Here's the API for wxTimeCtrl:
-
- wxTimeCtrl(
- parent, id = -1,
- value = '12:00:00 AM',
- pos = wxDefaultPosition,
- size = wxDefaultSize,
- fmt24hr = False,
- spinButton = None,
- style = wxTE_PROCESS_TAB,
- name = "time")
-
-
- - value
-
- 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.)
-
size
- - The size of the control will be automatically adjusted for 12/24 hour format
- if wxDefaultSize is specified.
-
- - fmt24hr
-
- 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.
-
- - spinButton
-
- If specified, this button's events will be bound to the behavior of the
- wxTimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
-
- - style
-
- By default, wxTimeCtrl will process TAB events, by allowing tab to the
- different cells within the control.
-
-
-
-
-- SetValue(time_string)
-
- Sets the value of the control to a particular time, given a valid time string;
-raises ValueError on invalid value
-
- - GetValue()
-
- Retrieves the string value of the time from the control
-
- - SetWxDateTime(wxDateTime)
-
- Uses the time portion of a wxDateTime to construct a value for the control.
-
- - GetWxDateTime()
-
- Retrieves the value of the control, and applies it to the wxDateTimeFromHMS()
-constructor, and returns the resulting value. (This returns the date portion as
-"today".)
-
- - SetMxDateTime(mxDateTime)
-
- Uses the time portion of an mxDateTime to construct a value for the control.
-NOTE: This imports mx.DateTime at runtime only if this or the Get function
-is called.
-
- - GetMxDateTime()
-
- Retrieves the value of the control and applies it to the DateTime.Time()
-constructor, and returns the resulting value. (mxDateTime is smart enough to
-know this is just a time value.)
-
- - BindSpinButton(wxSpinBtton)
-
- 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.)
-
- - EVT_TIMEUPDATE(win, id, func)
-
- func is fired whenever the value of the control changes.
-
-
-"""
-
-
+overview = overviewdoc
if __name__ == '__main__':
import sys,os
diff --git a/wxPython/wxPython/lib/maskededit.py b/wxPython/wxPython/lib/maskededit.py
new file mode 100644
index 0000000000..1b88d363ff
--- /dev/null
+++ b/wxPython/wxPython/lib/maskededit.py
@@ -0,0 +1,4879 @@
+#----------------------------------------------------------------------------
+# 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, 2003
+# Portions: (c) 2002 by Will Sadkin, 2002-2003
+# RCS-ID: $Id$
+# License: wxWindows license
+#----------------------------------------------------------------------------
+# NOTE:
+# This was written way it is because of the lack of masked edit controls
+# in wxWindows/wxPython.
+#
+# wxMaskedEdit 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 (wxTimeCtrl).
+#
+# wxMaskedEdit 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.
+##
+
+"""\
+Masked Edit Overview:
+=====================
+ wxMaskedTextCtrl
+ is a sublassed text control that can carefully control
+ the user's input based on a mask string you provide.
+
+ General usage example:
+ control = wxMaskedTextCtrl( 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 #
+ character.
+
+ wxMaskedComboBox
+ 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.
+
+ wxIpAddrCtrl
+ is a special subclass of wxMaskedTextCtrl that handles
+ cursor movement and natural typing of IP addresses.
+
+
+INITILIZATION PARAMETERS
+========================
+mask=
+Allowed mask characters and function:
+ Character Function
+ # Allow numeric only (0-9)
+ N Allow letters (a-z,A-Z) and numbers (0-9)
+ A Allow uppercase letters only (A-Z)
+ a Allow lowercase letters only (a-z)
+ X Allow any typeable character
+ C Allow any letter, upper or lower (a-z,A-Z)
+
+ Using these 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'.)
+
+ Note: Changing the mask for a control deletes any previous field classes
+ (and any associated validation or formatting constraints) for them.
+
+
+useFixedWidthFont=
+ 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.
+
+
+formatcodes=
+ These other properties can be passed to the class when instantiating it:
+ formatcodes A string of 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)
+ < stay in field until explicit navigation out of it
+ , Group digits
+ - Signed numerals, (negative #s shown in red by default)
+ 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
+
+fillChar=
+defaultValue=
+ 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.
+
+
+autoCompleteKeycodes=[]
+ 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.
+
+
+
+Validating User Input:
+======================
+ 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
+ choiceRequired= value must be member of choices list
+ compareNoCase= Perform case-insensitive matching when validating against list
+ 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.
+
+
+Coloring Behavior:
+==================
+ 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 .SetMaskParameters() function. Pass a color as string e.g. 'Yellow':
+
+ emptyBackgroundColor= Control Background color when identified as empty. Default=White
+ invalidBackgroundColor= Control Background color when identified as Not valid. Default=Yellow
+ validBackgroundColor= 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':
+ foregroundColor= Control foreground color when value is not negative. Default=Black
+ signedForegroundColor= Control foreground color when value is negative. Default=Red
+
+
+Fields:
+=======
+ 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.
+
+fields=
+ 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
+ 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 # ' ' '
+
+
+
+Control Class Functions:
+========================
+ .GetPlainValue(value=None)
+ 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"
+ .ClearValue() Returns the control's value to its default, and places the
+ cursor at the beginning of the control.
+ .SetValue() Does "smart insertion" of passed value into the control, as
+ does the .Paste() method.
+ .IsValid(value=None)
+ Returns True if the value specified (or the value of the control
+ if not specified) passes validation tests
+ .IsEmpty(value=None)
+ 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.
+ .IsDefault(value=None)
+ Returns True if the value specified (or the value of the control
+ if not specified) is equal to the initial value of the control.
+
+ .Refresh() Recolors the control as appropriate to its current settings.
+
+ .SetMaskParameters(**kwargs)
+ 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 = wxMaskedTextCtrl( self, -1 )
+ ctl.SetMaskParameters( mask='###-####',
+ defaultValue='555-1212',
+ formatcodes='F')
+
+ .GetMaskParameter(parametername)
+ This function allows you to retrieve the current value of a parameter
+ from the control.
+
+ .SetFieldParameters(field_index, **kwargs)
+ This function allows you to specify change individual field
+ parameters after construction. (Indices are 0-based.)
+
+ .GetFieldParameter(field_index, parametername)
+ 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. 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 decimal control
+(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
+integer control truncates the value.
+
+
+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.
+
+"""
+
+"""
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+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 wxIpAddrCtrl(wxMaskedTextCtrl) ), 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 wxMaskedTextCtrl, and .SetMark() for
+ wxMaskedComboBox.
+
+ ._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.
+
+ .Cut()
+ .Paste()
+ .SetValue() REQUIRED
+ Each class derived from wxMaskedEditMixin must redefine
+ these functions to call the _Cut(), _Paste() and _Paste()
+ 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 wxMaskedComboBox, this has to
+ be done with code, rather than specifying the proper
+ function in the base control, as there isn't one...)
+
+
+
+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_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
+
+ 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 wxMaskedTextCtrl and wxMaskedComboBox, 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 wxIpAddrCtrl)
+
+ ._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. wxIpAddrCtrl 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_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
+
+ _OnChangeField(event) primarily used for tab events, but can be
+ used for other keys (eg. '.' in wxIpAddrCtrl)
+
+ _OnErase(event) used for backspace and delete
+ _OnHome(event)
+ _OnEnd(event)
+
+"""
+
+from wxPython.wx import *
+import string, re, copy
+
+from wxPython.tools.dbg import Logger
+dbg = Logger()
+dbg(enable=0)
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## Constants for identifying control keys and classes of keys:
+
+WXK_CTRL_X = (ord('X')+1) - ord('A') ## These keys are not already defined in wx
+WXK_CTRL_V = (ord('V')+1) - ord('A')
+WXK_CTRL_C = (ord('C')+1) - ord('A')
+WXK_CTRL_S = (ord('S')+1) - ord('A')
+WXK_CTRL_A = (ord('A')+1) - ord('A')
+
+nav = (WXK_BACK, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, WXK_TAB, WXK_HOME, WXK_END, WXK_RETURN, WXK_PRIOR, WXK_NEXT)
+control = (WXK_BACK, WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, WXK_CTRL_X)
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## Constants for masking. This is where mask characters
+## are defined.
+## maskchars used to identify valid mask characters from all others
+## masktran used to replace mask chars with spaces
+## maskchardict used to defined allowed character sets for each mask char
+## #- 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 (letters, punctuation, digits)
+##
+maskchars = ("#","A","a","X","C","N")
+maskchardict = {
+ "#": string.digits,
+ "A": string.uppercase,
+ "a": string.lowercase,
+ "X": string.letters + string.punctuation + string.digits,
+ "C": string.letters,
+ "N": string.letters + string.digits
+ }
+
+months = '(01|02|03|04|05|06|07|08|09|10|11|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
+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(',')
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+## The following table defines the current set of autoformat codes:
+
+masktags = {
+ # Name: (mask, excludeChars, formatcodes, validRegex, choices, [choiceRequired])
+ "USPHONEFULLEXT":("(###) ###-#### x:###","",'F^-R',"^\(\d{3}\) \d{3}-\d{4}",[]),
+ "USPHONETIGHTEXT":("###-###-#### x:###","",'F^-R',"^\d{3}-\d{3}-\d{4}",[]),
+ "USPHONEFULL":("(###) ###-####","",'F^-R',"^\(\d{3}\) \d{3}-\d{4}",[]),
+ "USPHONETIGHT":("###-###-####","",'F^-R',"^\d{3}-\d{3}-\d{4}",[]),
+ "USSTATE":("AA","",'F!V',"([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), states, True),
+ "USSTATE3":("AAA","",'F!',"[A-Z]{3}",states, False),
+ "USDATETIMEMMDDYYYY/HHMMSS":("##/##/#### ##:##:## AM",'BCDEFGHIJKLMNOQRSTUVWXYZ','DF!','^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[]),
+ "USDATETIMEMMDDYYYY-HHMMSS":("##-##-#### ##:##:## AM",'BCDEFGHIJKLMNOQRSTUVWXYZ','DF!','^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[]),
+ "USDATEMILTIMEMMDDYYYY/HHMMSS":("##/##/#### ##:##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[]),
+ "USDATEMILTIMEMMDDYYYY-HHMMSS":("##-##-#### ##:##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[]),
+ "USDATETIMEMMDDYYYY/HHMM":("##/##/#### ##:## AM",'BCDEFGHIJKLMNOQRSTUVWXYZ','DF!','^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[]),
+ "USDATEMILTIMEMMDDYYYY/HHMM":("##/##/#### ##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,[]),
+ "USDATETIMEMMDDYYYY-HHMM":("##-##-#### ##:## AM",'BCDEFGHIJKLMNOQRSTUVWXYZ','DF!','^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[]),
+ "USDATEMILTIMEMMDDYYYY-HHMM":("##-##-#### ##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,[]),
+ "USDATEMMDDYYYY/":("##/##/####",'','DF','^' + months + '/' + days + '/' + '\d{4}',[]),
+ "EUDATEYYYYMMDD/":("####.##.##",'','DF','^' + '\d{4}'+ '.' + months + '.' + days,[]),
+ "USDATEMMDDYY/":("##/##/##",'','F','^' + months + '/' + days + '/\d\d',[]),
+ "USDATEMMDDYYYY-":("##-##-####",'','DF','^' + months + '-' + days + '-' +'\d{4}',[]),
+ "TIMEHHMMSS":("##:##:## AM", "BCDEFGHIJKLMNOQRSTUVWXYZ", 'TF!', '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[]),
+ "TIMEHHMM":("##:## AM", "BCDEFGHIJKLMNOQRSTUVWXYZ", 'TF!', '^' + hours + ':' + minutes + ' (A|P)M',[]),
+ "MILTIMEHHMMSS":("##:##:##", "", 'TF', '^' + milhours + ':' + minutes + ':' + seconds,[]),
+ "MILTIMEHHMM":("##:##", "", 'TF', '^' + milhours + ':' + minutes,[]),
+ "USSOCIALSEC":("###-##-####","",'F',"\d{3}-\d{2}-\d{4}",[]),
+ "CREDITCARD":("####-####-####-####","",'F',"\d{4}-\d{4}-\d{4}-\d{4}",[]),
+ "EXPDATEMMYY":("##/##", "", "F", "^" + months + "/\d\d",[]),
+ "USZIP":("#####","",'F',"^\d{5}",[]),
+ "USZIPPLUS4":("#####-####","",'F',"\d{5}-(\s{4}|\d{4})",[]),
+ "PERCENT":("0.##","",'F',"^0.\d\d",[]),
+ "AGE":("###","","F","^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]",[]),
+ "EMAIL":("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"," \\/*&%$#!+='\"","F",
+ "[a-zA-Z](\.\w+|\w*)@\w+.([a-zA-Z]\w*\.)*(com|org|net|edu|mil|gov|(co\.)?\w\w) *$",[]),
+ "IPADDR":("###.###.###.###", "", 'F_Sr<',
+ "( \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}",[])
+ }
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+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
+ '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
+ 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl
+ 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search
+ 'validRequired': False, ## Set to True to disallow input that results in an invalid value
+ 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID
+ }
+
+
+ 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():
+ 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._groupchar = ',' # (this might be changable in a future version, but for now is constant)
+
+ self._SetParameters(**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():
+ 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)
+ dbg(indent=0)
+
+ # First, Assign all parameters specified:
+ for key in Field.valid_params.keys():
+ if kwargs.has_key(key):
+ setattr(self, '_' + key, kwargs[key] )
+
+ # Now go do validation, semantic and inter-dependency parameter processing:
+ if kwargs.has_key('choiceRequired'): # (set/changed)
+ if self._choiceRequired:
+ self._choices = [choice.strip() for choice in self._choices]
+
+ if kwargs.has_key('compareNoCase'): # (set/changed)
+ if self._compareNoCase and self._choices:
+ self._choices = [item.lower() for item in self._choices]
+ dbg('modified choices:', self._choices)
+
+ 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._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('formatcodes') or kwargs.has_key('validRegex'):
+ self._regexMask = 'V' in self._formatcodes and self._validRegex
+
+ 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:
+ raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
+ else:
+ self._filter = None
+
+ if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed)
+ self._isInt = isInteger(self._mask)
+ dbg('isInt?', self._isInt)
+
+ if kwargs.has_key('validRange'): # (set/changed)
+ self._hasRange = False
+ self._rangeHigh = 0
+ self._rangeLow = 0
+ if self._validRange:
+ if type(self._validRange) != type(()) or len( self._validRange )!= 2 or self._validRange[0] >= self._validRange[1]:
+ 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'): # (set/changed)
+ self._hasList = False
+ if type(self._choices) not in (type(()), type([])):
+ 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) != type(''):
+ raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
+
+ # Verify each choice specified is valid:
+ for choice in self._choices:
+ if not self.IsValid(choice):
+ raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
+ self._hasList = True
+
+ # 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'):
+ 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
+ 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._emptyInvalid and self.IsEmpty(slice):
+ valid = False
+
+ 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):
+ compareStr = slice.strip()
+ if self._compareNoCase:
+ compareStr = compareStr.lower()
+ valid = (compareStr in self._choices)
+
+ elif self._hasRange and not self.IsEmpty(slice):
+ dbg('validating against range')
+ try:
+ valid = self._rangeLow <= int(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)
+ if self._isInt:
+ intStr = slice.replace( '-', '' ) # drop sign, if any
+ 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
+ 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
+ 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 )
+ dbg('adjusted slice: "%s"' % slice, indent=0)
+ return slice
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class wxMaskedEditMixin:
+ """
+ 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
+ 'emptyBackgroundColor': "White",
+ 'validBackgroundColor': "White",
+ 'invalidBackgroundColor': "Yellow",
+ 'foregroundColor': "Black",
+ 'signedForegroundColor': "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
+
+ # Validate legitimate set of parameters:
+ for key in kwargs.keys():
+ if key not in wxMaskedEditMixin.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:
+ WXK_BACK: self._OnErase,
+ WXK_LEFT: self._OnArrow,
+ WXK_RIGHT: self._OnArrow,
+ WXK_UP: self._OnAutoCompleteField,
+ WXK_DOWN: self._OnAutoCompleteField,
+ WXK_TAB: self._OnChangeField,
+ WXK_HOME: self._OnHome,
+ WXK_END: self._OnEnd,
+ WXK_RETURN: self._OnReturn,
+ WXK_PRIOR: self._OnAutoCompleteField,
+ WXK_NEXT: self._OnAutoCompleteField,
+
+ # default function control keys and handlers:
+ WXK_DELETE: self._OnErase,
+ WXK_CTRL_A: self._OnCtrl_A,
+ WXK_CTRL_C: self._baseCtrlEventHandler,
+ WXK_CTRL_S: self._OnCtrl_S,
+ WXK_CTRL_V: self._OnCtrl_V,
+ WXK_CTRL_X: self._OnCtrl_X,
+ }
+
+ ## 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)
+
+ ## self._ignoreChange is used by wxMaskedComboBox, 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
+ self._oldvalue = None
+
+ self._valid = True
+
+ # Set defaults for each parameter for this instance, and fully
+ # populate initial parameter list for configuration:
+ for key, value in wxMaskedEditMixin.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.SetMaskParameters(**kwargs)
+
+
+
+ def SetMaskParameters(self, **kwargs):
+ """
+ This public function can be used to set individual or multiple masked edit
+ parameters after construction.
+ """
+ dbg('wxMaskedEditMixin::SetMaskParameters', 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():
+ if key not in wxMaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
+ raise TypeError('%s: invalid keyword argument "%s"' % (self.name, key))
+ elif key in Field.valid_params:
+ 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
+
+ if autoformat != self._autoformat and autoformat in masktags.keys():
+ dbg('autoformat:', autoformat)
+ self._autoformat = autoformat
+ mask = masktags[self._autoformat][0]
+ constraint_kwargs['excludeChars'] = masktags[self._autoformat][1]
+ constraint_kwargs['formatcodes'] = masktags[self._autoformat][2]
+ constraint_kwargs['validRegex'] = masktags[self._autoformat][3]
+ constraint_kwargs['choices'] = masktags[self._autoformat][4]
+ if masktags[self._autoformat][4]:
+ constraint_kwargs['choiceRequired'] = masktags[self._autoformat][5]
+
+ 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)
+ 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)
+ raise AttributeError('invalid type for field parameter: %s' % repr(field))
+ self._fields[index] = field
+ else:
+ dbg(indent=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 wxMaskedEditMixin.valid_ctrl_params.keys():
+ if key in ( 'mask', 'fields' ): continue # (processed separately)
+ if ctrl_kwargs.has_key(key):
+ setattr(self, '_' + key, ctrl_kwargs[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 ('fillChar', 'compareNoCase', 'defaultValue', 'validRequired'):
+ 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)
+
+ 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.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') or not self._GetValue().strip():
+ self._SetInitialValue()
+
+ if self._autofit:
+ self.SetClientSize(self._calcSize())
+
+ # Set value/type-specific formatting
+ self._applyFormatting()
+ dbg(indent=0)
+
+
+ def GetMaskParameter(self, paramname):
+ """
+ Routine for retrieving the value of any given parameter
+ """
+ if wxMaskedEditMixin.valid_ctrl_params.has_key(paramname):
+ return getattr(self, '_' + paramname)
+ elif Field.valid_params.has_key(paramname):
+ return self._ctrl_constraints._GetParameter(paramname)
+ else:
+ TypeError('%s.GetMaskParameter: invalid parameter "%s"' % (self.name, paramname))
+
+
+ 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: %s is not a valid field for this control.' % (self.name, str(field_index)))
+ # 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)
+
+ 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: %s is not a valid field for this control.' % (self.name, str(field_index)))
+ elif Field.valid_params.has_key(paramname):
+ return self._fields[field_index]._GetParameter(paramname)
+ else:
+ TypeError('%s.GetMaskParameter: 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. 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. 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._isDec = isDecimal(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._isDec or self._isInt)
+## dbg('isDecimal(%s)?' % s, isDecimal(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
+
+ # 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(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.
+ """
+ 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:
+ self._lookupField = {}
+
+ if self._signOk: start = 1
+ else: start = 0
+
+ if self._isDec:
+ # 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)
+
+ self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, len(self._mask)),
+ mask=self._mask[self._decimalpos+1:len(self._mask)])
+
+ 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)
+ self._fields[0]._SetParameters(extent=(start, len(self._mask)),
+ mask=self._mask[start:len(self._mask)])
+
+ 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):
+ self._fields[field_index] = Field()
+ 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():
+ dbg(indent=0)
+ 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._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('%s: default value of "%s" is not a valid value' % (self.name, self._defaultValue))
+
+ # 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:
+ 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
+ for i in self._field_indices:
+ field = self._fields[i]
+ inherit_args = {}
+
+ 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 include_chars:
+ 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 exclude_chars:
+ 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 ['fillChar', 'compareNoCase', 'emptyInvalid', 'validRequired']:
+ if reset_args.has_key(param) and reset_args[param]:
+ inherit_args[param] = self.GetMaskParameter(param)
+
+ if inherit_args:
+ field._SetParameters(**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
+## 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:
+ valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
+ if not valid_paste:
+ dbg(indent=0)
+ raise ValueError('%s: "%s" could not be entered into field %d' % (self.name, choice, index))
+ elif replace_to > end:
+ dbg(indent=0)
+ raise ValueError('%s: "%s" will not fit into field %d' (self.name, choice, index))
+## 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 .SetMaskParameters()
+ routine, and indicates which if any parameters which can be
+ overridden by individual fields have been reset by request for the
+ whole control.
+ """
+ dbg('wxMaskedEditMixin::_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)
+ dbg('processed mask:', self._mask)
+
+ # Preserve original mask specified, for subsequent reprocessing
+ # if parameters change.
+ self._previous_mask = mask
+
+
+ # Set extent of field -1 to width of entire control:
+ self._ctrl_constraints._SetParameters(extent=(0,len(self._mask)))
+
+ # 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.
+ 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']
+
+ self._emptyInvalid = False
+ for field in self._fields.values():
+ if field._emptyInvalid:
+ self._emptyInvalid = True # if any field is required to be filled, color appropriately
+
+ self._calcTemplate(reset_fillchar, reset_default)
+
+ # Propagate control-level formatting and character constraints to each
+ # field if they don't already have them:
+ self._propagateConstraints(**reset_args)
+
+ # 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:
+ self._validateChoices()
+
+ 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
+ self._SetKeyHandler('-', self._OnChangeSign)
+ self._SetKeyHandler('+', self._OnChangeSign)
+ self._SetKeyHandler(' ', self._OnChangeSign)
+
+ if self._isDec or self._isInt:
+ dbg('Registering numeric navigation and control handlers')
+
+ # Replace up/down arrow default handling:
+ # make down act like tab, up act like shift-tab:
+ self._SetKeycodeHandler(WXK_DOWN, self._OnChangeField)
+ self._SetKeycodeHandler(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 dec, otherwise at 0.
+ self._SetKeyHandler('.', self._OnDecimalPoint)
+ self._SetKeyHandler('>', self._OnChangeField) # (Shift-'.')
+
+ # Allow selective insert of commas in numbers:
+ self._SetKeyHandler(',', self._OnGroupChar)
+
+ dbg(indent=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('wxMaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
+ if not value:
+ self._SetValue( self._template )
+ else:
+ # Apply validation as appropriate to passed value
+ self.SetValue(value)
+
+ # 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('wxMaskedEditMixin::_calcSize', indent=1)
+ cont = (size == wxDefaultSize or size is None)
+
+ if cont and self._autofit:
+ sizing_text = 'M' * len(self._mask)
+ if wxPlatform != "__WXMSW__": # give it a little extra space
+ sizing_text += 'M'
+ if wxPlatform == "__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('wxMaskedEditMixin::_setFont', indent=1)
+ if not self._useFixedWidthFont:
+ self._font = wxSystemSettings_GetFont(wxSYS_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 = wxFont( font.GetPointSize(), wxTELETYPE, 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('wxMaskedEditMixin::_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._oldvalue:
+ dbg('ignoring bogus text change event', indent=0)
+ else:
+ dbg('oldvalue: "%s", newvalue: "%s"' % (self._oldvalue, newvalue))
+ if self._Change():
+ self._CheckValid() # Recolor control as appropriate
+ event.Skip()
+ bValid = True
+ self._oldvalue = 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:
+ 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('wxMaskedEditMixin::_OnChar', indent=1)
+
+ # Get keypress value, adjusted by control options (e.g. convert to upper etc)
+ key = event.GetKeyCode()
+ orig_pos = self._GetInsertionPoint()
+ 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 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 not keep_processing:
+ dbg(indent=0)
+ return
+ # else skip default processing, but do final formatting
+ if key < 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)
+
+ if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars:
+ keep_processing = False
+ if (not wxValidator_IsSilent()) and orig_pos == pos:
+ wxBell()
+
+ 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 = self._insertKey(chr(key), pos, sel_start, sel_to, self._GetValue())
+ 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._isDec and newstr != self._template:
+ newstr = self._adjustDec(newstr)
+ dbg('adjusted str:', newstr)
+ if self.IsValid(newstr):
+ dbg("it is!")
+ keep_processing = True
+ wxCallAfter(self._SetInsertionPoint, self._decimalpos)
+ if not keep_processing:
+ dbg("key disallowed by validation")
+ if not wxValidator_IsSilent() and orig_pos == pos:
+ wxBell()
+
+ 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)
+
+ wxCallAfter(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 pos == 8 and unadjusted[8] != newstr[8]:
+ newpos = pos+2
+
+ wxCallAfter(self._SetInsertionPoint, newpos)
+ newfield = self._FindField(newpos)
+ if newfield != field and newfield._selectOnFieldEntry:
+ wxCallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
+ keep_processing = false
+ else:
+ dbg('char not allowed; orig_pos == pos?', orig_pos == pos)
+ keep_processing = False
+ if (not wxValidator_IsSilent()) and orig_pos == pos:
+ wxBell()
+
+ 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())
+ wxCallAfter(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('wxMaskedEditMixin::_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('wxMaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
+ if pos is None: pos = self._GetInsertionPoint()
+ elif pos < 0 or pos > len(self._mask):
+ 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("wxMaskedEditMixin::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('wxMaskedEditMixin::_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("wxMaskedEditMixin::_OnArrow", indent=1)
+ pos = self._GetInsertionPoint()
+ keycode = event.GetKeyCode()
+ sel_start, sel_to = self._GetSelection()
+ entry_end = self._goEnd(getPosOnly=True)
+ if keycode in (WXK_RIGHT, WXK_DOWN):
+ if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
+ or ( self._isTemplateChar(pos) and pos >= entry_end) ):
+ dbg(indent=0)
+ return False
+ elif self._isTemplateChar(pos):
+ self._AdjustField(pos)
+ elif keycode in (WXK_LEFT,WXK_UP) 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 (WXK_UP, 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 (WXK_UP, WXK_LEFT)
+ and sel_start != 0
+ and self._isTemplateChar(sel_start-1)):
+
+ # 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 (WXK_DOWN, WXK_RIGHT)
+ and sel_to != len(self._mask)
+ 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:
+ event.Skip()
+ else:
+ if( (sel_to == self._fields[0]._extent[0] and keycode == WXK_LEFT)
+ or (sel_to == len(self._mask) and keycode == WXK_RIGHT) ):
+ if not wxValidator_IsSilent():
+ wxBell()
+ else:
+ # treat arrows as normal, allowing selection
+ # as appropriate:
+ dbg('skipping event')
+ 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("wxMaskedEditMixin::_OnCtrl_S")
+ if self._demo:
+ print "wxMaskedEditMixin.GetValue() = %s\nwxMaskedEditMixin.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):
+ """ Handles ctrl-x keypress in control. Should return False to skip other processing. """
+ dbg("wxMaskedEditMixin::_OnCtrl_X", indent=1)
+ self._Cut()
+ dbg(indent=0)
+ return False
+
+
+ def _OnCtrl_V(self, event):
+ """ Handles ctrl-V keypress in control. Should return False to skip other processing. """
+ dbg("wxMaskedEditMixin::_OnCtrl_V", indent=1)
+ self._Paste()
+ dbg(indent=0)
+ return False
+
+
+ def _OnCtrl_A(self,event):
+ """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
+ end = self._goEnd(getPosOnly=True)
+ if event.ShiftDown():
+ wxCallAfter(self._SetInsertionPoint, 0)
+ wxCallAfter(self._SetSelection, 0, len(self._mask))
+ else:
+ wxCallAfter(self._SetInsertionPoint, 0)
+ wxCallAfter(self._SetSelection, 0, end)
+ return False
+
+
+ def _OnErase(self,event):
+ """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
+ dbg("wxMaskedEditMixin::_OnErase", indent=1)
+ sel_start, sel_to = self._GetSelection() ## check for a range of selected text
+ key = event.GetKeyCode()
+ if( ((sel_to == 0 or sel_to == self._fields[0]._extent[0]) and key == WXK_BACK)
+ or (sel_to == len(self._mask) and key == WXK_DELETE)):
+ if not wxValidator_IsSilent():
+ wxBell()
+ dbg(indent=0)
+ return False
+ if self._signOk and sel_start == self._signpos and self._isNeg:
+ dbg('sel_start %d == self._signpos %d and self._isNeg' % (sel_start, self._signpos))
+ dbg('self._isNeg => 0')
+ self._isNeg = False
+
+ field = self._FindField(sel_to)
+ start, end = field._extent
+ value = self._GetValue()
+
+ if( field._insertRight
+ and key == WXK_BACK
+ and sel_start >= start and sel_to == end # within field
+ and value[start:end] != self._template[start:end]): # and field not empty
+ dbg('delete left')
+ dbg('sel_start, start:', sel_start, start)
+ # special case: backspace at the end of a right insert field shifts contents right to cursor
+
+ if sel_start == end: # select "last char in field"
+ sel_start -= 1
+
+ newfield = value[start:sel_start]
+ dbg('cut newfield: "%s"' % newfield)
+ left = ""
+ for i in range(start, end - len(newfield)):
+ if field._padZero:
+ left += '0'
+ 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:]
+ pos = end
+ else:
+ if sel_start == sel_to:
+ dbg("current sel_start, sel_to:", sel_start, sel_to)
+ if key == 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):
+ newstr = value
+ newpos = sel_to
+ else:
+ newstr, newpos = self._insertKey(newchar, sel_to, sel_start, sel_to, value)
+
+ else:
+ newstr = value
+ newpos = sel_start
+ for i in range(sel_start, sel_to):
+ pos = i
+ newchar = self._template[pos] ## get an original template character to "clear" the current char
+
+ if not self._isTemplateChar(pos):
+ newstr, newpos = self._insertKey(newchar, pos, sel_start, sel_to, newstr)
+
+ pos = sel_start # put cursor back at beginning of selection
+ dbg('newstr:', newstr)
+
+ # 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 wxValidator_IsSilent():
+ wxBell()
+ 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 wxValidator_IsSilent():
+ wxBell()
+ dbg(indent=0)
+ return False
+
+ wxCallAfter(self._SetValue, newstr)
+ if self._isNeg and newstr[self._signpos] == ' ':
+ dbg("self._isNeg and newstr[self._signpos %d] == ' '" % self._signpos)
+ dbg('self._isNeg => 0')
+ self._isNeg = False
+ self._applyFormatting()
+
+ dbg('setting insertion point (later) to', pos)
+ wxCallAfter(self._SetInsertionPoint, pos)
+ dbg(indent=0)
+ return False
+
+
+ def _OnEnd(self,event):
+ """ Handles End keypress in control. Should return False to skip other processing. """
+ dbg("wxMaskedEditMixin::_OnEnd", indent=1)
+ pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+ end = self._goEnd(getPosOnly=True)
+
+ if event.ShiftDown():
+ dbg("shift-end; select to end of non-whitespace")
+ wxCallAfter(self._SetInsertionPoint, pos)
+ wxCallAfter(self._SetSelection, pos, end)
+ elif event.ControlDown():
+ dbg("control-end; select to end of control")
+ wxCallAfter(self._SetInsertionPoint, pos)
+ wxCallAfter(self._SetSelection, pos, len(self._mask))
+ else:
+ onChar = self._goEnd()
+ 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."
+ """
+ event.m_keyCode = WXK_TAB
+ event.Skip()
+
+
+ def _OnHome(self,event):
+ """ Handles Home keypress in control. Should return False to skip other processing."""
+ dbg("wxMaskedEditMixin::_OnHome", indent=1)
+ pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+ if event.ShiftDown():
+ dbg("shift-home; select to beginning of non-whitespace")
+ # Will Sadkin: for some reason that escapes me, the following
+ # is necessary to get the selection to work:
+ wxCallAfter(self._SetInsertionPoint, pos)
+ wxCallAfter(self._SetSelection, 0, pos)
+ else:
+ self._goHome()
+ 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('wxMaskedEditMixin::_OnChangeField', indent = 1)
+ # determine end of current field:
+ pos = self._GetInsertionPoint()
+ dbg('current pos:', pos)
+
+
+ masklength = len(self._mask)
+ if masklength < 0: # no fields; process tab normally
+ self._AdjustField(pos)
+ if event.GetKeyCode() == WXK_TAB:
+ dbg('tab to next ctrl')
+ event.Skip()
+ #else: do nothing
+ dbg(indent=0)
+ return False
+
+ field = self._FindField(pos)
+
+ 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 wxIpAddrCtrl.)
+
+ index = field._index
+ begin_field = field._extent[0]
+ if event.ControlDown():
+ dbg('select to beginning of field:', begin_field, pos)
+ wxCallAfter(self._SetInsertionPoint, begin_field)
+ wxCallAfter(self._SetSelection, begin_field, 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() == WXK_TAB:
+ dbg('tab to previous ctrl')
+ event.Skip()
+ else:
+ dbg('position at beginning')
+ wxCallAfter(self._SetInsertionPoint, begin_field)
+ dbg(indent=0)
+ return False
+ else:
+ # find beginning of previous field:
+ begin_prev = self._FindField(begin_field-1)._extent[0]
+ self._AdjustField(pos)
+ dbg('repositioning to', begin_prev)
+ wxCallAfter(self._SetInsertionPoint, begin_prev)
+ if self._FindField(begin_prev)._selectOnFieldEntry:
+ edit_start, edit_end = self._FindFieldExtent(begin_prev)
+ wxCallAfter(self._SetSelection, edit_start, edit_end)
+ dbg(indent=0)
+ return False
+
+ else:
+ # "Go forward"
+
+ end_field = field._extent[1]
+ if event.ControlDown():
+ dbg('select to end of field:', pos, end_field)
+ wxCallAfter(self._SetInsertionPoint, pos)
+ wxCallAfter(self._SetSelection, pos, end_field)
+ dbg(indent=0)
+ return False
+ else:
+ dbg('end of current field:', end_field)
+ dbg('go to next field')
+ if end_field == self._fields[self._field_indices[-1]]._extent[1]:
+ self._AdjustField(pos)
+ if event.GetKeyCode() == WXK_TAB:
+ dbg('tab to next ctrl')
+ event.Skip()
+ else:
+ dbg('position at end')
+ wxCallAfter(self._SetInsertionPoint, end_field)
+ dbg(indent=0)
+ return False
+ else:
+ # we have to find the start of the next field
+ next_pos = self._findNextEntry(end_field)
+ if next_pos == end_field:
+ dbg('already in last field')
+ self._AdjustField(pos)
+ if event.GetKeyCode() == 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)
+ wxCallAfter(self._SetInsertionPoint, next_pos)
+ edit_start, edit_end = self._FindFieldExtent(next_pos)
+ dbg('select', edit_start, edit_end)
+ wxCallAfter(self._SetSelection, edit_start, edit_end)
+ else:
+ if field._insertRight:
+ next_pos = field._extent[1]
+ dbg('move to ', next_pos)
+ wxCallAfter(self._SetInsertionPoint, next_pos)
+ dbg(indent=0)
+ return False
+
+
+ def _OnDecimalPoint(self, event):
+ dbg('wxMaskedEditMixin::_OnDecimalPoint', indent=1)
+
+ pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
+
+ if self._isDec: ## handle decimal 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] + '.' + value[self._decimalpos+1:]
+ dbg('value: "%s"' % self._GetValue(), 'clipped_text:', clipped_text)
+ newstr = self._adjustDec(clipped_text)
+ else:
+ newstr = self._adjustDec(value)
+ wxCallAfter(self._SetValue, newstr)
+ wxCallAfter(self._SetInsertionPoint, self._decimalpos+1)
+ 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:', clipped_text)
+ newstr = self._adjustInt(clipped_text)
+ wxCallAfter(self._SetValue, newstr)
+ keep_processing = False
+ dbg(indent=0)
+
+
+ def _OnChangeSign(self, event):
+ dbg('wxMaskedEditMixin::_OnChangeSign', indent=1)
+ key = event.GetKeyCode()
+ pos = self._adjustPos(self._GetInsertionPoint(), key)
+ if chr(key) in ("-","+") or (chr(key) == " " and pos == self._signpos):
+ cursign = self._isNeg
+## dbg('cursign:', cursign)
+ if chr(key) == "-":
+ self._isNeg = (not self._isNeg) ## flip value
+ else:
+ self._isNeg = False
+## dbg('new sign:', self._isNeg)
+
+ text, signpos = self._getSignedValue()
+ if text is not None:
+ self._signpos = signpos
+## dbg('self._signpos now:', self._signpos)
+ else:
+ text = self._GetValue()
+ self._signpos = 0
+## dbg('self._signpos now:', self._signpos)
+ if self._isNeg:
+ text = '-' + text[1:]
+
+ wxCallAfter(self._SetValue, text)
+ wxCallAfter(self._applyFormatting)
+ if pos == self._signpos:
+ wxCallAfter(self._SetInsertionPoint, self._signpos+1)
+ else:
+ wxCallAfter(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 ',' if appropriate.
+ """
+ dbg('wxMaskedEditMixin::_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 wxValidator_IsSilent():
+ wxBell()
+
+ 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 wxValidator_IsSilent():
+ wxBell()
+
+ if keep_processing:
+ wxCallAfter(self._SetValue, newstr)
+ wxCallAfter(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 < len(self._mask):
+ 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 < len(self._mask):
+ 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 < len(self._mask):
+ pos += 1
+ return pos
+
+
+ def _OnAutoCompleteField(self, event):
+ dbg('wxMaskedEditMixin::_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()
+ text = slice.replace(field._fillChar, '')
+ text = text.strip()
+ keep_processing = True # (assume True to start)
+ dbg('field._hasList?', field._hasList)
+ if field._hasList:
+ dbg('choices:', field._choices)
+ choices, choice_required = field._choices, field._choiceRequired
+ if keycode in (WXK_PRIOR, WXK_UP):
+ direction = -1
+ else:
+ direction = 1
+ match_index = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase)
+ if( match_index is None
+ and (keycode in self._autoCompleteKeycodes + [WXK_PRIOR, WXK_NEXT]
+ or (keycode in [WXK_UP, 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 + [WXK_PRIOR, WXK_NEXT]
+ or (keycode in [WXK_UP, WXK_DOWN] and event.ShiftDown())
+ or (keycode == WXK_DOWN and len(text) < len(choices[match_index])) ) ):
+
+ # We're allowed to auto-complete:
+ value = self._GetValue()
+ newvalue = value[:edit_start] + choices[match_index] + value[edit_end:]
+ dbg('match found; setting value to "%s"' % newvalue)
+ self._SetValue(newvalue)
+ self._SetInsertionPoint(edit_end)
+ self._CheckValid() # recolor as appopriate
+
+ if keycode in (WXK_UP, WXK_DOWN, WXK_LEFT, WXK_RIGHT):
+ # treat as left right arrow if unshifted, tab/shift tab if shifted.
+ if event.ShiftDown():
+ if keycode in (WXK_DOWN, WXK_RIGHT):
+ # remove "shifting" and treat as (forward) tab:
+ event.m_shiftDown = False
+ keep_processing = self._OnChangeField(event)
+ else:
+ keep_processing = self._OnArrow(event)
+ # some other key; keep processing the key
+
+ dbg('keep processing?', keep_processing, indent=0)
+ return keep_processing
+
+
+ def _autoComplete(self, direction, choices, value, compareNoCase):
+ """
+ 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.
+ """
+ if value is None:
+ dbg('nothing to match against', indent=0)
+ return None
+
+ if compareNoCase:
+ choices = [choice.strip().lower() for choice in choices]
+ value = value.lower()
+ else:
+ choices = [choice.strip() for choice in choices]
+
+ if value in choices:
+ index = choices.index(value)
+ dbg('matched "%s"' % choices[index])
+ if direction == -1:
+ 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 ', index)
+ match = index
+ else:
+ value = value.strip()
+ dbg('no match; try to auto-complete:')
+ match = None
+ dbg('searching for "%s"' % value)
+ for index in range(len(choices)):
+ choice = choices[index]
+ if choice.find(value, 0) == 0:
+ dbg('match found:', choice)
+ match = index
+ break
+ if match is not None:
+ dbg('matched', match)
+ else:
+ dbg('no match found')
+ dbg(indent=0)
+ return 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._isDec and newvalue != self._template:
+ newvalue = self._adjustDec(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 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
+ if( self._signOk
+ and pos == self._signpos
+ and key in (ord('-'), ord('+'), ord(' ')) ):
+ return pos
+
+ # 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 key not in self._nav and self._isTemplateChar(pos):
+ field = self._FindField(pos)
+ start, end = field._extent
+ field_len = end - start
+ slice = self._GetValue()[start:end].strip()
+ if field._insertRight: # if allow right-insert
+ if pos == end: # if cursor at right edge of field
+ # if not filled or supposed to stay in field, keep current position
+ if len(slice) < field_len or not field._moveOnFieldFull:
+ pass
+ else:
+ # if at start of control, move to right edge
+ if self._signOk and field._index == 0 and pos == 0:
+ pos = end # move to right edge
+ else:
+ # must be full; move to next field:
+ pos = self._findNextEntry(pos)
+ self._SetInsertionPoint(pos)
+ if pos < sel_to: # restore selection
+ self._SetSelection(pos, sel_to)
+
+ elif( 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 _adjustDec(self, candidate=None):
+ """
+ 'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
+ """
+ dbg('wxMaskedEditMixin::_adjustDec, candidate = "%s"' % candidate, indent=1)
+ lenOrd,lenDec = self._mask.split('.') ## Get ordinal, decimal lengths
+ lenOrd = len(lenOrd)
+ lenDec = len(lenDec)
+ if candidate is None: value = self._GetValue()
+ else: value = candidate
+ dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
+ ordStr,decStr = value.split('.')
+
+ ordStr = self._fields[0]._AdjustField(ordStr)
+ dbg('adjusted ordStr: "%s"' % ordStr)
+ lenOrd = len(ordStr)
+ decStr = decStr + ('0'*(lenDec-len(decStr))) # add trailing spaces to decimal
+
+ dbg('ordStr "%(ordStr)s"' % locals())
+ dbg('lenOrd:', lenOrd)
+
+ ordStr = string.rjust( ordStr[-lenOrd:], lenOrd)
+ dbg('right-justifed ordStr = "%(ordStr)s"' % locals())
+ newvalue = ordStr + '.' + decStr
+ if self._signOk:
+ 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('.')
+ if newdecpos < self._decimalpos:
+ padlen = self._decimalpos - newdecpos
+ newvalue = string.join([' ' * padlen] + [newvalue] ,'')
+ dbg('newvalue = "%s"' % newvalue)
+ if candidate is None:
+ wxCallAfter(self._SetValue, newvalue)
+ dbg(indent=0)
+ return newvalue
+
+
+ def _adjustInt(self, candidate=None):
+ """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
+ dbg("wxMaskedEditMixin::_adjustInt")
+ lenInt = len(self._mask)
+ if candidate is None: value = self._GetValue()
+ else: value = candidate
+ intStr = self._fields[0]._AdjustField(value)
+ intStr = intStr.strip() # drop extra spaces
+ if self._isNeg:
+ intStr = '-' + intStr
+
+ if self._fields[0]._alignRight: ## Only if right-alignment is enabled
+ intStr = intStr.rjust( lenInt )
+ else:
+ intStr = intStr.ljust( lenInt )
+
+ if candidate is not None:
+ wxCallAfter(self._SetValue, intStr )
+ return intStr
+
+
+ def _adjustDate(self, candidate=None, fixcentury=False):
+ """
+ 'Fixes' a date control, expanding the year if it can.
+ Applies various self-formatting options.
+ """
+ dbg("wxMaskedEditMixin::_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
+
+ 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
+ if( len(year) < 4
+ and (fixcentury
+ or (self._GetInsertionPoint() > 7 and text[8] == ' ')
+ or (self._GetInsertionPoint() > 8 and text[9] == ' ') ) ):
+ ## 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[6:10]
+
+ 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 = wxDateTime_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
+ text = makeDate(year, month, day, self._datestyle, text) + text[10:]
+ dbg('newdate:', text, indent=0)
+ return text
+
+
+ def _goEnd(self, getPosOnly=False):
+ """ Moves the insertion point to the end of user-entry """
+ dbg("wxMaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
+ text = self._GetValue()
+ for i in range( min( len(self._mask)-1, len(text)-1 ), -1, -1):
+ if self._isMaskChar(i):
+ char = text[i]
+ if char != ' ':
+ break
+
+ pos = min(i+1,len(self._mask))
+ 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):
+ """ Moves the insertion point to the beginning of user-entry """
+ dbg("wxMaskedEditMixin::_goHome", indent=1)
+ text = self._GetValue()
+ for i in range(len(self._mask)):
+ if self._isMaskChar(i):
+ break
+ self._SetInsertionPoint(max(i,0))
+ dbg(indent=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 = 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 < len(self._mask):
+ 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 < len(self._mask):
+ return not self._isMaskChar(Pos)
+ else:
+ return False
+
+
+ def _isCharAllowed(self, char, pos, checkRegex=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):
+ start, end = field._extent
+ if pos == end or (sel_start, sel_to) == field._extent:
+ pos = end - 1
+ right_insert = True
+
+ if self._isTemplateChar( pos ): ## if a template character, return empty
+ dbg(indent=0)
+ return False
+
+ if self._isMaskChar( pos ):
+ okChars = self._getAllowedChars(pos)
+ if self._fields[0]._groupdigits and (self._isInt or (self._isDec and pos < self._decimalpos)):
+ okChars += self._fields[0]._groupchar
+## 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:
+ newvalue, newpos = self._insertKey(char, pos+1, sel_start, sel_to, value)
+ else:
+ newvalue, newpos = self._insertKey(char, pos, 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(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('wxMaskedEditMixin::_applyFormatting', indent=1)
+
+ # Handle negative numbers
+ if (self._isDec or self._isInt) and self._isNeg and self._signOk:
+ signpos = self._GetValue().find('-')
+ if signpos == -1:
+ self._isNeg = False
+ dbg('supposedly negative, but no sign found; new sign:', self._isNeg)
+ elif signpos != self._signpos:
+ self._signpos = signpos
+ dbg("self._signpos now:", self._signpos)
+
+
+ if self._signOk and self._isNeg:
+ dbg('setting foreground to', self._signedForegroundColor)
+ self.SetForegroundColour(self._signedForegroundColor)
+ else:
+ dbg('setting foreground to', self._foregroundColor)
+ self.SetForegroundColour(self._foregroundColor)
+
+ if self.IsEmpty():
+ if self._emptyInvalid:
+ dbg('setting background to', self._invalidBackgroundColor)
+ else:
+ dbg('setting background to', self._emptyBackgroundColor)
+ self.SetBackgroundColour(self._emptyBackgroundColor)
+ self.Refresh()
+ dbg(indent=0, suspend=0)
+
+
+ def _getAbsValue(self, candidate=None):
+ """ Return an unsigned value (i.e. strip the '-' prefix if any).
+ """
+ dbg('wxMaskedEditMixin::_getAbsValue; candidate=', candidate, indent=1)
+ if candidate is None: text = self._GetValue()
+ else: text = candidate
+
+ if self._isInt:
+ signpos = 0
+ text = self._template[0] + text[1:]
+ else: # decimal value
+ try:
+ groupchar = self._fields[0]._groupchar
+ value = float(text.replace(groupchar,''))
+ if value < 0:
+ signpos = text.find('-')
+ text = text[:signpos] + self._template[signpos] + text[signpos+1:]
+ else:
+ # look backwards from the decimal point for the 1st non-digit
+ dbg('decimal pos:', self._decimalpos)
+ signpos = self._decimalpos-1
+ dbg('text: "%s"' % text)
+ dbg('text[%d]:' % signpos, text[signpos])
+ dbg('text[signpos] in list(string.digits) + [groupchar]?', text[signpos] in list(string.digits) + [groupchar])
+ while text[signpos] in list(string.digits) + [groupchar] and signpos > 0:
+ signpos -= 1
+ dbg('text[%d]:' % signpos, text[signpos])
+ except ValueError:
+ dbg('invalid number', indent=0)
+ return None, None
+
+ dbg('abstext = "%s"' % text, 'signpos:', signpos)
+ dbg(indent=0)
+ return text, 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('wxMaskedEditMixin::_getSignedValue; candidate=', candidate, indent=1)
+ if candidate is None: text = self._GetValue()
+ else: text = candidate
+
+
+ abstext, signpos = self._getAbsValue(text)
+ if self._signOk:
+ if abstext is None:
+ dbg(indent=0)
+ return abstext, signpos
+
+ if self._isNeg:
+ sign = '-'
+ else:
+ sign = ' '
+ text = text[:signpos] + sign + text[signpos+1:]
+ else:
+ text = abstext
+ dbg('signedtext = "%s"' % text, 'signpos:', signpos)
+ dbg(indent=0)
+ return text, signpos
+
+
+ def GetPlainValue(self, candidate=None):
+ """ Returns control's value stripped of the template text.
+ plainvalue = wxMaskedEditMixin.GetPlainValue()
+ """
+ if candidate is None: text = self._GetValue()
+ else: text = candidate
+
+ if (self._isInt or self._isDec):
+ return string.replace( text," ","") ## If numeric or decimal, return the value itself
+
+ elif self.IsEmpty():
+ return ""
+ else:
+ plain = ""
+ for idx in range( len( self._template)):
+ if self._mask[idx] in maskchars:
+ plain += text[idx]
+ return plain
+
+
+ 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('wxMaskedEditMixin::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("wxMaskedEditMixin::_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._isMaskChar(i):
+ field = self._FindField(i)
+ if field._padZero:
+ newvalue[i] = '0'
+ else:
+ newvalue[i] = self._template[i]
+## if not((self._isDec or self._isInt) and value[i] == ' '):
+## 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):
+ """ Handles replacement of the character at the current insertion point."""
+ dbg('wxMaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
+
+ text = self._eraseSelection(value)
+ field = self._FindField(pos)
+ start, end = field._extent
+ newtext = ""
+ if field._insertRight:
+ # special case; do right insertion if either whole field selected or cursor at right edge of field
+ if pos == end or (sel_start, sel_to) == field._extent: # right edge insert
+ fstr = text[start:end]
+ erasable_chars = [field._fillChar, ' ']
+ if field._padZero: erasable_chars.append('0')
+ if fstr[0] in erasable_chars:
+ fstr = fstr[1:] + char
+ newtext = text[:start] + fstr + text[end:]
+ if self._signOk and field._index == 0: start -= 1 # account for sign position
+ if( field._moveOnFieldFull
+ and len(fstr.lstrip()) == end-start): # if field now full
+ newpos = self._findNextEntry(end) # go to next field
+ else:
+ newpos = end # else keep cursor at right edge
+
+ if not newtext:
+ before = text[0:pos]
+ after = text[pos+1:]
+ newtext = before + char + after
+ newpos = pos+1
+
+ 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('wxMaskedEditMixin::_OnFocus')
+ wxCallAfter(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('wxMaskedEditMixin::_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._isDec): ## Numeric type
+ valid = self._validateNumeric(value)
+ dbg("valid Number?", valid)
+
+ if valid and not self.IsDefault(value):
+ ## valid so far; ensure also allowed by any list or regex provided:
+ valid = self._validateGeneric(value)
+ dbg("valid value?", valid)
+
+ if valid and self._emptyInvalid:
+ for field in self._fields.values():
+ start, end = field._extent
+ if field.IsEmpty(value[start:end]):
+ valid = False
+ break
+## elif self.IsEmpty(value):
+## valid = not self._emptyInvalid ## are empty values are OK
+## dbg('valid "empty" value?', valid)
+
+ dbg('valid?', valid)
+
+ if not candidate:
+ self._valid = valid
+ if self._valid and self.IsEmpty() and not self._emptyInvalid:
+ dbg('empty illegal; coloring', self._emptyBackgroundColor)
+ self.SetBackgroundColour(self._emptyBackgroundColor)
+ elif self._valid:
+ dbg('valid')
+ self.SetBackgroundColour(self._validBackgroundColor)
+ else:
+ dbg('invalid; coloring', self._invalidBackgroundColor)
+ self.SetBackgroundColour(self._invalidBackgroundColor) ## Change BG color if invalid
+
+ if self._valid != oldValid:
+ dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
+ dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
+ self._Refresh() ## Update the valid color -if- the value changed
+ 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._isDec:
+ number = float(value.replace(groupchar, ''))
+ else:
+ number = int( value.replace(groupchar, ''))
+ 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._isDec and groupcharpos > self._decimalpos:
+ # 1st one found on right-hand side is past decimal point
+ dbg('groupchar in fraction; illegal')
+ valid = False
+ elif self._isDec:
+ ord = value[:self._decimalpos]
+ else:
+ ord = value
+
+ parts = ord.split(groupchar)
+ 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('wxMaskedEditMixin::_validateDate', indent=1)
+ if candidate is None: value = self._GetValue()
+ else: value = candidate
+ dbg('value = "%s"' % value)
+ text = self._adjustDate(value) ## 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:10]
+ 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:]
+
+ parts = getDateParts( datestr, self._datestyle)
+ year,month,day = [int(part) for part in parts]
+ except ValueError:
+ dbg('cannot convert string to integer parts')
+ 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 = wxDateTimeFromDMY(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[11:].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')
+ 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('wxMaskedEditMixin::_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 = wxDateTime_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')
+ if valid: dbg('valid time')
+ dbg(indent=0)
+ return valid
+
+
+ def _OnKillFocus(self,event):
+ """ Handler for EVT_KILL_FOCUS event.
+ """
+ dbg('wxMaskedEditMixin::_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('wxMaskedEditMixin::_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)
+ 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()
+ if self._FindField(0)._selectOnFieldEntry:
+ edit_start, edit_end = self._FindFieldExtent(self._GetInsertionPoint())
+ self._SetSelection(edit_start, edit_end)
+ elif self._fields[0]._insertRight:
+ self._SetInsertionPoint(self._fields[0]._extent[1])
+
+ elif sel_start == 0 and self._GetValue()[0] == '-' and (self._isDec or self._isInt) and self._signOk:
+ dbg('control is empty; start at beginning after -')
+ self._SetInsertionPoint(1) ## Move past minus sign space if signed
+ if self._FindField(0)._selectOnFieldEntry:
+ edit_start, edit_end = self._FindFieldExtent(self._GetInsertionPoint())
+ self._SetSelection(1, edit_end)
+ elif self._fields[0]._insertRight:
+ self._SetInsertionPoint(self._fields[0]._extent[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, 'len(self._mask):', len(self._mask))
+ 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("wxMaskedEditMixin::_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()
+
+ wxCallAfter(self._SetValue, self._eraseSelection() )
+ wxCallAfter(self._SetInsertionPoint, sel_start)
+ 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):
+ """
+ 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('wxMaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d)' % locals(), indent=1)
+ select_length = sel_to - sel_start
+ maxlength = select_length
+ dbg('sel_to - sel_start:', maxlength)
+ if maxlength == 0:
+ maxlength = len(self._mask) - sel_start
+ dbg('maxlength:', maxlength)
+ length_considered = len(paste_text)
+ if length_considered > maxlength:
+ dbg('paste text will not fit into the control:', indent=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 < len(self._mask):
+ char = paste_text[i]
+ field = self._FindField(replace_to)
+ 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):
+ 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]:
+ 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
+ dbg('valid_paste?', valid_paste)
+ if valid_paste:
+ dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
+ dbg(indent=0, suspend=0)
+ return valid_paste, replacement_text, replace_to
+
+
+ def _Paste( self, value=None ):
+ """
+ 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('wxMaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
+ if value is None:
+ paste_text = self._getClipboardContents()
+ else:
+ paste_text = value
+
+ if paste_text:
+ dbg('paste text:', paste_text)
+ # (conversion will raise ValueError if paste isn't legal)
+ sel_start, sel_to = self._GetSelection()
+ valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to)
+
+ if not valid_paste:
+ dbg('paste text not legal for the selection or portion of the control following the cursor;')
+ dbg(indent=0)
+ return False
+ # else...
+ text = self._eraseSelection()
+
+ new_text = text[:sel_start] + replacement_text + text[replace_to:]
+ dbg("new_text:", '"'+new_text+'"')
+
+ if new_text == '':
+ self.ClearValue()
+ else:
+ wxCallAfter(self._SetValue, string.ljust(new_text,len(self._mask)))
+ new_pos = sel_start + len(replacement_text)
+ wxCallAfter(self._SetInsertionPoint, new_pos)
+ dbg(indent=0)
+
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class wxMaskedTextCtrl( wxTextCtrl, wxMaskedEditMixin ):
+ """
+ This is the primary derivation from wxMaskedEditMixin. It provides
+ a general masked text control that can be configured with different
+ masks.
+ """
+
+ def __init__( self, parent, id=-1, value = '',
+ pos = wxDefaultPosition,
+ size = wxDefaultSize,
+ style = wxTE_PROCESS_TAB,
+ validator=wxDefaultValidator, ## placeholder provided for data-transfer logic
+ name = 'maskedTextCtrl',
+ setupEventHandling = True, ## setup event handling by default
+ **kwargs):
+
+ wxTextCtrl.__init__(self, parent, id, value='',
+ pos=pos, size = size,
+ style=style, validator=validator,
+ name=name)
+
+ self.controlInitialized = True
+ wxMaskedEditMixin.__init__( self, name, **kwargs )
+ self._SetInitialValue(value)
+
+ 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_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
+
+
+ def __repr__(self):
+ return "" % self.GetValue()
+
+
+ def _GetSelection(self):
+ """
+ Allow mixin to get the text selection of this control.
+ REQUIRED by any class derived from wxMaskedEditMixin.
+ """
+ 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 wxMaskedEditMixin.
+ """
+ return self.SetSelection( sel_start, sel_to )
+
+ def SetSelection(self, sel_start, sel_to):
+ """
+ This is just for debugging...
+ """
+ dbg("wxMaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
+ wxTextCtrl.SetSelection(self, sel_start, sel_to)
+
+
+ def _GetInsertionPoint(self):
+ return self.GetInsertionPoint()
+
+ def _SetInsertionPoint(self, pos):
+ self.SetInsertionPoint(pos)
+
+ def SetInsertionPoint(self, pos):
+ """
+ This is just for debugging...
+ """
+ dbg("wxMaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals())
+ wxTextCtrl.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 wxMaskedEditMixin.
+ """
+ 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 wxMaskedEditMixin.
+ """
+ dbg('wxMaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
+ wxTextCtrl.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('wxMaskedTextCtrl::SetValue = "%s"' % value, indent=1)
+ self._Paste(value)
+ dbg(indent=0)
+
+
+ def _Refresh(self):
+ """
+ Allow mixin to refresh the base control with this function.
+ REQUIRED by any class derived from wxMaskedEditMixin.
+ """
+ dbg('wxMaskedTextCtrl::_Refresh', indent=1)
+ wxTextCtrl.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('wxMaskedTextCtrl::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 wxMaskedEditMixin.
+ """
+ return wxTextCtrl.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.
+ """
+ self._Cut() # call the mixin's Cut method
+
+
+ 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.
+ """
+ self._Paste() # call the mixin's Paste method
+
+
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
+## events, we have to do it ourselves when we auto-complete.
+class wxMaskedComboBoxSelectEvent(wxPyCommandEvent):
+ def __init__(self, id, selection = 0, object=None):
+ wxPyCommandEvent.__init__(self, 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 wxMaskedComboBox( wxComboBox, wxMaskedEditMixin ):
+ """
+ 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 = wxDefaultPosition,
+ size = wxDefaultSize,
+ choices = [],
+ style = wxCB_DROPDOWN,
+ validator = wxDefaultValidator,
+ 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 & wxCB_READONLY == wxCB_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
+
+ wxMaskedEditMixin.__init__( self, name, **kwargs )
+ self._choices = self._ctrl_constraints._choices
+ dbg('self._choices:', self._choices)
+
+ if self._ctrl_constraints._alignRight:
+ choices = [choice.rjust(len(self._mask)) for choice in choices]
+ else:
+ choices = [choice.ljust(len(self._mask)) for choice in choices]
+
+ wxComboBox.__init__(self, parent, id, value='',
+ pos=pos, size = size,
+ choices=choices, style=style|wxWANTS_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(len(self._mask))
+ else:
+ value = value.ljust(len(self._mask))
+
+ if self.__readonly:
+ self.SetStringSelection(value)
+ else:
+ self._SetInitialValue(value)
+
+
+ self._SetKeycodeHandler(WXK_UP, self.OnSelectChoice)
+ self._SetKeycodeHandler(WXK_DOWN, self.OnSelectChoice)
+
+ 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_CHAR( self, self._OnChar ) ## handle each keypress
+ EVT_KEY_DOWN( self, self.OnKeyDown ) ## for special processing of up/down keys
+ EVT_KEY_DOWN( self, self._OnKeyDown ) ## for processing the rest of the control keys
+ ## (next in evt chain)
+ EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately
+
+
+ def __repr__(self):
+ return "" % self.GetValue()
+
+
+ def calcSize(self, size=None):
+ """
+ Calculate automatic size if allowed; override 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 wxMaskedEditMixin.
+ """
+ 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 wxMaskedEditMixin.
+ """
+ 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 wxMaskedEditMixin.
+ """
+ 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 wxMaskedEditMixin.
+ """
+ # 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(len(self._mask))
+ else:
+ value = value.ljust(len(self._mask))
+ wxComboBox.SetValue(self, value)
+
+ 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.
+ """
+ self._Paste(value)
+
+
+ def _Refresh(self):
+ """
+ Allow mixin to refresh the base control with this function.
+ REQUIRED by any class derived from wxMaskedEditMixin.
+ """
+ wxComboBox.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 wxMaskedEditMixin.
+ """
+ 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.
+ """
+ self._Cut() # call the mixin's Cut method
+
+
+ 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.
+ """
+ self._Paste() # call the mixin's Paste method
+
+
+ def Append( self, choice ):
+ """
+ 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._ctrl_constraints._alignRight:
+ choice = choice.rjust(len(self._mask))
+ else:
+ choice = choice.ljust(len(self._mask))
+
+ if self._ctrl_constraints._choiceRequired:
+ choice = choice.lower().strip()
+ self._choices.append(choice)
+
+ if not self.IsValid(choice):
+ raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (self.name, choice))
+
+
+ wxComboBox.Append(self, choice)
+
+
+ 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.
+ """
+ self._choices = []
+ if self._ctrl_constraints._choices:
+ self.SetMaskParameters(choices=[])
+ wxComboBox.Clear(self)
+
+
+ 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('wxMaskedComboBox::GetMark', indent=1)
+ 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)
+
+ wxComboBox.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)
+ wxComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
+ wxComboBox.SetInsertionPoint(self, sel_start)
+ wxComboBox.SetMark(self, sel_start, sel_to)
+
+ self._ignoreChange = False # tell _OnTextChange() to pay attn again
+
+ dbg('computed selection:', sel_start, sel_to, indent=0)
+ dbg(suspend=0) # resume regular debugging
+ return sel_start, sel_to
+
+
+ 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('wxMaskedComboBox::OnSelectChoice', indent=1)
+
+ # force case-insensitive comparison for matching purposes:
+ value = self.GetValue().lower().strip()
+ if event.GetKeyCode() == WXK_UP:
+ direction = -1
+ else:
+ direction = 1
+ match_index = self._autoComplete(direction, self._choices, value, self._ctrl_constraints._compareNoCase)
+ if match_index is not None:
+ dbg('setting selection to', match_index)
+ self.SetSelection(match_index)
+ # issue appropriate event to outside:
+ self.GetEventHandler().ProcessEvent(
+ wxMaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
+ 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.SetSelection(0)
+ self.GetEventHandler().ProcessEvent(
+ wxMaskedComboBoxSelectEvent( self.GetId(), 0, self ) )
+ self._CheckValid()
+ keep_processing = False
+ else:
+ # attempt field-level auto-complete
+ dbg(indent=0)
+ keep_processing = self._OnAutoCompleteField(event)
+ dbg(indent=0)
+ return keep_processing
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class wxIpAddrCtrl( wxMaskedTextCtrl ):
+ """
+ This class is a particular type of wxMaskedTextCtrl 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 = wxDefaultPosition,
+ size = wxDefaultSize,
+ style = wxTE_PROCESS_TAB,
+ validator = wxDefaultValidator,
+ name = 'wxIpAddrCtrl',
+ 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}"
+
+ if not kwargs.has_key('emptyInvalid'):
+ kwargs['emptyInvalid'] = True
+
+ wxMaskedTextCtrl.__init__(
+ self, parent, id=id, value = value,
+ pos=pos, size=size,
+ style = style,
+ validator = validator,
+ name = name,
+ setupEventHandling = setupEventHandling,
+ **kwargs)
+
+ field_params = {}
+ if not kwargs.has_key('validRequired'):
+ field_params['validRequired'] = True
+
+ 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('wxIpAddrCtrl::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_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 = wxMaskedTextCtrl.GetValue(self)
+ return value.replace(' ','') # remove spaces from the value
+
+
+ def _OnCtrl_S(self, event):
+ dbg("wxIpAddrCtrl::_OnCtrl_S")
+ if self._demo:
+ print "value:", self.GetAddress()
+ return False
+
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+## these are helper subroutines:
+
+def movetodec( 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 = ("^##/##/####$","^####/##/##$","^##/##/#### ",
+ "^####/##/## ","^##-##-####$","^####-##-##$",
+ "^##-##-#### ","^####-##-## ")
+ 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 isDecimal( 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 ):
+ clip = dateStr[0:10]
+ 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(wxPySimpleApp):
+ def OnInit(self):
+ from wxPython.lib.rcsizer import RowColSizer
+ self.frame = wxFrame( NULL, -1, "wxMaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
+ self.panel = wxPanel( self.frame, -1)
+ self.sizer = RowColSizer()
+ self.labels = []
+ self.editList = []
+ rowcount = 4
+
+ id, id1 = wxNewId(), wxNewId()
+ self.command1 = wxButton( self.panel, id, "&Close" )
+ self.command2 = wxButton( self.panel, id1, "&AutoFormats" )
+ self.sizer.Add(self.command1, row=0, col=0, flag=wxALL, border = 5)
+ self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wxALL, border = 5)
+ EVT_BUTTON( self.panel, id, self.onClick )
+## self.panel.SetDefaultItem(self.command1 )
+ EVT_BUTTON( self.panel, id1, self.onClickPage )
+
+ self.check1 = wxCheckBox( self.panel, -1, "Disallow Empty" )
+ self.check2 = wxCheckBox( self.panel, -1, "Highlight Empty" )
+ self.sizer.Add( self.check1, row=0,col=3, flag=wxALL,border=5 )
+ self.sizer.Add( self.check2, row=0,col=4, flag=wxALL,border=5 )
+ EVT_CHECKBOX( self.panel, self.check1.GetId(), self._onCheck1 )
+ EVT_CHECKBOX( self.panel, self.check2.GetId(), self._onCheck2 )
+
+
+ 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 = wxStaticText( self.panel, -1, label)
+ self.label2 = wxStaticText( self.panel, -1, "Description")
+ self.label3 = wxStaticText( self.panel, -1, "Mask Value")
+ self.label4 = wxStaticText( self.panel, -1, "Format")
+ self.label5 = wxStaticText( self.panel, -1, "Reg Expr Val. (opt)")
+ self.label6 = wxStaticText( self.panel, -1, "wxMaskedEdit Ctrl")
+ self.label7 = wxStaticText( self.panel, -1, label2)
+ self.label7.SetForegroundColour("Blue")
+ self.label1.SetForegroundColour("Blue")
+ self.label2.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label3.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label4.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label5.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label6.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+
+ self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wxALL,border=5)
+ self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wxALL,border=5)
+ self.sizer.Add( self.label2, row=3,col=0, flag=wxALL,border=5)
+ self.sizer.Add( self.label3, row=3,col=1, flag=wxALL,border=5)
+ self.sizer.Add( self.label4, row=3,col=2, flag=wxALL,border=5)
+ self.sizer.Add( self.label5, row=3,col=3, flag=wxALL,border=5)
+ self.sizer.Add( self.label6, row=3,col=4, flag=wxALL,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( wxStaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wxALL)
+ self.sizer.Add( wxStaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wxALL)
+ self.sizer.Add( wxStaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wxALL)
+ self.sizer.Add( wxStaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wxALL)
+
+ if control in controls[:]:#-2]:
+ newControl = wxMaskedTextCtrl( 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.SetMaskParameters(choiceRequired = True)
+ else:
+ newControl = wxMaskedComboBox( 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=wxALL,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.Checked()
+ if value:
+ for control in self.editList:
+ control.SetMaskParameters(emptyInvalid=True)
+ control.Refresh()
+ else:
+ for control in self.editList:
+ control.SetMaskParameters(emptyInvalid=False)
+ control.Refresh()
+ self.panel.Refresh()
+
+ def _onCheck2(self,event):
+ """ Highlight empty values"""
+ value = event.Checked()
+ if value:
+ for control in self.editList:
+ control.SetMaskParameters( emptyBackgroundColor = 'Aquamarine')
+ control.Refresh()
+ else:
+ for control in self.editList:
+ control.SetMaskParameters( emptyBackgroundColor = 'White')
+ control.Refresh()
+ self.panel.Refresh()
+
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+class test2(wxFrame):
+ def __init__(self, parent, id, caption):
+ wxFrame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
+ from wxPython.lib.rcsizer import RowColSizer
+ self.panel = wxPanel( 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 = wxStaticText( self.panel, -1, label)
+ self.label2 = wxStaticText( self.panel, -1, "Description")
+ self.label3 = wxStaticText( self.panel, -1, "AutoFormat Code")
+ self.label4 = wxStaticText( self.panel, -1, "wxMaskedEdit Control")
+ self.label1.SetForegroundColour("Blue")
+ self.label2.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label3.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+ self.label4.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
+
+ self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wxALL,border=5)
+ self.sizer.Add( self.label2, row=3,col=0, flag=wxALL,border=5)
+ self.sizer.Add( self.label3, row=3,col=1, flag=wxALL,border=5)
+ self.sizer.Add( self.label4, row=3,col=2, flag=wxALL,border=5)
+
+ id, id1 = wxNewId(), wxNewId()
+ self.command1 = wxButton( self.panel, id, "&Close")
+ self.command2 = wxButton( self.panel, id1, "&Print Formats")
+ EVT_BUTTON( self.panel, id, self.onClick)
+ self.panel.SetDefaultItem(self.command1)
+ EVT_BUTTON( self.panel, id1, self.onClickPrint)
+
+ # 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)","MILTIMEHHMM"),
+ ("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 wxIpAddrCtrl)")
+ ]
+
+ for control in controls:
+ self.sizer.Add( wxStaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wxALL)
+ self.sizer.Add( wxStaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wxALL)
+ if control in controls[:-1]:
+ self.sizer.Add( wxMaskedTextCtrl( self.panel, -1, "",
+ autoformat = control[1],
+ demo = True),
+ row=rowcount,col=2,flag=wxALL,border=5)
+ else:
+ self.sizer.Add( wxIpAddrCtrl( self.panel, -1, "", demo=True ),
+ row=rowcount,col=2,flag=wxALL,border=5)
+ rowcount += 1
+
+ self.sizer.Add(self.command1, row=0, col=0, flag=wxALL, border = 5)
+ self.sizer.Add(self.command2, row=0, col=1, flag=wxALL, 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][0], masktags[format][3])
+
+## ---------- ---------- ---------- ---------- ---------- ---------- ----------
+
+if __name__ == "__main__":
+ app = test()
+
+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: wxMaskedComboBox 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, wxMaskedComboBox
+## doesn't do the nice field-tabbing that wxMaskedTextCtrl 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 wxMaskedComboBox
+## 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 wxMaskedTextCtrl 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 wxMaskedComboBox.
+
+
+
+## CHANGELOG:
+## ====================
+## 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 wxIpAddrCtrl 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 wxTimeCtrl 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 wxMaskedComboBox.
+## 10. Fixed bug in wxIpAddrCtrl causing traceback if field value was of
+## the form '# #'. Modified control code for wxIpAddrCtrl 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 wxMaskedTextCtrl and wxMaskedComboBox,
+## now included.
+## 11. wxMaskedComboBox 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 wxIpAddrCtrl 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. wxIpAddrCtrl.)
+## 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 wxMaskedTextCtrl.
+## 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/wxPython/lib/timectrl.py b/wxPython/wxPython/lib/timectrl.py
index 52d9c4b4fc..82772a92de 100644
--- a/wxPython/wxPython/lib/timectrl.py
+++ b/wxPython/wxPython/lib/timectrl.py
@@ -18,43 +18,240 @@
# cursor-position specific, so the control intercepts the key codes before the
# validator would fire.
#
+# wxTimeCtrl 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.")
+#
+"""
+
+wxTimeCtrl 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.
+
+Left/right/tab keys to switch cells within a wxTimeCtrl, and the up/down arrows act
+like a spin control. wxTimeCtrl also allows for an actual spin button to be attached
+to the control, so that it acts like the up/down arrow keys.
+
+The ! or c key sets the value of the control to the current time.
+
+Here's the API for wxTimeCtrl:
+
+ wxTimeCtrl(
+ parent, id = -1,
+ value = '12:00:00 AM',
+ pos = wxDefaultPosition,
+ size = wxDefaultSize,
+ style = wxTE_PROCESS_TAB,
+ validator = wxDefaultValidator,
+ name = "time",
+ fmt24hr = False,
+ spinButton = None,
+ min = None,
+ max = None,
+ limited = None,
+ oob_color = "Yellow"
+)
+
+
+ - value
+
- 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.)
+
size
+ - The size of the control will be automatically adjusted for 12/24 hour format
+ if wxDefaultSize is specified.
+
- style
+
- By default, wxTimeCtrl will process TAB events, by allowing tab to the
+ different cells within the control.
+
- validator
+
- By default, wxTimeCtrl 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.
+
+ - fmt24hr
+
- 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.
+
+ - spinButton
+
- If specified, this button's events will be bound to the behavior of the
+ wxTimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
+
+ - min
+
- Defines the lower bound for "valid" selections in the control.
+ By default, wxTimeCtrl 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.
+
- max
+
- 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. min = 11pm, max= 10pm
+ means all but the hour from 10:00pm to 11pm are valid times.)
+
- limited
+
- If True, the control will not permit entry of values that fall outside the
+ set bounds.
+
+ - oob_color
+
- 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.
+
+
+
+
+
+- EVT_TIMEUPDATE(win, id, func)
+
- func is fired whenever the value of the control changes.
+
+
+ - SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)
+
- Sets the value of the control to a particular time, given a valid
+value; raises ValueError on invalid value.
+NOTE: This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
+was successfully imported by the class module.
+
+ - GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)
+
- 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.
+
+ - GetWxDateTime(value=None)
+
- 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.
+
+ - GetMxDateTime()
+
- 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.)
+
+
+ - BindSpinButton(wxSpinBtton)
+
- 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.)
+
+
+ - SetMin(min=None)
+
- 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 None 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.
+
- 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.
+
+ - GetMin(as_string=False)
+
- 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.
+
+
+ - SetMax(max=None)
+
- 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 None 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.
+
- 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.
+
+ - GetMax(as_string = False)
+
- 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.
+
+
+
+ - SetBounds(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, and result in the behavior of
+an unbounded control.
+
+ - GetBounds(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. 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.
+
+
+ - IsInBounds(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. It will raise ValueError if the value
+specified is not a wxDateTime, mxDateTime (if available) or parsable string.
+
+
+ - IsValid(value)
+
- Returns Trueif specified value is a legal time value and
+falls within the current bounds of the given control.
+
+
+ - SetLimited(bool)
+
- 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.
+
- IsLimited()
+
- Returns True if the control is currently limiting the
+value to fall within the current bounds.
+
+
+
+"""
+
+import string, copy
from wxPython.wx import *
-import string
-
-# wxWindows' wxTextCtrl translates Composite "control key"
-# events into single events before returning them to its OnChar
-# routine. The doc says that this results in 1 for Ctrl-A, 2 for
-# Ctrl-B, etc. However, there are no wxPython or wxWindows
-# symbols for them, so I'm defining codes for Ctrl-X (cut) and
-# Ctrl-V (paste) here for readability:
-WXK_CTRL_X = (ord('X')+1) - ord('A')
-WXK_CTRL_V = (ord('V')+1) - ord('A')
-
-# The following bit of function is for debugging the subsequent code.
-# To turn on debugging output, set _debug to 1
-_debug = 0
-_indent = 0
-
-if _debug:
- def _dbg(*args, **kwargs):
- global _indent
-
- if len(args):
- if _indent: print ' ' * 3 * _indent,
- for arg in args: print arg,
- print
- # else do nothing
-
- # post process args:
- for kwarg, value in kwargs.items():
- if kwarg == 'indent' and value: _indent = _indent + 1
- elif kwarg == 'indent' and value == 0: _indent = _indent - 1
- if _indent < 0: _indent = 0
-else:
- def _dbg(*args, **kwargs):
- pass
+from wxPython.tools.dbg import Logger
+from wxPython.lib.maskededit import wxMaskedTextCtrl, Field
+import wxPython.utils
+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 = wxNewId()
@@ -71,98 +268,173 @@ class TimeUpdatedEvent(wxPyCommandEvent):
return self.value
-# Set up all the positions of the cells in the wxTimeCtrl (once at module import):
-# Format of control is:
-# hh:mm:ss xM
-# 1
-# positions: 01234567890
-_listCells = ['hour', 'minute', 'second', 'am_pm']
-_listCellRange = [(0,1,2), (3,4,5), (6,7,8), (9,10,11)]
-_listDelimPos = [2,5,8]
+class wxTimeCtrl(wxMaskedTextCtrl):
-# Create dictionary of cell ranges, indexed by name or position in the range:
-_dictCellRange = {}
-for i in range(4):
- _dictCellRange[_listCells[i]] = _listCellRange[i]
-for cell in _listCells:
- for i in _dictCellRange[cell]:
- _dictCellRange[i] = _dictCellRange[cell]
+ valid_ctrl_params = {
+ 'display_seconds' : 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 wxMaskedTextCtrl "invalid" color
+ }
-
-# Create lists of starting and ending positions for each range, and a dictionary of starting
-# positions indexed by name
-_listStartCellPos = []
-_listEndCellPos = []
-for tup in _listCellRange:
- _listStartCellPos.append(tup[0]) # 1st char of cell
- _listEndCellPos.append(tup[1]) # last char of cell (not including delimiter)
-
-_dictStartCellPos = {}
-for i in range(4):
- _dictStartCellPos[_listCells[i]] = _listStartCellPos[i]
-
-
-class wxTimeCtrl(wxTextCtrl):
def __init__ (
self, parent, id=-1, value = '12:00:00 AM',
pos = wxDefaultPosition, size = wxDefaultSize,
- fmt24hr=0,
+ fmt24hr=False,
spinButton = None,
- style = wxTE_PROCESS_TAB, name = "time"
- ):
- wxTextCtrl.__init__(self, parent, id, value='',
- pos=pos, size=size, style=style, name=name)
+ style = wxTE_PROCESS_TAB,
+ validator = wxDefaultValidator,
+ name = "time",
+ **kwargs ):
+ # set defaults for control:
+ dbg('setting defaults:')
+ for key, param_value in wxTimeCtrl.valid_ctrl_params.items():
+ # This is done this way to make setattr behave consistently with
+ # "private attribute" name mangling
+ setattr(self, "_wxTimeCtrl__" + 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 positional args (from original release) differently from rest of kwargs:)
self.__fmt24hr = fmt24hr
- if size == wxDefaultSize:
- # set appropriate default sizes depending on format:
- if self.__fmt24hr:
- testText = '00:00:00'
- else:
- testText = '00:00:00 MM'
- _dbg(wxPlatform)
+ maskededit_kwargs = {}
- if wxPlatform != "__WXMSW__": # give it a little extra space
- testText += 'M'
- if wxPlatform == "__WXMAC__": # give it even a little more...
- testText += 'M'
+ # assign keyword args as appropriate:
+ for key, param_value in kwargs.items():
+ if key not in wxTimeCtrl.valid_ctrl_params.keys():
+ raise AttributeError('invalid keyword argument "%s"' % key)
- w, h = self.GetTextExtent(testText)
- self.SetClientSize( (w+4, self.GetClientSize().height) )
+ if key == "display_seconds":
+ self.__display_seconds = param_value
+
+ 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 self.__fmt24hr:
+ if self.__display_seconds: maskededit_kwargs['autoformat'] = 'MILTIMEHHMMSS'
+ else: maskededit_kwargs['autoformat'] = 'MILTIMEHHMM'
+
+ # 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" % self.GetValue()
-
def SetValue(self, value):
"""
- Validating SetValue function for time strings, doing 12/24 format conversion as appropriate.
+ 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('wxTimeCtrl::SetValue', indent=1)
- dict_range = _dictCellRange # (for brevity)
- dict_start = _dictStartCellPos
-
- fmt12len = dict_range['am_pm'][-1]
- fmt24len = dict_range['second'][-1]
+ dbg('wxTimeCtrl::SetValue(%s)' % repr(value), indent=1)
try:
- separators_correct = value[2] == ':' and value[5] == ':'
- len_ok = len(value) in (fmt12len, fmt24len)
+ strtime = self._toGUI(self.__validateValue(value))
+ except:
+ dbg('validation failed', indent=0)
+ raise
- if len(value) > fmt24len:
- separators_correct = separators_correct and value[8] == ' '
- hour = int(value[dict_range['hour'][0]:dict_range['hour'][-1]])
- hour_ok = ((hour in range(0,24) and len(value) == fmt24len)
- or (hour in range(1,13) and len(value) == fmt12len
- and value[dict_start['am_pm']:] in ('AM', 'PM')))
+ dbg('strtime:', strtime)
+ self._SetValue(strtime)
+ dbg(indent=0)
- minute = int(value[dict_range['minute'][0]:dict_range['minute'][-1]])
- min_ok = minute in range(60)
- second = int(value[dict_range['second'][0]:dict_range['second'][-1]])
- sec_ok = second in range(60)
+ def GetValue(self,
+ as_wxDateTime = False,
+ as_mxDateTime = False,
+ as_wxTimeSpan = False,
+ as_mxDateTimeDelta = False):
- _dbg('len_ok =', len_ok, 'separators_correct =', separators_correct)
- _dbg('hour =', hour, 'hour_ok =', hour_ok, 'min_ok =', min_ok, 'sec_ok =', sec_ok)
- if len_ok and hour_ok and min_ok and sec_ok and separators_correct:
- _dbg('valid time string')
+ 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 = wxTimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
+ elif as_mxDateTimeDelta:
+ value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
+ else:
+ value = wxMaskedTextCtrl.GetValue(self)
+ return value
- self.__hour = hour
- if len(value) == fmt12len: # handle 12 hour format conversion for actual hour:
- am = value[dict_start['am_pm']:] == 'AM'
- if hour != 12 and not am:
- self.__hour = hour = (hour+12) % 24
- elif hour == 12:
- if am: self.__hour = hour = 0
-
- self.__minute = minute
- self.__second = second
-
- # valid time
- need_to_convert = ((self.__fmt24hr and len(value) == fmt12len)
- or (not self.__fmt24hr and len(value) == fmt24len))
- _dbg('need_to_convert =', need_to_convert)
-
- if need_to_convert: #convert to 12/24 hour format as specified:
- if self.__fmt24hr and len(value) == fmt12len:
- text = '%.2d:%.2d:%.2d' % (hour, minute, second)
- else:
- if hour > 12:
- hour = hour - 12
- am_pm = 'PM'
- elif hour == 12:
- am_pm = 'PM'
- else:
- if hour == 0: hour = 12
- am_pm = 'AM'
- text = '%2d:%.2d:%.2d %s' % (hour, minute, second, am_pm)
- else:
- text = value
- _dbg('text=', text)
- wxTextCtrl.SetValue(self, text)
- _dbg('firing TimeUpdatedEvent...')
- evt = TimeUpdatedEvent(self.GetId(), text)
- evt.SetEventObject(self)
- self.GetEventHandler().ProcessEvent(evt)
- else:
- _dbg('len_ok:', len_ok, 'separators_correct =', separators_correct)
- _dbg('hour_ok:', hour_ok, 'min_ok:', min_ok, 'sec_ok:', sec_ok, indent=0)
- raise ValueError, 'value is not a valid time string'
-
- except (TypeError, ValueError):
- _dbg(indent=0)
- raise ValueError, 'value is not a valid time string'
- _dbg(indent=0)
def SetWxDateTime(self, wxdt):
- value = '%2d:%.2d:%.2d' % (wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
- self.SetValue(value)
+ """
+ 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 wxTimeCtrl; 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('wxTimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
+ if value is None:
+ dbg('getting control value')
+ value = self.GetValue()
+ dbg('value = "%s"' % value)
+
+ valid = True # assume true
+ if type(value) == types.StringType:
+
+ # Construct constant wxDateTime, then try to parse the string:
+ wxdt = wxDateTimeFromDMY(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, wxPython.utils.wxDateTimePtr):
+ hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
+ elif isinstance(value, wxPython.utils.wxTimeSpanPtr):
+ 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 self.__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 = wxDateTimeFromDMY(1, 0, 1970)
+ wxdt.SetHour(hour)
+ wxdt.SetMinute(minute)
+ wxdt.SetSecond(second)
+
+ dbg('wxdt:', wxdt, indent=0, suspend=0)
+ return wxdt
- def GetWxDateTime(self):
- t = wxDateTimeFromHMS(self.__hour, self.__minute, self.__second)
- return t
def SetMxDateTime(self, mxdt):
- from mx import DateTime
- value = '%2d:%.2d:%.2d' % (mxdt.hour, mxdt.minute, mxdt.second)
+ """
+ Because SetValue can take an mxDateTime, (if DateTime is importable),
+ this is now just an alias.
+ """
self.SetValue(value)
- def GetMxDateTime(self):
- from mx import DateTime
- t = DateTime.Time(self.__hour, self.__minute, self.__second)
+
+ def GetMxDateTime(self, value=None):
+ if value is None:
+ value = self.GetValue()
+ t = DateTime.DateTime(1970,1,1) + DateTime.Parser.TimeFromString(value)
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('wxTimeCtrl::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('wxTimeCtrl::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('wxTimeCtrl::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('wxTimeCtrl::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('wxTimeCtrl::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 + wxTimeSpan(24, 0, 0, 0)) - max
+
+ half_interval = wxTimeSpan(
+ 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 + wxTimeSpan(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('wxTimeCtrl::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 = wxDateTimeFromDMY(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
+
+
#-------------------------------------------------------------------------------------------------------------
# these are private functions and overrides:
- def __SetCurrentCell(self, pos):
- """
- Sets state variables that indicate the current cell and position within the control.
- """
- self.__posCurrent = pos
- self.__cellStart, self.__cellEnd = _dictCellRange[pos][0], _dictCellRange[pos][-1]
+
+ def __OnTextChange(self, event=None):
+ dbg('wxTimeCtrl::OnTextChange', indent=1)
+
+ # Allow wxMaskedtext 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 wxMaskedTextCtrl._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('wxTimeCtrl::SetInsertionPoint', pos, indent=1)
-
- # Adjust pos to legal value if not already
- if pos < 0: pos = 0
- elif pos in _listDelimPos + [_dictCellRange[self.__lastCell]]:
- pos = pos - 1
- if self.__lastCell == 'am_pm' and pos in _dictCellRange[self.__lastCell]:
- pos = _dictStartCellPos[self.__lastCell]
-
- self.__SetCurrentCell(pos)
- wxTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
- _dbg(indent=0)
+ dbg('wxTimeCtrl::SetInsertionPoint', pos, indent=1)
+ wxMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
+ self.__posCurrent = self.GetInsertionPoint()
+ dbg(indent=0)
def SetSelection(self, sel_start, sel_to):
- _dbg('wxTimeCtrl::SetSelection', sel_start, sel_to, indent=1)
+ dbg('wxTimeCtrl::SetSelection', sel_start, sel_to, indent=1)
# Adjust selection range to legal extent if not already
if sel_start < 0:
- self.SetInsertionPoint(0)
- sel_start = self.__posCurrent
-
- elif sel_start in _listDelimPos + [_dictCellRange[self.__lastCell]]:
- self.SetInsertionPoint(sel_start - 1)
- sel_start = self.__posCurrent
+ sel_start = 0
if self.__posCurrent != sel_start: # force selection and insertion point to match
self.SetInsertionPoint(sel_start)
-
- if sel_to not in _dictCellRange[sel_start]:
- sel_to = _dictCellRange[sel_start][-1] # limit selection to end of current cell
+ 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
- self.__posSelectTo = sel_to
- wxTextCtrl.SetSelection(self, sel_start, sel_to)
- _dbg(indent=0)
+ wxMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
+ dbg(indent=0)
- 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('wxTimeCtrl::OnFocus')
- wxCallAfter(self.__FixSelection)
- event.Skip()
-
-
- 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('wxTimeCtrl::FixSelection', indent=1)
- sel_start, sel_to = self.GetSelection()
- if sel_start == 0 and sel_to in _dictCellRange[self.__lastCell]:
- # 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.SetInsertionPoint(0)
- self.SetSelection(self.__cellStart, self.__cellEnd)
- _dbg(indent=0)
-
-
- def __OnTextChange(self, event):
- """
- This private event handler is required to retain the current position information of the cursor
- after update to the underlying text control is done.
- """
- _dbg('wxTimeCtrl::OnTextChange', indent=1)
- self.__SetCurrentCell(self.__posCurrent) # ensure cell range vars are set
-
- # Note: must call self.SetSelection here to preserve insertion point cursor after update!
- # (I don't know why, but this does the trick!)
- if self.__bSelection:
- _dbg('reselecting from ', self.__posCurrent, 'to', self.__posSelectTo)
- self.SetSelection(self.__posCurrent, self.__posSelectTo)
- else:
- self.SetSelection(self.__posCurrent, self.__posCurrent)
- event.Skip()
- _dbg(indent=0)
-
def __OnSpin(self, key):
- self.__IncrementValue(key, self.__posCurrent)
+ """
+ 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()
- self.SetInsertionPoint(self.__posCurrent)
- self.SetSelection(self.__posCurrent, self.__posSelectTo)
+ 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('wxTimeCtrl::OnSpinUp', indent=1)
+ dbg('wxTimeCtrl::OnSpinUp', indent=1)
self.__OnSpin(WXK_UP)
- _dbg(indent=0)
+ keep_processing = False
+ dbg(indent=0)
+ return keep_processing
def __OnSpinDown(self, event):
@@ -406,381 +956,146 @@ class wxTimeCtrl(wxTextCtrl):
Event handler for any bound spin button on EVT_SPIN_DOWN;
causes control to behave as if down arrow was pressed.
"""
- _dbg('wxTimeCtrl::OnSpinDown', indent=1)
+ dbg('wxTimeCtrl::OnSpinDown', indent=1)
self.__OnSpin(WXK_DOWN)
- _dbg(indent=0)
-
-
- def __OnChangePos(self, event):
- """
- Event handler for motion events; this handler
- changes limits the selection to the new cell boundaries.
- """
- _dbg('wxTimeCtrl::OnChangePos', indent=1)
- pos = self.GetInsertionPoint()
- self.__SetCurrentCell(pos)
- sel_start, sel_to = self.GetSelection()
- selection = sel_start != sel_to
- if not selection:
- # disallow position at end of field:
- if pos in _listDelimPos + [_dictCellRange[self.__lastCell][-1]]:
- self.SetInsertionPoint(pos-1)
- self.__posSelectTo = self.__cellEnd
- else:
- # only allow selection to end of current cell:
- if sel_to < pos: self.__posSelectTo = self.__cellStart
- elif sel_to > pos: self.__posSelectTo = self.__cellEnd
-
- _dbg('new pos =', self.__posCurrent, 'select to ', self.__posSelectTo)
- self.SetSelection(self.__posCurrent, self.__posSelectTo)
- if event: event.Skip()
- _dbg(indent=0)
-
-
- def __OnDoubleClick(self, event):
- """
- Event handler for left double-click mouse events; this handler
- causes the cell at the double-click point to be selected.
- """
- _dbg('wxTimeCtrl::OnDoubleClick', indent=1)
- pos = self.GetInsertionPoint()
- self.__SetCurrentCell(pos)
- if self.__posCurrent != self.__cellStart:
- self.SetInsertionPoint(self.__cellStart)
- self.SetSelection(self.__cellStart, self.__cellEnd)
- _dbg(indent=0)
+ keep_processing = False
+ dbg(indent=0)
+ return keep_processing
def __OnChar(self, event):
"""
- This private event handler is the main control point for the wxTimeCtrl.
- It governs whether input characters are accepted and if so, handles them
- so as to provide appropriate cursor and selection behavior for the control.
+ 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('wxTimeCtrl::OnChar', indent=1)
+ dbg('wxTimeCtrl::OnChar', indent=1)
+ keycode = event.GetKeyCode()
+ dbg('keycode:', keycode)
+ if keycode == ord(':'):
+ dbg('colon seen! removing shift attribute')
+ event.m_shiftDown = False
+ wxMaskedTextCtrl._OnChar(self, event ) ## handle each keypress
+ dbg(indent=0)
- # NOTE: Returning without calling event.Skip() eats the event before it
- # gets to the text control...
- key = event.GetKeyCode()
- text = self.GetValue()
+ 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(wxDateTime_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('wxTimeCtrl::LimitSelection', indent=1)
pos = self.GetInsertionPoint()
- if pos != self.__posCurrent:
- _dbg("insertion point has moved; resetting current cell")
- self.__SetCurrentCell(pos)
- self.SetSelection(self.__posCurrent, self.__posCurrent)
-
+ self.__posCurrent = pos
sel_start, sel_to = self.GetSelection()
selection = sel_start != sel_to
- _dbg('sel_start=', sel_start, 'sel_to =', sel_to)
- if not selection:
- self.__bSelection = False # predict unselection of entire region
+ 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('keycode = ', key)
- _dbg('pos = ', pos)
-
- # don't allow deletion, cut or paste:
- if key in (WXK_DELETE, WXK_BACK, WXK_CTRL_X, WXK_CTRL_V):
- pass
-
- elif key == WXK_TAB: # skip to next field if applicable:
- _dbg('key == WXK_TAB')
- dict_range = _dictCellRange # (for brevity)
- dict_start = _dictStartCellPos
- if event.ShiftDown(): # tabbing backwords
-
- ###(NOTE: doesn't work; wxTE_PROCESS_TAB doesn't appear to send us this event!)
-
- _dbg('event.ShiftDown()')
- if pos in dict_range['hour']: # already in 1st field
- self.__SetCurrentCell(dict_start['hour']) # ensure we have our member vars set
- event.Skip() #then do normal tab processing for the form
- _dbg(indent=0)
- return
-
- elif pos in dict_range['minute']: # skip to hours field
- new_pos = dict_start['hour']
- elif pos in dict_range['second']: # skip to minutes field
- new_pos = dict_start['minute']
- elif pos in dict_range['am_pm']: # skip to seconds field
- new_pos = dict_start['second']
-
- self.SetInsertionPoint(new_pos) # force insert point to jump to next cell (swallowing TAB)
- self.__OnChangePos(None) # update selection accordingly
-
- else:
- # Tabbing forwards through control...
-
- if pos in dict_range[self.__lastCell]: # already in last field; ensure we have our members set
- self.__SetCurrentCell(dict_start[self.__lastCell])
- _dbg('tab in last cell')
- event.Skip() # then do normal tab processing for the form
- _dbg(indent=0)
- return
-
- if pos in dict_range['second']: # skip to AM/PM field (if not last cell)
- new_pos = dict_start['am_pm']
- elif pos in dict_range['minute']: # skip to seconds field
- new_pos = dict_start['second']
- elif pos in dict_range['hour']: # skip to minutes field
- new_pos = dict_start['minute']
-
- self.SetInsertionPoint(new_pos) # force insert point to jump to next cell (swallowing TAB)
- self.__OnChangePos(None) # update selection accordingly
-
- elif key == WXK_LEFT: # move left; set insertion point as appropriate:
- _dbg('key == WXK_LEFT')
- if event.ShiftDown(): # selecting a range...
- _dbg('event.ShiftDown()')
- if pos in _listStartCellPos: # can't select pass delimiters
- if( sel_to == pos+2 and sel_to != _dictCellRange['am_pm'][-1]):
- self.SetSelection(pos, pos+1) # allow deselection of 2nd char in cell if not am/pm
- # else ignore event
-
- elif pos in _listEndCellPos: # can't use normal selection, because position ends up
- # at delimeter
- _dbg('set selection from', pos-1, 'to', self.__posCurrent)
- self.SetInsertionPoint(pos-1) # this selects the previous position
- self.SetSelection(self.__posCurrent, pos)
- else:
- self.SetInsertionPoint(sel_to - 1) # this unselects the last digit
- self.SetSelection(self.__posCurrent, pos)
-
- else: # ... not selecting
- if pos == 0: # can't position before position 0
- pass
- elif pos in _listStartCellPos: # skip (left) OVER the colon/space:
- self.SetInsertionPoint(pos-2)
- self.__OnChangePos(None) # set the selection appropriately
- else:
- self.SetInsertionPoint(pos-1) # reposition the cursor and
- self.__OnChangePos(None) # set the selection appropriately
-
-
- elif key == WXK_RIGHT: # move right
- _dbg('key == WXK_RIGHT')
- if event.ShiftDown():
- _dbg('event.ShiftDown()')
- if sel_to in _listDelimPos: # can't select pass delimiters
- pass
- else:
- self.SetSelection(self.__posCurrent, sel_to+1)
- else:
- if( (self.__lastCell == 'second'
- and pos == _dictStartCellPos['second']+1)
- or (self.__lastCell == 'am_pm'
- and pos == _dictStartCellPos['am_pm']) ):
- pass # don't allow cursor past last cell
- elif pos in _listEndCellPos: # skip (right) OVER the colon/space:
- self.SetInsertionPoint(pos+2)
- self.__OnChangePos(None) # set the selection appropriately
- else:
- self.SetInsertionPoint(pos+1) # reposition the cursor and
- self.__OnChangePos(None) # set the selection appropriately
-
- elif key in (WXK_UP, WXK_DOWN):
- _dbg('key in (WXK_UP, WXK_DOWN)')
- self.__IncrementValue(key, pos) # increment/decrement as appropriate
-
-
- elif key < WXK_SPACE or key == WXK_DELETE or key > 255:
- event.Skip() # non alphanumeric; process normally (Right thing to do?)
-
- elif chr(key) in ['!', 'c', 'C']: # Special character; sets the value of the control to "now"
- _dbg("key == '!'; setting time to 'now'")
- now = wxDateTime_Now()
- self.SetWxDateTime(now)
-
- elif chr(key) in string.digits: # let ChangeValue validate and update current position
- self.__ChangeValue(chr(key), pos) # handle event (and swallow it)
-
- elif chr(key) in ('a', 'A', 'p', 'P', ' '): # let ChangeValue validate and update current position
- self.__ChangeValue(chr(key), pos) # handle event (and swallow it)
-
- else: # disallowed char; swallow event
- pass
- _dbg(indent=0)
+ 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('wxTimeCtrl::IncrementValue', key, pos, indent=1)
+ dbg('wxTimeCtrl::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 == WXK_UP: increment = 1
+ else: increment = -1
- sel_start, sel_to = self.GetSelection()
- selection = sel_start != sel_to
- cell_selected = selection and sel_to -1 != pos
+ if slice in ('A', 'P'):
+ if slice == 'A': newslice = 'P'
+ elif slice == 'P': newslice = 'A'
+ newvalue = text[:start] + newslice + text[end:]
- dict_start = _dictStartCellPos # (for brevity)
+ 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 = wxDateTimeFromDMY(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()
- # Determine whether we should change the entire cell or just a portion of it:
- if( cell_selected
- or (pos in _listStartCellPos and not selection)
- or (text[pos] == ' ' and text[pos+1] not in ('1', '2'))
- or (text[pos] == '9' and text[pos-1] == ' ' and key == WXK_UP)
- or (text[pos] == '1' and text[pos-1] == ' ' and key == WXK_DOWN)
- or pos >= dict_start['am_pm']):
+ else: # minute or second field; handled the same way:
+ newslice = "%02d" % ((int(slice) + increment) % 60)
+ newvalue = text[:start] + newslice + text[end:]
- self.__IncrementCell(key, pos)
+ try:
+ self.SetValue(newvalue)
+
+ except ValueError: # must not be in bounds:
+ if not wxValidator_IsSilent():
+ wxBell()
+ 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.__display_seconds: strval = wxdt.Format('%H:%M:%S')
+ else: strval = wxdt.Format('%H:%M')
else:
- if key == WXK_UP: inc = 1
- else: inc = -1
+ if self.__display_seconds: strval = wxdt.Format('%I:%M:%S %p')
+ else: strval = wxdt.Format('%I:%M %p')
- if pos == dict_start['hour'] and not self.__fmt24hr:
- if text[pos] == ' ': digit = '1' # allow ' ' or 1 for 1st digit in 12hr format
- else: digit = ' '
- else:
- if pos == dict_start['hour']:
- if int(text[pos + 1]) >3: mod = 2 # allow for 20-23
- else: mod = 3 # allow 00-19
- elif pos == dict_start['hour'] + 1:
- if self.__fmt24hr:
- if text[pos - 1] == '2': mod = 4 # allow hours 20-23
- else: mod = 10 # allow hours 00-19
- else:
- if text[pos - 1] == '1': mod = 3 # allow hours 10-12
- else: mod = 10 # allow 0-9
-
- elif pos in (dict_start['minute'],
- dict_start['second']): mod = 6 # allow minutes/seconds 00-59
- else: mod = 10
-
- digit = '%d' % ((int(text[pos]) + inc) % mod)
-
- _dbg("new digit = \'%s\'" % digit)
- self.__ChangeValue(digit, pos)
- _dbg(indent=0)
+ return strval
- def __IncrementCell(self, key, pos):
- _dbg('wxTimeCtrl::IncrementCell', key, pos, indent=1)
- self.__SetCurrentCell(pos) # determine current cell
- hour, minute, second = self.__hour, self.__minute, self.__second
- text = self.GetValue()
- dict_start = _dictStartCellPos # (for brevity)
- if key == WXK_UP: inc = 1
- else: inc = -1
-
- if self.__cellStart == dict_start['am_pm']:
- am = text[dict_start['am_pm']:] == 'AM'
- if am: hour = hour + 12
- else: hour = hour - 12
- else:
- if self.__cellStart == dict_start['hour']:
- hour = (hour + inc) % 24
- elif self.__cellStart == dict_start['minute']:
- minute = (minute + inc) % 60
- elif self.__cellStart == dict_start['second']:
- second = (second + inc) % 60
-
- newvalue = '%.2d:%.2d:%.2d' % (hour, minute, second)
-
- self.SetValue(newvalue)
- self.SetInsertionPoint(self.__cellStart)
- self.SetSelection(self.__cellStart, self.__cellEnd)
- _dbg(indent=0)
-
-
- def __ChangeValue(self, char, pos):
- _dbg('wxTimeCtrl::ChangeValue', "\'" + char + "\'", pos, indent=1)
- text = self.GetValue()
-
- self.__SetCurrentCell(pos)
- sel_start, sel_to = self.GetSelection()
- self.__posSelectTo = sel_to
- self.__bSelection = selection = sel_start != sel_to
- cell_selected = selection and sel_to -1 != pos
- _dbg('cell_selected =', cell_selected, indent=0)
-
- dict_start = _dictStartCellPos # (for brevity)
-
- if pos in _listDelimPos: return # don't allow change of punctuation
-
- elif( 0 < pos < dict_start['am_pm'] and char not in string.digits):
- return # AM/PM not allowed in this position
-
- # See if we're changing the hour cell, and validate/update appropriately:
- #
- hour_start = dict_start['hour'] # (ie. 0)
-
- if pos == hour_start: # if at 1st position,
- if self.__fmt24hr: # and using 24 hour format
- if cell_selected: # replace cell contents with hour represented by digit
- newtext = '%.2d' % int(char) + text[hour_start+2:]
- elif char not in ('0', '1', '2'): # return if digit not 0,1, or 2
- return
- else: # relace current position
- newtext = char + text[pos+1:]
- else: # (12 hour format)
- if cell_selected:
- if char == ' ': return # can't erase entire cell
- elif char == '0': # treat 0 as '12'
- newtext = '12' + text[hour_start+2:]
- else: # replace cell contents with hour represented by digit
- newtext = '%2d' % int(char) + text[hour_start+2:]
- else:
- if char not in ('1', ' '): # can only type a 1 or space
- return
- if text[pos+1] not in ('0', '1', '2'): # and then, only if other column is 0,1, or 2
- return
- if char == ' ' and text[pos+1] == '0': # and char isn't space if 2nd column is 0
- return
- else: # ok; replace current position
- newtext = char + text[pos+1:]
- if char == ' ': self.SetInsertionPoint(pos+1) # move insert point to legal position
-
- elif pos == hour_start+1: # if editing 2nd position of hour
- if( not self.__fmt24hr # and using 12 hour format
- and text[hour_start] == '1' # if 1st char is 1,
- and char not in ('0', '1', '2')): # disallow anything bug 0,1, or 2
- return
- newtext = text[hour_start] + char + text[hour_start+2:] # else any digit ok
-
- # Do the same sort of validation for minute and second cells
- elif pos in (dict_start['minute'], dict_start['second']):
- if cell_selected: # if cell selected, replace value
- newtext = text[:pos] + '%.2d' % int(char) + text[pos+2:]
- elif int(char) > 5: return # else disallow > 59 for minute and second fields
- else:
- newtext = text[:pos] + char + text[pos+1:] # else ok
-
- elif pos in (dict_start['minute']+1, dict_start['second']+1):
- newtext = text[:pos] + char + text[pos+1:] # all digits ok for 2nd digit of minute/second
-
- # Process AM/PM cell
- elif pos == dict_start['am_pm']:
- char = char.upper()
- if char not in ('A','P'): return # disallow all but A or P as 1st char of column
- newtext = text[:pos] + char + text[pos+1:]
- else: return # not a valid position
-
- _dbg(indent=1)
- # update member position vars and set selection to character changed
- if not cell_selected:
- _dbg('reselecting current digit')
- self.__posSelectTo = pos+1
-
- _dbg('newtext=', newtext)
- self.SetValue(newtext)
- self.SetInsertionPoint(self.__posCurrent)
- self.SetSelection(self.__posCurrent, self.__posSelectTo)
- _dbg(indent=0)
-
-
- def Cut(self):
+ def __validateValue( self, value ):
"""
- Override wxTextCtrl::Cut() method, as this operation should not
- be allowed for wxTimeCtrls.
+ 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.
"""
- return
+ dbg('wxTimeCtrl::__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
- def Paste(self):
- """
- Override wxTextCtrl::Paste() method, as this operation should not
- be allowed for wxTimeCtrls.
- """
- return
-
+ 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 wxTimeCtrl:
@@ -814,12 +1129,12 @@ if __name__ == '__main__':
EVT_TIMEUPDATE(self, self.tc.GetId(), self.OnTimeChange)
def OnTimeChange(self, event):
- _dbg('OnTimeChange: value = ', event.GetValue())
+ dbg('OnTimeChange: value = ', event.GetValue())
wxdt = self.tc.GetWxDateTime()
- _dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
+ dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
if self.test_mx:
mxdt = self.tc.GetMxDateTime()
- _dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
+ dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
class MyApp(wxApp):