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")
-
- -
-
-
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):