From: Robin Dunn Date: Mon, 19 Apr 2004 23:24:37 +0000 (+0000) Subject: Added new MaskedEditControl code from Will Sadkin. The modules are X-Git-Url: https://git.saurik.com/wxWidgets.git/commitdiff_plain/c878ceeae8d69f231477ef0f207766093547ab86 Added new MaskedEditControl code from Will Sadkin. The modules are now locaed in their own sub-package, wx.lib.masked. Demos updated. git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@26874 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- diff --git a/wxPython/demo/MaskedEditControls.py b/wxPython/demo/MaskedEditControls.py index c20988837a..b88f53b07e 100644 --- a/wxPython/demo/MaskedEditControls.py +++ b/wxPython/demo/MaskedEditControls.py @@ -4,9 +4,8 @@ import sys import traceback import wx -import wx.lib.maskededit as med -import wx.lib.maskedctrl as mctl -import wx.lib.scrolledpanel as scroll +import wx.lib.masked as masked +import wx.lib.scrolledpanel as scroll class demoMixin: @@ -18,7 +17,7 @@ class demoMixin: mask = wx.StaticText( self, -1, "Mask Value" ) formatcode = wx.StaticText( self, -1, "Format" ) regex = wx.StaticText( self, -1, "Regexp Validator(opt.)" ) - ctrl = wx.StaticText( self, -1, "MaskedTextCtrl" ) + ctrl = wx.StaticText( self, -1, "Masked TextCtrl" ) description.SetFont( wx.Font(9, wx.SWISS, wx.NORMAL, wx.BOLD)) mask.SetFont( wx.Font(9, wx.SWISS, wx.NORMAL, wx.BOLD)) @@ -41,7 +40,7 @@ class demoMixin: sizer.Add( wx.StaticText( self, -1, control[4]) ) if control in controls: - newControl = med.MaskedTextCtrl( self, -1, "", + newControl = masked.TextCtrl( self, -1, "", mask = control[1], excludeChars = control[2], formatcodes = control[3], @@ -79,7 +78,7 @@ class demoPage1(scroll.ScrolledPanel, demoMixin): self.editList = [] label = wx.StaticText( self, -1, """\ -Here are some basic MaskedTextCtrls to give you an idea of what you can do +Here are some basic masked TextCtrls to give you an idea of what you can do with this control. Note that all controls have been auto-sized by including 'F' in the format codes. @@ -152,8 +151,8 @@ class demoPage2(scroll.ScrolledPanel, demoMixin): label = wx.StaticText( self, -1, """\ All these controls have been created by passing a single parameter, the autoformat code, -and use the factory class MaskedCtrl with its default controlType. -The maskededit module contains an internal dictionary of types and formats (autoformats). +and use the factory class masked.Ctrl with its default controlType. +The masked package contains an internal dictionary of types and formats (autoformats). Many of these already do complicated validation; To see some examples, try 29 Feb 2002 vs. 2004 for the date formats, or email address validation. """) @@ -163,7 +162,7 @@ Many of these already do complicated validation; To see some examples, try description = wx.StaticText( self, -1, "Description") autofmt = wx.StaticText( self, -1, "AutoFormat Code") - ctrl = wx.StaticText( self, -1, "MaskedCtrl") + ctrl = wx.StaticText( self, -1, "Masked Ctrl") description.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) ) autofmt.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) ) @@ -174,10 +173,10 @@ Many of these already do complicated validation; To see some examples, try grid.Add( autofmt, 0, wx.ALIGN_LEFT ) grid.Add( ctrl, 0, wx.ALIGN_LEFT ) - for autoformat, desc in med.autoformats: + for autoformat, desc in masked.autoformats: grid.Add( wx.StaticText( self, -1, desc), 0, wx.ALIGN_LEFT ) grid.Add( wx.StaticText( self, -1, autoformat), 0, wx.ALIGN_LEFT ) - grid.Add( mctl.MaskedCtrl( self, -1, "", + grid.Add( masked.Ctrl( self, -1, "", autoformat = autoformat, demo = True, name = autoformat), @@ -197,7 +196,7 @@ class demoPage3(scroll.ScrolledPanel, demoMixin): self.editList = [] label = wx.StaticText( self, -1, """\ -Here MaskedTextCtrls that have default values. The states +Here masked TextCtrls that have default values. The states control has a list of valid values, and the unsigned integer has a legal range specified. """) @@ -215,7 +214,7 @@ has a legal range specified. controls = [ #description mask excl format regexp range,list,initial - ("U.S. State (2 char)", "AA", "", 'F!_', "[A-Z]{2}", '',med.states, med.states[0]), + ("U.S. State (2 char)", "AA", "", 'F!_', "[A-Z]{2}", '', masked.states, masked.states[0]), ("Integer (signed)", "#{6}", "", 'F-_', "", '','', ' 0 '), ("Integer (unsigned)\n(1-399)","######", "", 'F_', "", (1,399),'', '1 '), ("Float (signed)", "#{6}.#{9}", "", 'F-_R', "", '','', '000000.000000000'), @@ -256,7 +255,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list. description = wx.StaticText( self, -1, "Description" ) autofmt = wx.StaticText( self, -1, "AutoFormat Code" ) fields = wx.StaticText( self, -1, "Field Objects" ) - ctrl = wx.StaticText( self, -1, "MaskedTextCtrl" ) + ctrl = wx.StaticText( self, -1, "Masked TextCtrl" ) description.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) ) autofmt.SetFont( wx.Font( 9, wx.SWISS, wx.NORMAL, wx.BOLD ) ) @@ -270,7 +269,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list. grid.Add( ctrl, 0, wx.ALIGN_LEFT ) autoformat = "USPHONEFULLEXT" - fieldsDict = {0: med.Field(choices=["617","781","508","978","413"], choiceRequired=True)} + fieldsDict = {0: masked.Field(choices=["617","781","508","978","413"], choiceRequired=True)} fieldsLabel = """\ {0: Field(choices=[ "617","781", @@ -279,7 +278,7 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list. grid.Add( wx.StaticText( self, -1, "Restricted Area Code"), 0, wx.ALIGN_LEFT ) grid.Add( wx.StaticText( self, -1, autoformat), 0, wx.ALIGN_LEFT ) grid.Add( wx.StaticText( self, -1, fieldsLabel), 0, wx.ALIGN_LEFT ) - grid.Add( med.MaskedTextCtrl( self, -1, "", + grid.Add( masked.TextCtrl( self, -1, "", autoformat = autoformat, fields = fieldsDict, demo = True, @@ -287,12 +286,12 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list. 0, wx.ALIGN_LEFT ) autoformat = "EXPDATEMMYY" - fieldsDict = {1: med.Field(choices=["03", "04", "05"], choiceRequired=True)} + fieldsDict = {1: masked.Field(choices=["03", "04", "05"], choiceRequired=True)} fieldsLabel = """\ {1: Field(choices=[ "03", "04", "05"], choiceRequired=True)}""" - exp = med.MaskedTextCtrl( self, -1, "", + exp = masked.TextCtrl( self, -1, "", autoformat = autoformat, fields = fieldsDict, demo = True, @@ -303,15 +302,15 @@ Page Up and Shift-Up arrow will similarly cycle backwards through the list. grid.Add( wx.StaticText( self, -1, fieldsLabel), 0, wx.ALIGN_LEFT ) grid.Add( exp, 0, wx.ALIGN_LEFT ) - fieldsDict = {0: med.Field(choices=["02134","02155"], choiceRequired=True), - 1: med.Field(choices=["1234", "5678"], choiceRequired=False)} + fieldsDict = {0: masked.Field(choices=["02134","02155"], choiceRequired=True), + 1: masked.Field(choices=["1234", "5678"], choiceRequired=False)} fieldsLabel = """\ {0: Field(choices=["02134","02155"], choiceRequired=True), 1: Field(choices=["1234", "5678"], choiceRequired=False)}""" autoformat = "USZIPPLUS4" - zip = med.MaskedTextCtrl( self, -1, "", + zip = masked.TextCtrl( self, -1, "", autoformat = autoformat, fields = fieldsDict, demo = True, @@ -336,7 +335,7 @@ class demoPage5(scroll.ScrolledPanel, demoMixin): labelMaskedCombos = wx.StaticText( self, -1, """\ -These are some examples of MaskedComboBox:""") +These are some examples of masked.ComboBox:""") labelMaskedCombos.SetForegroundColour( "Blue" ) @@ -344,8 +343,8 @@ These are some examples of MaskedComboBox:""") A state selector; only "legal" values can be entered:""") - statecode = med.MaskedComboBox( self, -1, med.states[0], - choices = med.states, + statecode = masked.ComboBox( self, -1, masked.states[0], + choices = masked.states, autoformat="USSTATE") label_statename = wx.StaticText( self, -1, """\ @@ -353,9 +352,9 @@ A state name selector, with auto-select:""") # Create this one using factory function: - statename = mctl.MaskedCtrl( self, -1, med.state_names[0], - controlType = mctl.controlTypes.MASKEDCOMBO, - choices = med.state_names, + statename = masked.Ctrl( self, -1, masked.state_names[0], + controlType = masked.controlTypes.COMBO, + choices = masked.state_names, autoformat="USSTATENAME", autoSelect=True) statename.SetCtrlParameters(formatcodes = 'F!V_') @@ -363,8 +362,8 @@ with auto-select:""") numerators = [ str(i) for i in range(1, 4) ] denominators = [ string.ljust(str(i), 2) for i in [2,3,4,5,8,16,32,64] ] - fieldsDict = {0: med.Field(choices=numerators, choiceRequired=False), - 1: med.Field(choices=denominators, choiceRequired=True)} + fieldsDict = {0: masked.Field(choices=numerators, choiceRequired=False), + 1: masked.Field(choices=denominators, choiceRequired=True)} choices = [] for n in numerators: for d in denominators: @@ -377,8 +376,8 @@ A masked ComboBox for fraction selection. Choices for each side of the fraction can be selected with PageUp/Down:""") - fraction = mctl.MaskedCtrl( self, -1, "", - controlType = mctl.MASKEDCOMBO, + fraction = masked.Ctrl( self, -1, "", + controlType = masked.controlTypes.COMBO, choices = choices, choiceRequired = True, mask = "#/##", @@ -392,7 +391,7 @@ A masked ComboBox to validate text from a list of numeric codes:""") choices = ["91", "136", "305", "4579"] - code = med.MaskedComboBox( self, -1, choices[0], + code = masked.ComboBox( self, -1, choices[0], choices = choices, choiceRequired = True, formatcodes = "F_r", @@ -402,8 +401,8 @@ text from a list of numeric codes:""") Programmatically set choice sets:""") self.list_selector = wx.ComboBox(self, -1, '', choices = ['list1', 'list2', 'list3']) - self.dynamicbox = mctl.MaskedCtrl( self, -1, ' ', - controlType = mctl.controlTypes.MASKEDCOMBO, + self.dynamicbox = masked.Ctrl( self, -1, ' ', + controlType = masked.controlTypes.COMBO, mask = 'XXXX', formatcodes = 'F_', # these are to give dropdown some initial height, @@ -415,23 +414,23 @@ choice sets:""") labelIpAddrs = wx.StaticText( self, -1, """\ -Here are some examples of IpAddrCtrl, a control derived from MaskedTextCtrl:""") +Here are some examples of IpAddrCtrl, a control derived from masked.TextCtrl:""") labelIpAddrs.SetForegroundColour( "Blue" ) label_ipaddr1 = wx.StaticText( self, -1, "An empty control:") - ipaddr1 = med.IpAddrCtrl( self, -1, style = wx.TE_PROCESS_TAB ) + ipaddr1 = masked.IpAddrCtrl( self, -1, style = wx.TE_PROCESS_TAB ) label_ipaddr2 = wx.StaticText( self, -1, "A restricted mask:") - ipaddr2 = med.IpAddrCtrl( self, -1, mask=" 10. 1.109.###" ) + ipaddr2 = masked.IpAddrCtrl( self, -1, mask=" 10. 1.109.###" ) label_ipaddr3 = wx.StaticText( self, -1, """\ A control with restricted legal values: 10. (1|2) . (129..255) . (0..255)""") - ipaddr3 = mctl.MaskedCtrl( self, -1, - controlType = mctl.controlTypes.IPADDR, + ipaddr3 = masked.Ctrl( self, -1, + controlType = masked.controlTypes.IPADDR, mask=" 10. #.###.###") ipaddr3.SetFieldParameters(0, validRegex="1|2",validRequired=False ) # requires entry to match or not allowed @@ -441,22 +440,22 @@ A control with restricted legal values: labelNumerics = wx.StaticText( self, -1, """\ -Here are some useful configurations of a MaskedTextCtrl for integer and floating point input that still treat -the control as a text control. (For a true numeric control, check out the MaskedNumCtrl class!)""") +Here are some useful configurations of a masked.TextCtrl for integer and floating point input that still treat +the control as a text control. (For a true numeric control, check out the masked.NumCtrl class!)""") labelNumerics.SetForegroundColour( "Blue" ) label_intctrl1 = wx.StaticText( self, -1, """\ An integer entry control with shifting insert enabled:""") - self.intctrl1 = med.MaskedTextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,F>') + self.intctrl1 = masked.TextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,F>') label_intctrl2 = wx.StaticText( self, -1, """\ Right-insert integer entry:""") - self.intctrl2 = med.MaskedTextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,Fr') + self.intctrl2 = masked.TextCtrl(self, -1, name='intctrl', mask="#{9}", formatcodes = '_-,Fr') label_floatctrl = wx.StaticText( self, -1, """\ A floating point entry control with right-insert for ordinal:""") - self.floatctrl = med.MaskedTextCtrl(self, -1, name='floatctrl', mask="#{9}.#{2}", formatcodes="F,_-R", useParensForNegatives=False) + self.floatctrl = masked.TextCtrl(self, -1, name='floatctrl', mask="#{9}.#{2}", formatcodes="F,_-R", useParensForNegatives=False) self.floatctrl.SetFieldParameters(0, formatcodes='r<', validRequired=True) # right-insert, require explicit cursor movement to change fields self.floatctrl.SetFieldParameters(1, defaultValue='00') # don't allow blank fraction @@ -588,7 +587,7 @@ with right-insert for ordinal:""") formatcodes += 'r' mask = '###' else: - choices = med.states + choices = masked.states mask = 'AA' formatcodes += '!' self.dynamicbox.SetCtrlParameters( mask = mask, @@ -628,15 +627,15 @@ def runTest(frame, nb, log): def RunStandalone(): app = wx.PySimpleApp() - frame = wx.Frame(None, -1, "Test MaskedTextCtrl", size=(640, 480)) + frame = wx.Frame(None, -1, "Test MaskedEditCtrls", size=(640, 480)) win = TestMaskedTextCtrls(frame, -1, sys.stdout) frame.Show(True) app.MainLoop() #---------------------------------------------------------------------------- - +import wx.lib.masked.maskededit as maskededit overview = """

-""" + med.__doc__ + """
+""" + maskededit.__doc__ + """
 
""" diff --git a/wxPython/demo/MaskedNumCtrl.py b/wxPython/demo/MaskedNumCtrl.py index e0e4077600..6d5bda9937 100644 --- a/wxPython/demo/MaskedNumCtrl.py +++ b/wxPython/demo/MaskedNumCtrl.py @@ -4,8 +4,8 @@ import sys import traceback import wx -import wx.lib.maskededit as me -import wx.lib.maskednumctrl as mnum +from wx.lib import masked + #---------------------------------------------------------------------- class TestPanel( wx.Panel ): @@ -16,40 +16,40 @@ class TestPanel( wx.Panel ): panel = wx.Panel( self, -1 ) header = wx.StaticText(panel, -1, """\ -This shows the various options for MaskedNumCtrl. +This shows the various options for masked.NumCtrl. The controls at the top reconfigure the resulting control at the bottom. """) header.SetForegroundColour( "Blue" ) intlabel = wx.StaticText( panel, -1, "Integer width:" ) - self.integerwidth = mnum.MaskedNumCtrl( + self.integerwidth = masked.NumCtrl( panel, value=10, integerWidth=2, allowNegative=False ) fraclabel = wx.StaticText( panel, -1, "Fraction width:" ) - self.fractionwidth = mnum.MaskedNumCtrl( - panel, value=0, integerWidth=2, allowNegative=False + self.fractionwidth = masked.NumCtrl( + panel, value=0, integerWidth=2, allowNegative=False ) groupcharlabel = wx.StaticText( panel,-1, "Grouping char:" ) - self.groupchar = me.MaskedTextCtrl( + self.groupchar = masked.TextCtrl( panel, -1, value=',', mask='&', excludeChars = '-()', formatcodes='F', emptyInvalid=True, validRequired=True ) decimalcharlabel = wx.StaticText( panel,-1, "Decimal char:" ) - self.decimalchar = me.MaskedTextCtrl( + self.decimalchar = masked.TextCtrl( panel, -1, value='.', mask='&', excludeChars = '-()', formatcodes='F', emptyInvalid=True, validRequired=True ) self.set_min = wx.CheckBox( panel, -1, "Set minimum value:" ) - # Create this MaskedNumCtrl using factory, to show how: - self.min = mnum.MaskedNumCtrl( panel, integerWidth=5, fractionWidth=2 ) + # Create this masked.NumCtrl using factory, to show how: + self.min = masked.Ctrl( panel, integerWidth=5, fractionWidth=2, controlType=masked.controlTypes.NUMBER ) self.min.Enable( False ) self.set_max = wx.CheckBox( panel, -1, "Set maximum value:" ) - self.max = mnum.MaskedNumCtrl( panel, integerWidth=5, fractionWidth=2 ) + self.max = masked.NumCtrl( panel, integerWidth=5, fractionWidth=2 ) self.max.Enable( False ) @@ -68,7 +68,7 @@ The controls at the top reconfigure the resulting control at the bottom. font.SetWeight(wx.BOLD) label.SetFont(font) - self.target_ctl = mnum.MaskedNumCtrl( panel, -1, name="target control" ) + self.target_ctl = masked.NumCtrl( panel, -1, name="target control" ) label_numselect = wx.StaticText( panel, -1, """\ Programmatically set the above @@ -141,15 +141,15 @@ value entry ctrl:""") panel.Move( (50,10) ) self.panel = panel - self.Bind(mnum.EVT_MASKEDNUM, self.OnSetIntWidth, self.integerwidth ) - self.Bind(mnum.EVT_MASKEDNUM, self.OnSetFractionWidth, self.fractionwidth ) + self.Bind(masked.EVT_NUM, self.OnSetIntWidth, self.integerwidth ) + self.Bind(masked.EVT_NUM, self.OnSetFractionWidth, self.fractionwidth ) self.Bind(wx.EVT_TEXT, self.OnSetGroupChar, self.groupchar ) self.Bind(wx.EVT_TEXT, self.OnSetDecimalChar, self.decimalchar ) self.Bind(wx.EVT_CHECKBOX, self.OnSetMin, self.set_min ) self.Bind(wx.EVT_CHECKBOX, self.OnSetMax, self.set_max ) - self.Bind(mnum.EVT_MASKEDNUM, self.SetTargetMinMax, self.min ) - self.Bind(mnum.EVT_MASKEDNUM, self.SetTargetMinMax, self.max ) + self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.min ) + self.Bind(masked.EVT_NUM, self.SetTargetMinMax, self.max ) self.Bind(wx.EVT_CHECKBOX, self.SetTargetMinMax, self.limit_target ) self.Bind(wx.EVT_CHECKBOX, self.OnSetAllowNone, self.allow_none ) @@ -158,7 +158,7 @@ value entry ctrl:""") self.Bind(wx.EVT_CHECKBOX, self.OnSetUseParens, self.use_parens ) self.Bind(wx.EVT_CHECKBOX, self.OnSetSelectOnEntry, self.select_on_entry ) - self.Bind(mnum.EVT_MASKEDNUM, self.OnTargetChange, self.target_ctl ) + self.Bind(masked.EVT_NUM, self.OnTargetChange, self.target_ctl ) self.Bind(wx.EVT_COMBOBOX, self.OnNumberSelect, self.numselect ) @@ -323,6 +323,7 @@ def runTest( frame, nb, log ): return win #---------------------------------------------------------------------- +import wx.lib.masked.numctrl as mnum overview = mnum.__doc__ if __name__ == '__main__': diff --git a/wxPython/demo/TimeCtrl.py b/wxPython/demo/TimeCtrl.py index 3f7098d0ed..7b730fb0b9 100644 --- a/wxPython/demo/TimeCtrl.py +++ b/wxPython/demo/TimeCtrl.py @@ -1,12 +1,12 @@ -# +# # 11/21/2003 - Jeff Grimmett (grimmtooth@softhome.net) # # o presense of spin control causing probs (see spin ctrl demo for details) -# +# -import wx -import wx.lib.timectrl as timectl -import wx.lib.scrolledpanel as scrolled +import wx +import wx.lib.scrolledpanel as scrolled +import wx.lib.masked as masked #---------------------------------------------------------------------- @@ -18,21 +18,21 @@ class TestPanel( scrolled.ScrolledPanel ): text1 = wx.StaticText( self, -1, "12-hour format:") - self.time12 = timectl.TimeCtrl( self, -1, name="12 hour control" ) + self.time12 = masked.TimeCtrl( self, -1, name="12 hour control" ) spin1 = wx.SpinButton( self, -1, wx.DefaultPosition, (-1,20), 0 ) self.time12.BindSpinButton( spin1 ) text2 = wx.StaticText( self, -1, "24-hour format:") spin2 = wx.SpinButton( self, -1, wx.DefaultPosition, (-1,20), 0 ) - self.time24 = timectl.TimeCtrl( - self, -1, name="24 hour control", fmt24hr=True, - spinButton = spin2 + self.time24 = masked.TimeCtrl( + self, -1, name="24 hour control", fmt24hr=True, + spinButton = spin2 ) text3 = wx.StaticText( self, -1, "No seconds\nor spin button:") - self.spinless_ctrl = timectl.TimeCtrl( - self, -1, name="spinless control", - display_seconds = False + self.spinless_ctrl = masked.TimeCtrl( + self, -1, name="spinless control", + display_seconds = False ) grid = wx.FlexGridSizer( 0, 2, 10, 5 ) @@ -54,8 +54,8 @@ class TestPanel( scrolled.ScrolledPanel ): buttonChange = wx.Button( self, -1, "Change Controls") self.radio12to24 = wx.RadioButton( - self, -1, "Copy 12-hour time to 24-hour control", - wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP + self, -1, "Copy 12-hour time to 24-hour control", + wx.DefaultPosition, wx.DefaultSize, wx.RB_GROUP ) self.radio24to12 = wx.RadioButton( @@ -86,17 +86,17 @@ class TestPanel( scrolled.ScrolledPanel ): self.set_bounds = wx.CheckBox( self, -1, "Set time bounds:" ) minlabel = wx.StaticText( self, -1, "minimum time:" ) - self.min = timectl.TimeCtrl( self, -1, name="min", display_seconds = False ) + self.min = masked.TimeCtrl( self, -1, name="min", display_seconds = False ) self.min.Enable( False ) maxlabel = wx.StaticText( self, -1, "maximum time:" ) - self.max = timectl.TimeCtrl( self, -1, name="max", display_seconds = False ) + self.max = masked.TimeCtrl( self, -1, name="max", display_seconds = False ) self.max.Enable( False ) self.limit_check = wx.CheckBox( self, -1, "Limit control" ) label = wx.StaticText( self, -1, "Resulting time control:" ) - self.target_ctrl = timectl.TimeCtrl( self, -1, name="new" ) + self.target_ctrl = masked.TimeCtrl( self, -1, name="new" ) grid2 = wx.FlexGridSizer( 0, 2, 0, 0 ) grid2.Add( (20, 0), 0, wx.ALIGN_LEFT|wx.ALL, 5 ) @@ -142,14 +142,14 @@ class TestPanel( scrolled.ScrolledPanel ): self.SetupScrolling() self.Bind(wx.EVT_BUTTON, self.OnButtonClick, buttonChange ) - self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.time12 ) - self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.time24 ) - self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.spinless_ctrl ) + self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.time12 ) + self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.time24 ) + self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.spinless_ctrl ) self.Bind(wx.EVT_CHECKBOX, self.OnBoundsCheck, self.set_bounds ) self.Bind(wx.EVT_CHECKBOX, self.SetTargetMinMax, self.limit_check ) - self.Bind(timectl.EVT_TIMEUPDATE, self.SetTargetMinMax, self.min ) - self.Bind(timectl.EVT_TIMEUPDATE, self.SetTargetMinMax, self.max ) - self.Bind(timectl.EVT_TIMEUPDATE, self.OnTimeChange, self.target_ctrl ) + self.Bind(masked.EVT_TIMEUPDATE, self.SetTargetMinMax, self.min ) + self.Bind(masked.EVT_TIMEUPDATE, self.SetTargetMinMax, self.max ) + self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeChange, self.target_ctrl ) def OnTimeChange( self, event ): @@ -204,7 +204,7 @@ class TestPanel( scrolled.ScrolledPanel ): min, max = None, None cur_min, cur_max = self.target_ctrl.GetBounds() - + print cur_min, min if min and (min != cur_min): self.target_ctrl.SetMin( min ) if max and (max != cur_max): self.target_ctrl.SetMax( max ) @@ -225,11 +225,11 @@ def runTest( frame, nb, log ): return win #---------------------------------------------------------------------- - +import wx.lib.masked.timectrl as timectl overview = timectl.__doc__ if __name__ == '__main__': import sys,os import run - run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) + run.main(['', os.path.basename(sys.argv[0])]) diff --git a/wxPython/docs/CHANGES.txt b/wxPython/docs/CHANGES.txt index af1127d3e8..dcdf99190b 100644 --- a/wxPython/docs/CHANGES.txt +++ b/wxPython/docs/CHANGES.txt @@ -17,6 +17,9 @@ Added some convenience methods to wx.Bitmap: SetSize, GetSize, and wx.EmptyBitmap can be called with a wx.Size (or a 2-element sequence) object too. Similar changes were done for wx.Image as well. +Added new MaskedEditControl code from Will Sadkin. The modules are +now locaed in their own sub-package, wx.lib.masked. Demos updated. + diff --git a/wxPython/docs/MigrationGuide.txt b/wxPython/docs/MigrationGuide.txt index bf4e356de2..3828d4dc72 100644 --- a/wxPython/docs/MigrationGuide.txt +++ b/wxPython/docs/MigrationGuide.txt @@ -619,6 +619,14 @@ Similarly, the wxSystemSettings backwards compatibiility aliases for GetSystemColour, GetSystemFont and GetSystemMetric have also gone into the bit-bucket. Use GetColour, GetFont and GetMetric instead. +Use the Python True/False constants instead of the true, TRUE, false, +FALSE that used to be provided with wxPython. + +Use None instead of the ancient and should have been removed a long +time ago wx.NULL alias. + +wx.TreeCtrl no longer needs to be passed the cookie variable as the +2nd parameter. It still returns it though, for use with GetNextChild. The wx.NO_FULL_REPAINT_ON_RESIZE style is now the default style for all windows. The name still exists for compatibility, but it is set @@ -667,3 +675,8 @@ functions in wxPython for parameters that are expecting an integer. If the object is not already an integer then it will be asked to convert itself to one. A similar conversion fragment is in place for parameters that expect floating point values. + +**[Changed in 2.5.1.6]** The MaskedEditCtrl modules have been moved +to their own sub-package, wx.lib.masked. See the docstrings and demo +for changes in capabilities, usage, etc. + diff --git a/wxPython/wx/lib/masked/__init__.py b/wxPython/wx/lib/masked/__init__.py new file mode 100644 index 0000000000..1a5a59ea70 --- /dev/null +++ b/wxPython/wx/lib/masked/__init__.py @@ -0,0 +1,20 @@ +#---------------------------------------------------------------------- +# Name: wxPython.lib.masked +# Purpose: A package containing the masked edit controls +# +# Author: Will Sadkin, Jeff Childers +# +# Created: 6-Mar-2004 +# RCS-ID: $Id$ +# Copyright: (c) 2004 +# License: wxWidgets license +#---------------------------------------------------------------------- + +# import relevant external symbols into package namespace: +from maskededit import * +from textctrl import BaseMaskedTextCtrl, TextCtrl +from combobox import BaseMaskedComboBox, ComboBox, MaskedComboBoxSelectEvent +from numctrl import NumCtrl, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, EVT_NUM, NumberUpdatedEvent +from timectrl import TimeCtrl, wxEVT_TIMEVAL_UPDATED, EVT_TIMEUPDATE, TimeUpdatedEvent +from ipaddrctrl import IpAddrCtrl +from ctrl import Ctrl, controlTypes diff --git a/wxPython/wx/lib/masked/combobox.py b/wxPython/wx/lib/masked/combobox.py new file mode 100644 index 0000000000..3619c398f2 --- /dev/null +++ b/wxPython/wx/lib/masked/combobox.py @@ -0,0 +1,540 @@ +#---------------------------------------------------------------------------- +# Name: masked.combobox.py +# Authors: Will Sadkin +# Email: wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Will Sadkin, 2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# +# This masked edit class allows for the semantics of masked controls +# to be applied to combo boxes. +# +#---------------------------------------------------------------------------- + +import wx +from wx.lib.masked import * + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +dbg = Logger() +##dbg(enable=0) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +## Because calling SetSelection programmatically does not fire EVT_COMBOBOX +## events, we have to do it ourselves when we auto-complete. +class MaskedComboBoxSelectEvent(wx.PyCommandEvent): + def __init__(self, id, selection = 0, object=None): + wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id) + + self.__selection = selection + self.SetEventObject(object) + + def GetSelection(self): + """Retrieve the value of the control at the time + this event was generated.""" + return self.__selection + + +class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): + """ + This masked edit control adds the ability to use a masked input + on a combobox, and do auto-complete of such values. + """ + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + choices = [], + style = wx.CB_DROPDOWN, + validator = wx.DefaultValidator, + name = "maskedComboBox", + setupEventHandling = True, ## setup event handling by default): + **kwargs): + + + # This is necessary, because wxComboBox currently provides no + # method for determining later if this was specified in the + # constructor for the control... + self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY + + kwargs['choices'] = choices ## set up maskededit to work with choice list too + + ## Since combobox completion is case-insensitive, always validate same way + if not kwargs.has_key('compareNoCase'): + kwargs['compareNoCase'] = True + + MaskedEditMixin.__init__( self, name, **kwargs ) + + self._choices = self._ctrl_constraints._choices +## dbg('self._choices:', self._choices) + + if self._ctrl_constraints._alignRight: + choices = [choice.rjust(self._masklength) for choice in choices] + else: + choices = [choice.ljust(self._masklength) for choice in choices] + + wx.ComboBox.__init__(self, parent, id, value='', + pos=pos, size = size, + choices=choices, style=style|wx.WANTS_CHARS, + validator=validator, + name=name) + + self.controlInitialized = True + + # Set control font - fixed width by default + self._setFont() + + if self._autofit: + self.SetClientSize(self._CalcSize()) + + if value: + # ensure value is width of the mask of the control: + if self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + if self.__readonly: + self.SetStringSelection(value) + else: + self._SetInitialValue(value) + + + self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice) + self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice) + + if setupEventHandling: + ## Setup event handlers + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown ) ## for special processing of up/down keys + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys + ## (next in evt chain) + self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + + + def __repr__(self): + return "" % self.GetValue() + + + def _CalcSize(self, size=None): + """ + Calculate automatic size if allowed; augment base mixin function + to account for the selector button. + """ + size = self._calcSize(size) + return (size[0]+20, size[1]) + + + def _GetSelection(self): + """ + Allow mixin to get the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetMark() + + def _SetSelection(self, sel_start, sel_to): + """ + Allow mixin to set the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.SetMark( sel_start, sel_to ) + + + def _GetInsertionPoint(self): + return self.GetInsertionPoint() + + def _SetInsertionPoint(self, pos): + self.SetInsertionPoint(pos) + + + def _GetValue(self): + """ + Allow mixin to get the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetValue() + + def _SetValue(self, value): + """ + Allow mixin to set the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + # For wxComboBox, ensure that values are properly padded so that + # if varying length choices are supplied, they always show up + # in the window properly, and will be the appropriate length + # to match the mask: + if self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + # Record current selection and insertion point, for undo + self._prevSelection = self._GetSelection() + self._prevInsertionPoint = self._GetInsertionPoint() + wx.ComboBox.SetValue(self, value) + # text change events don't always fire, so we check validity here + # to make certain formatting is applied: + self._CheckValid() + + def SetValue(self, value): + """ + This function redefines the externally accessible .SetValue to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if not self._mask: + wx.ComboBox.SetValue(value) # revert to base control behavior + return + # else... + # empty previous contents, replacing entire value: + self._SetInsertionPoint(0) + self._SetSelection(0, self._masklength) + + if( len(value) < self._masklength # value shorter than control + and (self._isFloat or self._isInt) # and it's a numeric control + and self._ctrl_constraints._alignRight ): # and it's a right-aligned control + # try to intelligently "pad out" the value to the right size: + value = self._template[0:self._masklength - len(value)] + value +## dbg('padded value = "%s"' % value) + + # For wxComboBox, ensure that values are properly padded so that + # if varying length choices are supplied, they always show up + # in the window properly, and will be the appropriate length + # to match the mask: + elif self._ctrl_constraints._alignRight: + value = value.rjust(self._masklength) + else: + value = value.ljust(self._masklength) + + + # make SetValue behave the same as if you had typed the value in: + try: + value = self._Paste(value, raise_on_invalid=True, just_return_value=True) + if self._isFloat: + self._isNeg = False # (clear current assumptions) + value = self._adjustFloat(value) + elif self._isInt: + self._isNeg = False # (clear current assumptions) + value = self._adjustInt(value) + elif self._isDate and not self.IsValid(value) and self._4digityear: + value = self._adjustDate(value, fixcentury=True) + except ValueError: + # If date, year might be 2 digits vs. 4; try adjusting it: + if self._isDate and self._4digityear: + dateparts = value.split(' ') + dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) + value = string.join(dateparts, ' ') +## dbg('adjusted value: "%s"' % value) + value = self._Paste(value, raise_on_invalid=True, just_return_value=True) + else: + raise + + self._SetValue(value) +#### dbg('queuing insertion after .SetValue', self._masklength) + wx.CallAfter(self._SetInsertionPoint, self._masklength) + wx.CallAfter(self._SetSelection, self._masklength, self._masklength) + + + def _Refresh(self): + """ + Allow mixin to refresh the base control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + wx.ComboBox.Refresh(self) + + def Refresh(self): + """ + This function redefines the externally accessible .Refresh() to + validate the contents of the masked control as it refreshes. + NOTE: this must be done in the class derived from the base wx control. + """ + self._CheckValid() + self._Refresh() + + + def _IsEditable(self): + """ + Allow mixin to determine if the base control is editable with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return not self.__readonly + + + def Cut(self): + """ + This function redefines the externally accessible .Cut to be + a smart "erase" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Cut() # call the mixin's Cut method + else: + wx.ComboBox.Cut(self) # else revert to base control behavior + + + def Paste(self): + """ + This function redefines the externally accessible .Paste to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Paste() # call the mixin's Paste method + else: + wx.ComboBox.Paste(self) # else revert to base control behavior + + + def Undo(self): + """ + This function defines the undo operation for the control. (The default + undo is 1-deep.) + """ + if self._mask: + self._Undo() + else: + wx.ComboBox.Undo() # else revert to base control behavior + + + def Append( self, choice, clientData=None ): + """ + This function override is necessary so we can keep track of any additions to the list + of choices, because wxComboBox doesn't have an accessor for the choice list. + The code here is the same as in the SetParameters() mixin function, but is + done for the individual value as appended, so the list can be built incrementally + without speed penalty. + """ + if self._mask: + if type(choice) not in (types.StringType, types.UnicodeType): + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + elif not self.IsValid(choice): + raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) + + if not self._ctrl_constraints._choices: + self._ctrl_constraints._compareChoices = [] + self._ctrl_constraints._choices = [] + self._hasList = True + + compareChoice = choice.strip() + + if self._ctrl_constraints._compareNoCase: + compareChoice = compareChoice.lower() + + if self._ctrl_constraints._alignRight: + choice = choice.rjust(self._masklength) + else: + choice = choice.ljust(self._masklength) + if self._ctrl_constraints._fillChar != ' ': + choice = choice.replace(' ', self._fillChar) +## dbg('updated choice:', choice) + + + self._ctrl_constraints._compareChoices.append(compareChoice) + self._ctrl_constraints._choices.append(choice) + self._choices = self._ctrl_constraints._choices # (for shorthand) + + if( not self.IsValid(choice) and + (not self._ctrl_constraints.IsEmpty(choice) or + (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ): + raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name)) + + wx.ComboBox.Append(self, choice, clientData) + + + + def Clear( self ): + """ + This function override is necessary so we can keep track of any additions to the list + of choices, because wxComboBox doesn't have an accessor for the choice list. + """ + if self._mask: + self._choices = [] + self._ctrl_constraints._autoCompleteIndex = -1 + if self._ctrl_constraints._choices: + self.SetCtrlParameters(choices=[]) + wx.ComboBox.Clear(self) + + + def _OnCtrlParametersChanged(self): + """ + Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so + we can update the base control: + """ + if self.controlInitialized and self._choices != self._ctrl_constraints._choices: + wx.ComboBox.Clear(self) + self._choices = self._ctrl_constraints._choices + for choice in self._choices: + wx.ComboBox.Append( self, choice ) + + + def GetMark(self): + """ + This function is a hack to make up for the fact that wxComboBox has no + method for returning the selected portion of its edit control. It + works, but has the nasty side effect of generating lots of intermediate + events. + """ +## dbg(suspend=1) # turn off debugging around this function +## dbg('MaskedComboBox::GetMark', indent=1) + if self.__readonly: +## dbg(indent=0) + return 0, 0 # no selection possible for editing +## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have! + sel_start = sel_to = self.GetInsertionPoint() +## dbg("current sel_start:", sel_start) + value = self.GetValue() +## dbg('value: "%s"' % value) + + self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any) + + wx.ComboBox.Cut(self) + newvalue = self.GetValue() +## dbg("value after Cut operation:", newvalue) + + if newvalue != value: # something was selected; calculate extent +## dbg("something selected") + sel_to = sel_start + len(value) - len(newvalue) + wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change) + wx.ComboBox.SetInsertionPoint(self, sel_start) + wx.ComboBox.SetMark(self, sel_start, sel_to) + + self._ignoreChange = False # tell _OnTextChange() to pay attn again + +## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) + return sel_start, sel_to + + + def SetSelection(self, index): + """ + Necessary for bookkeeping on choice selection, to keep current value + current. + """ +## dbg('MaskedComboBox::SetSelection(%d)' % index) + if self._mask: + self._prevValue = self._curValue + self._curValue = self._choices[index] + self._ctrl_constraints._autoCompleteIndex = index + wx.ComboBox.SetSelection(self, index) + + + def OnKeyDown(self, event): + """ + This function is necessary because navigation and control key + events do not seem to normally be seen by the wxComboBox's + EVT_CHAR routine. (Tabs don't seem to be visible no matter + what... {:-( ) + """ + if event.GetKeyCode() in self._nav + self._control: + self._OnChar(event) + return + else: + event.Skip() # let mixin default KeyDown behavior occur + + + def OnSelectChoice(self, event): + """ + This function appears to be necessary, because the processing done + on the text of the control somehow interferes with the combobox's + selection mechanism for the arrow keys. + """ +## dbg('MaskedComboBox::OnSelectChoice', indent=1) + + if not self._mask: + event.Skip() + return + + value = self.GetValue().strip() + + if self._ctrl_constraints._compareNoCase: + value = value.lower() + + if event.GetKeyCode() == wx.WXK_UP: + direction = -1 + else: + direction = 1 + match_index, partial_match = self._autoComplete( + direction, + self._ctrl_constraints._compareChoices, + value, + self._ctrl_constraints._compareNoCase, + current_index = self._ctrl_constraints._autoCompleteIndex) + if match_index is not None: +## dbg('setting selection to', match_index) + # issue appropriate event to outside: + self._OnAutoSelect(self._ctrl_constraints, match_index=match_index) + self._CheckValid() + keep_processing = False + else: + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + field = self._FindField(pos) + if self.IsEmpty() or not field._hasList: +## dbg('selecting 1st value in list') + self._OnAutoSelect(self._ctrl_constraints, match_index=0) + self._CheckValid() + keep_processing = False + else: + # attempt field-level auto-complete +## dbg(indent=0) + keep_processing = self._OnAutoCompleteField(event) +## dbg('keep processing?', keep_processing, indent=0) + return keep_processing + + + def _OnAutoSelect(self, field, match_index): + """ + Override mixin (empty) autocomplete handler, so that autocompletion causes + combobox to update appropriately. + """ +## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) +## field._autoCompleteIndex = match_index + if field == self._ctrl_constraints: + self.SetSelection(match_index) +## dbg('issuing combo selection event') + self.GetEventHandler().ProcessEvent( + MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) ) + self._CheckValid() +## dbg('field._autoCompleteIndex:', match_index) +## dbg('self.GetSelection():', self.GetSelection()) +## dbg(indent=0) + + + def _OnReturn(self, event): + """ + For wxComboBox, it seems that if you hit return when the dropdown is + dropped, the event that dismisses the dropdown will also blank the + control, because of the implementation of wxComboBox. So here, + we look and if the selection is -1, and the value according to + (the base control!) is a value in the list, then we schedule a + programmatic wxComboBox.SetSelection() call to pick the appropriate + item in the list. (and then do the usual OnReturn bit.) + """ +## dbg('MaskedComboBox::OnReturn', indent=1) +## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) + if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: + wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) + + event.m_keyCode = wx.WXK_TAB + event.Skip() +## dbg(indent=0) + + +class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): + """ + This extra level of inheritance allows us to add the generic set of + masked edit parameters only to this class while allowing other + classes to derive from the "base" masked combobox control, and provide + a smaller set of valid accessor functions. + """ + pass + + diff --git a/wxPython/wx/lib/masked/ctrl.py b/wxPython/wx/lib/masked/ctrl.py new file mode 100644 index 0000000000..897d0663bd --- /dev/null +++ b/wxPython/wx/lib/masked/ctrl.py @@ -0,0 +1,109 @@ +#---------------------------------------------------------------------------- +# Name: wxPython.lib.masked.ctrl.py +# Author: Will Sadkin +# Created: 09/24/2003 +# Copyright: (c) 2003 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace (minor) +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Removed wx prefix +# + +""" +

+masked.Ctrl is actually a factory function for several types of +masked edit controls: +

+

+

+masked.Ctrl works by looking for a special controlType +parameter in the variable arguments of the control, to determine +what kind of instance to return. +controlType can be one of: +


+    controlTypes.TEXT
+    controlTypes.COMBO
+    controlTypes.IPADDR
+    controlTypes.TIME
+    controlTypes.NUMBER
+
+These constants are also available individually, ie, you can +use either of the following: +

+    from wxPython.wx.lib.masked import Ctrl, COMBO, TEXT, NUMBER, TIME
+    from wxPython.wx.lib.masked import Ctrl, controlTypes
+
+If not specified as a keyword argument, the default controlType is +controlTypes.TEXT. +

+Each of the above classes has its own unique arguments, but MaskedCtrl +provides a single "unified" interface for masked controls. Masked.TextCtrl, +masked.ComboBox and masked.IpAddrCtrl are all documented below; the others have +their own demo pages and interface descriptions. + +""" + +from wx.lib.masked import TextCtrl, ComboBox, IpAddrCtrl +from wx.lib.masked import NumCtrl +from wx.lib.masked import TimeCtrl + + +# "type" enumeration for class instance factory function +TEXT = 0 +COMBO = 1 +IPADDR = 2 +TIME = 3 +NUMBER = 4 + +# for ease of import +class controlTypes: + TEXT = TEXT + COMBO = COMBO + IPADDR = IPADDR + TIME = TIME + NUMBER = NUMBER + + +def Ctrl( *args, **kwargs): + """ + Actually a factory function providing a unifying + interface for generating masked controls. + """ + if not kwargs.has_key('controlType'): + controlType = TEXT + else: + controlType = kwargs['controlType'] + del kwargs['controlType'] + + if controlType == TEXT: + return TextCtrl(*args, **kwargs) + + elif controlType == COMBO: + return ComboBox(*args, **kwargs) + + elif controlType == IPADDR: + return IpAddrCtrl(*args, **kwargs) + + elif controlType == TIME: + return TimeCtrl(*args, **kwargs) + + elif controlType == NUMBER: + return NumCtrl(*args, **kwargs) + + else: + raise AttributeError( + "invalid controlType specified: %s" % repr(controlType)) + + diff --git a/wxPython/wx/lib/masked/ipaddrctrl.py b/wxPython/wx/lib/masked/ipaddrctrl.py new file mode 100644 index 0000000000..9fea97faa6 --- /dev/null +++ b/wxPython/wx/lib/masked/ipaddrctrl.py @@ -0,0 +1,187 @@ +#---------------------------------------------------------------------------- +# Name: masked.ipaddrctrl.py +# Authors: Will Sadkin +# Email: wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Will Sadkin, 2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# NOTE: +# Masked.IpAddrCtrl is a minor modification to masked.TextCtrl, that is +# specifically tailored for entering IP addresses. It allows for +# right-insert fields and provides an accessor to obtain the entered +# address with extra whitespace removed. +# +#---------------------------------------------------------------------------- + +import wx +from wx.lib.masked import BaseMaskedTextCtrl + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +dbg = Logger() +##dbg(enable=0) + +class IpAddrCtrlAccessorsMixin: + # Define IpAddrCtrl's list of attributes having their own + # Get/Set functions, exposing only those that make sense for + # an IP address control. + + exposed_basectrl_params = ( + 'fields', + 'retainFieldValidation', + 'formatcodes', + 'fillChar', + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'signedForegroundColour', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'emptyInvalid', + 'validFunc', + 'validRequired', + ) + + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + +class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): + """ + This class is a particular type of MaskedTextCtrl that accepts + and understands the semantics of IP addresses, reformats input + as you move from field to field, and accepts '.' as a navigation + character, so that typing an IP address can be done naturally. + """ + + + + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = 'IpAddrCtrl', + setupEventHandling = True, ## setup event handling by default + **kwargs): + + if not kwargs.has_key('mask'): + kwargs['mask'] = mask = "###.###.###.###" + if not kwargs.has_key('formatcodes'): + kwargs['formatcodes'] = 'F_Sr<' + if not kwargs.has_key('validRegex'): + kwargs['validRegex'] = "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}" + + + BaseMaskedTextCtrl.__init__( + self, parent, id=id, value = value, + pos=pos, size=size, + style = style, + validator = validator, + name = name, + setupEventHandling = setupEventHandling, + **kwargs) + + + # set up individual field parameters as well: + field_params = {} + field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))" + + # require "valid" string; this prevents entry of any value > 255, but allows + # intermediate constructions; overall control validation requires well-formatted value. + field_params['formatcodes'] = 'V' + + if field_params: + for i in self._field_indices: + self.SetFieldParameters(i, **field_params) + + # This makes '.' act like tab: + self._AddNavKey('.', handler=self.OnDot) + self._AddNavKey('>', handler=self.OnDot) # for "shift-." + + + def OnDot(self, event): +## dbg('IpAddrCtrl::OnDot', indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + oldvalue = self.GetValue() + edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) + if not event.ShiftDown(): + if pos > edit_start and pos < edit_end: + # clip data in field to the right of pos, if adjusting fields + # when not at delimeter; (assumption == they hit '.') + newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:] + self._SetValue(newvalue) + self._SetInsertionPoint(pos) +## dbg(indent=0) + return self._OnChangeField(event) + + + + def GetAddress(self): + value = BaseMaskedTextCtrl.GetValue(self) + return value.replace(' ','') # remove spaces from the value + + + def _OnCtrl_S(self, event): +## dbg("IpAddrCtrl::_OnCtrl_S") + if self._demo: + print "value:", self.GetAddress() + return False + + def SetValue(self, value): +## dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1) + if type(value) not in (types.StringType, types.UnicodeType): +## dbg(indent=0) + raise ValueError('%s must be a string', str(value)) + + bValid = True # assume True + parts = value.split('.') + if len(parts) != 4: + bValid = False + else: + for i in range(4): + part = parts[i] + if not 0 <= len(part) <= 3: + bValid = False + break + elif part.strip(): # non-empty part + try: + j = string.atoi(part) + if not 0 <= j <= 255: + bValid = False + break + else: + parts[i] = '%3d' % j + except: + bValid = False + break + else: + # allow empty sections for SetValue (will result in "invalid" value, + # but this may be useful for initializing the control: + parts[i] = ' ' # convert empty field to 3-char length + + if not bValid: +## dbg(indent=0) + raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value)) + else: +## dbg('parts:', parts) + value = string.join(parts, '.') + BaseMaskedTextCtrl.SetValue(self, value) +## dbg(indent=0) + + diff --git a/wxPython/wx/lib/masked/maskededit.py b/wxPython/wx/lib/masked/maskededit.py new file mode 100644 index 0000000000..1c528c9c66 --- /dev/null +++ b/wxPython/wx/lib/masked/maskededit.py @@ -0,0 +1,6676 @@ +#---------------------------------------------------------------------------- +# Name: maskededit.py +# Authors: Jeff Childers, Will Sadkin +# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 +# Portions: (c) 2002 by Will Sadkin, 2002-2003 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# NOTE: +# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by +# Jason Hihn, and borrows liberally from Will Sadkin's original masked edit +# control for time entry, TimeCtrl (which is now rewritten using this +# control!). +# +# MaskedEdit controls do not normally use validators, because they do +# careful manipulation of the cursor in the text window on each keystroke, +# and validation is cursor-position specific, so the control intercepts the +# key codes before the validator would fire. However, validators can be +# provided to do data transfer to the controls. +# +#---------------------------------------------------------------------------- +# +# This file now contains the bulk of the logic behind all masked controls, +# the MaskedEditMixin class, the Field class, and the autoformat codes. +# +#---------------------------------------------------------------------------- +# +# 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com) +# +# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files, +# o Reorganized code into masked package +# +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace. No guarantees. This is one huge file. +# +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Missed wx.DateTime stuff earlier. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o MaskedEditMixin -> MaskedEditMixin +# o wxMaskedTextCtrl -> maskedTextCtrl +# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent +# o wxMaskedComboBox -> MaskedComboBox +# o wxIpAddrCtrl -> IpAddrCtrl +# o wxTimeCtrl -> TimeCtrl +# + +"""\ +Masked Edit Overview: +===================== +masked.TextCtrl + is a sublassed text control that can carefully control the user's input + based on a mask string you provide. + + General usage example: + control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####') + + The example above will create a text control that allows only numbers to be + entered and then only in the positions indicated in the mask by the # sign. + +masked.ComboBox + 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. + +masked.Ctrl + is actually a factory function for several types of masked edit controls: + + masked.TextCtrl - standard masked edit text box + masked.ComboBox - adds combobox capabilities + masked.IpAddrCtrl - adds special semantics for IP address entry + masked.TimeCtrl - special subclass handling lots of types as values + masked.NumCtrl - special subclass handling numeric values + + It works by looking for a controlType parameter in the keyword + arguments of the control, to determine what kind of instance to return. + If not specified as a keyword argument, the default control type returned + will be masked.TextCtrl. + + Each of the above classes has its own set of arguments, but masked.Ctrl + provides a single "unified" interface for masked controls. Those for + masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented + below; the others have their own demo pages and interface descriptions. + (See end of following discussion for how to configure the wxMaskedCtrl() + to select the above control types.) + + +INITILIZATION PARAMETERS +======================== +mask= +Allowed mask characters and function: + Character Function + # Allow numeric only (0-9) + N Allow letters and numbers (0-9) + A Allow uppercase letters only + a Allow lowercase letters only + C Allow any letter, upper or lower + X Allow string.letters, string.punctuation, string.digits + & Allow string.punctuation only + + + These controls define these sets of characters using string.letters, + string.uppercase, etc. These sets are affected by the system locale + setting, so in order to have the masked controls accept characters + that are specific to your users' language, your application should + set the locale. + For example, to allow international characters to be used in the + above masks, you can place the following in your code as part of + your application's initialization code: + + import locale + locale.setlocale(locale.LC_ALL, '') + + + Using these mask characters, a variety of template masks can be built. See + the demo for some other common examples include date+time, social security + number, etc. If any of these characters are needed as template rather + than mask characters, they can be escaped with \, ie. \N means "literal N". + (use \\ for literal backslash, as in: r'CCC\\NNN'.) + + + Note: + Masks containing only # characters and one optional decimal point + character are handled specially, as "numeric" controls. Such + controls have special handling for typing the '-' key, handling + the "decimal point" character as truncating the integer portion, + optionally allowing grouping characters and so forth. + There are several parameters and format codes that only make sense + when combined with such masks, eg. groupChar, decimalChar, and so + forth (see below). These allow you to construct reasonable + numeric entry controls. + + 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 are specified as a string of single character formatting + codes that modify behavior of the control: + _ Allow spaces + ! Force upper + ^ Force lower + R Right-align field(s) + r Right-insert in field(s) (implies R) + < Stay in field until explicit navigation out of it + + > Allow insert/delete within partially filled fields (as + opposed to the default "overwrite" mode for fixed-width + masked edit controls.) This allows single-field controls + or each field within a multi-field control to optionally + behave more like standard text controls. + (See EMAIL or phone number autoformat examples.) + + Note: This also governs whether backspace/delete operations + shift contents of field to right of cursor, or just blank the + erased section. + + Also, when combined with 'r', this indicates that the field + or control allows right insert anywhere within the current + non-empty value in the field. (Otherwise right-insert behavior + is only performed to when the entire right-insertable field is + selected or the cursor is at the right edge of the field. + + + , Allow grouping character in integer fields of numeric controls + and auto-group/regroup digits (if the result fits) when leaving + such a field. (If specified, .SetValue() will attempt to + auto-group as well.) + ',' is also the default grouping character. To change the + grouping character and/or decimal character, use the groupChar + and decimalChar parameters, respectively. + Note: typing the "decimal point" character in such fields will + clip the value to that left of the cursor for integer + fields of controls with "integer" or "floating point" masks. + If the ',' format code is specified, this will also cause the + resulting digits to be regrouped properly, using the current + grouping character. + - Prepend and reserve leading space for sign to mask and allow + signed values (negative #s shown in red by default.) Can be + used with argument useParensForNegatives (see below.) + 0 integer fields get leading zeros + D Date[/time] field + T Time field + F Auto-Fit: the control calulates its size from + the length of the template mask + V validate entered chars against validRegex before allowing them + to be entered vs. being allowed by basic mask and then having + the resulting value just colored as invalid. + (See USSTATE autoformat demo for how this can be used.) + S select entire field when navigating to new field + +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. + +groupChar= +decimalChar= + These parameters govern what character is used to group numbers + and is used to indicate the decimal point for numeric format controls. + The default groupChar is ',', the default decimalChar is '.' + By changing these, you can customize the presentation of numbers + for your location. + eg: formatcodes = ',', groupChar="'" allows 12'345.34 + formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34 + +shiftDecimalChar= + The default "shiftDecimalChar" (used for "backwards-tabbing" until + shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for + other keyboards, you may want to customize this, eg '?' for shift ',' on + AZERTY keyboards, ':' or ';' for other European keyboards, etc. + +useParensForNegatives=False + This option can be used with signed numeric format controls to + indicate signs via () rather than '-'. + +autoSelect=False + This option can be used to have a field or the control try to + auto-complete on each keystroke if choices have been specified. + +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 + choices= A list of strings that are allowed choices for the control. + choiceRequired= value must be member of choices list + compareNoCase= Perform case-insensitive matching when validating against list + Note: for masked.ComboBox, this defaults to True. + 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 .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow': + + emptyBackgroundColour= Control Background color when identified as empty. Default=White + invalidBackgroundColour= Control Background color when identified as Not valid. Default=Yellow + validBackgroundColour= Control Background color when identified as Valid. Default=white + + + The following parameters control the default foreground color coloring behavior of the + control. Pass a color as string e.g. 'Yellow': + foregroundColour= Control foreground color when value is not negative. Default=Black + signedForegroundColour= Control foreground color when value is negative. Default=Red + + +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 + groupChar # if set for a field, it will override the control's default + defaultValue # sets field-specific default value; overrides any default from control + compareNoCase # overrides control's settings + emptyInvalid # determines whether field is required to be filled at all times + validRequired # if set, requires field to contain valid value + + If any of the above parameters are subsequently specified for the control as a + whole, that new value will be propagated to each field, unless the + retainFieldValidation control-level parameter is set. + + formatcodes # Augments control's settings + excludeChars # ' ' ' + includeChars # ' ' ' + validRegex # ' ' ' + validRange # ' ' ' + choices # ' ' ' + choiceRequired # ' ' ' + validFunc # ' ' ' + + + +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 replacement" of passed value into the control, as does + the .Paste() method. As with other text entry controls, the + .SetValue() text replacement begins at left-edge of the control, + with missing mask characters inserted as appropriate. + .SetValue will also adjust integer, float or date mask entry values, + adding commas, auto-completing years, etc. as appropriate. + For "right-aligned" numeric controls, it will also now automatically + right-adjust any value whose length is less than the width of the + control before attempting to set the value. + If a value does not follow the format of the control's mask, or will + not fit into the control, a ValueError exception will be raised. + Eg: + mask = '(###) ###-####' + .SetValue('1234567890') => '(123) 456-7890' + .SetValue('(123)4567890') => '(123) 456-7890' + .SetValue('(123)456-7890') => '(123) 456-7890' + .SetValue('123/4567-890') => illegal paste; ValueError + + mask = '#{6}.#{2}', formatcodes = '_,-', + .SetValue('111') => ' 111 . ' + .SetValue(' %9.2f' % -111.12345 ) => ' -111.12' + .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00' + .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError + + mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control + .SetValue('111') => padded value misalignment ValueError: " 111" will not fit + .SetValue('%.2f' % 111 ) => ' 111.00' + .SetValue('%.2f' % -111.12345 ) => ' -111.12' + + + .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. + + .SetCtrlParameters(**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 = masked.TextCtrl( self, -1 ) + ctl.SetCtrlParameters( mask='###-####', + defaultValue='555-1212', + formatcodes='F') + + .GetCtrlParameter(parametername) + This function allows you to retrieve the current value of a parameter + from the control. + + Note: Each of the control parameters can also be set using its + own Set and Get function. These functions follow a regular form: + All of the parameter names start with lower case; for their + corresponding Set/Get function, the parameter name is capitalized. + Eg: ctl.SetMask('###-####') + ctl.SetDefaultValue('555-1212') + ctl.GetChoiceRequired() + ctl.GetFormatcodes() + + Note: After any change in parameters, the choices for the + control are reevaluated to ensure that they are still legal. If you + have large choice lists, it is therefore more efficient to set parameters + before setting the choices available. + + .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 point. Without a decimal (e.g. '######', the control will treat it as an integer +value. With a decimal (e.g. '###.##'), the control will act as a floating point control +(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the +integer control truncates the value. However, for a true numeric control, +masked.NumCtrl provides all this, and true numeric input/output support as well. + + +Check your controls by calling each control's .IsValid() function and the +.IsEmpty() function to determine which controls have been a) filled in and +b) filled in properly. + + +Regular expression validations can be used flexibly and creatively. +Take a look at the demo; the zip-code validation succeeds as long as the +first five numerals are entered. the last four are optional, but if +any are entered, there must be 4 to be valid. + +masked.Ctrl Configuration +========================== +masked.Ctrl works by looking for a special controlType +parameter in the variable arguments of the control, to determine +what kind of instance to return. +controlType can be one of: + + controlTypes.TEXT + controlTypes.COMBO + controlTypes.IPADDR + controlTypes.TIME + controlTypes.NUMBER + +These constants are also available individually, ie, you can +use either of the following: + + from wxPython.wx.lib.masked import MaskedCtrl, controlTypes + from wxPython.wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR + +If not specified as a keyword argument, the default controlType is +controlTypes.TEXT. +""" + +""" ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +DEVELOPER COMMENTS: + +Naming Conventions +------------------ + All methods of the Mixin that are not meant to be exposed to the external + interface are prefaced with '_'. Those functions that are primarily + intended to be internal subroutines subsequently start with a lower-case + letter; those that are primarily intended to be used and/or overridden + by derived subclasses start with a capital letter. + + The following methods must be used and/or defined when deriving a control + from MaskedEditMixin. NOTE: if deriving from a *masked edit* control + (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary, + as it's already been done for you in the base class. + + ._SetInitialValue() + This function must be called after the associated base + control has been initialized in the subclass __init__ + function. It sets the initial value of the control, + either to the value specified if non-empty, the + default value if specified, or the "template" for + the empty control as necessary. It will also set/reset + the font if necessary and apply formatting to the + control at this time. + + ._GetSelection() + REQUIRED + Each class derived from MaskedEditMixin must define + the function for getting the start and end of the + current text selection. The reason for this is + that not all controls have the same function name for + doing this; eg. wxTextCtrl uses .GetSelection(), + whereas we had to write a .GetMark() function for + wxComboBox, because .GetSelection() for the control + gets the currently selected list item from the combo + box, and the control doesn't (yet) natively provide + a means of determining the text selection. + ._SetSelection() + REQUIRED + Similarly to _GetSelection, each class derived from + MaskedEditMixin must define the function for setting + the start and end of the current text selection. + (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for + masked.ComboBox. + + ._GetInsertionPoint() + ._SetInsertionPoint() + REQUIRED + For consistency, and because the mixin shouldn't rely + on fixed names for any manipulations it does of any of + the base controls, we require each class derived from + MaskedEditMixin to define these functions as well. + + ._GetValue() + ._SetValue() REQUIRED + Each class derived from MaskedEditMixin must define + the functions used to get and set the raw value of the + control. + This is necessary so that recursion doesn't take place + when setting the value, and so that the mixin can + call the appropriate function after doing all its + validation and manipulation without knowing what kind + of base control it was mixed in with. To handle undo + functionality, the ._SetValue() must record the current + selection prior to setting the value. + + .Cut() + .Paste() + .Undo() + .SetValue() REQUIRED + Each class derived from MaskedEditMixin must redefine + these functions to call the _Cut(), _Paste(), _Undo() + and _SetValue() methods, respectively for the control, + so as to prevent programmatic corruption of the control's + value. This must be done in each derivation, as the + mixin cannot itself override a member of a sibling class. + + ._Refresh() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to refresh the base control. + + .Refresh() REQUIRED + Each class derived from MaskedEditMixin must redefine + this function so that it checks the validity of the + control (via self._CheckValid) and then refreshes + control using the base class method. + + ._IsEditable() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to determine if the base control is + editable or not. (For masked.ComboBox, this has to + be done with code, rather than specifying the proper + function in the base control, as there isn't one...) + ._CalcSize() REQUIRED + Each class derived from MaskedEditMixin must define + the function used to determine how wide the control + should be given the mask. (The mixin function + ._calcSize() provides a baseline estimate.) + + +Event Handling +-------------- + Event handlers are "chained", and MaskedEditMixin usually + swallows most of the events it sees, thereby preventing any other + handlers from firing in the chain. It is therefore required that + each class derivation using the mixin to have an option to hook up + the event handlers itself or forego this operation and let a + subclass of the masked control do so. For this reason, each + subclass should probably include the following code: + + if setupEventHandling: + ## Setup event handlers + EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection + EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator + EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick + EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu + EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + EVT_CHAR( self, self._OnChar ) ## handle each keypress + EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + where setupEventHandling is an argument to its constructor. + + These 5 handlers must be "wired up" for the masked edit + controls to provide default behavior. (The setupEventHandling + is an argument to masked.TextCtrl and masked.ComboBox, so + that controls derived from *them* may replace one of these + handlers if they so choose.) + + If your derived control wants to preprocess events before + taking action, it should then set up the event handling itself, + so it can be first in the event handler chain. + + + The following routines are available to facilitate changing + the default behavior of masked edit controls: + + ._SetKeycodeHandler(keycode, func) + ._SetKeyHandler(char, func) + Use to replace default handling for any given keycode. + func should take the key event as argument and return + False if no further action is required to handle the + key. Eg: + self._SetKeycodeHandler(WXK_UP, self.IncrementValue) + self._SetKeyHandler('-', self._OnChangeSign) + + "Navigation" keys are assumed to change the cursor position, and + therefore don't cause automatic motion of the cursor as insertable + characters do. + + ._AddNavKeycode(keycode, handler=None) + ._AddNavKey(char, handler=None) + Allows controls to specify other keys (and optional handlers) + to be treated as navigational characters. (eg. '.' in IpAddrCtrl) + + ._GetNavKeycodes() Returns the current list of navigational keycodes. + + ._SetNavKeycodes(key_func_tuples) + Allows replacement of the current list of keycode + processed as navigation keys, and bind associated + optional keyhandlers. argument is a list of key/handler + tuples. Passing a value of None for the handler in a + given tuple indicates that default processing for the key + is desired. + + ._FindField(pos) Returns the Field object associated with this position + in the control. + + ._FindFieldExtent(pos, getslice=False, value=None) + Returns edit_start, edit_end of the field corresponding + to the specified position within the control, and + optionally also returns the current contents of that field. + If value is specified, it will retrieve the slice the corresponding + slice from that value, rather than the current value of the + control. + + ._AdjustField(pos) + This is, the function that gets called for a given position + whenever the cursor is adjusted to leave a given field. + By default, it adjusts the year in date fields if mask is a date, + It can be overridden by a derived class to + adjust the value of the control at that time. + (eg. IpAddrCtrl reformats the address in this way.) + + ._Change() Called by internal EVT_TEXT handler. Return False to force + skip of the normal class change event. + ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force + skip of the normal class keypress event. + ._LostFocus() Called by internal EVT_KILL_FOCUS handler + + ._OnKeyDown(event) + This is the default EVT_KEY_DOWN routine; it just checks for + "navigation keys", and if event.ControlDown(), it fires the + mixin's _OnChar() routine, as such events are not always seen + by the "cooked" EVT_CHAR routine. + + ._OnChar(event) This is the main EVT_CHAR handler for the + MaskedEditMixin. + + The following routines are used to handle standard actions + for control keys: + _OnArrow(event) used for arrow navigation events + _OnCtrl_A(event) 'select all' + _OnCtrl_C(event) 'copy' (uses base control function, as copy is non-destructive) + _OnCtrl_S(event) 'save' (does nothing) + _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste + _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection + _OnCtrl_Z(event) 'undo' - resets value to previous value (if any) + + _OnChangeField(event) primarily used for tab events, but can be + used for other keys (eg. '.' in IpAddrCtrl) + + _OnErase(event) used for backspace and delete + _OnHome(event) + _OnEnd(event) + + The following routine provides a hook back to any class derivations, so that + they can react to parameter changes before any value is set/reset as a result of + those changes. (eg. masked.ComboBox needs to detect when the choices list is + modified, either implicitly or explicitly, so it can reset the base control + to have the appropriate choice list *before* the initial value is reset to match.) + + _OnCtrlParametersChanged() + +Accessor Functions +------------------ + For convenience, each class derived from MaskedEditMixin should + define an accessors mixin, so that it exposes only those parameters + that make sense for the derivation. This is done with an intermediate + level of inheritance, ie: + + class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): + + class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): + class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): + class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): + class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): + class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): + + etc. + + Each accessors mixin defines Get/Set functions for the base class parameters + that are appropriate for that derivation. + This allows the base classes to be "more generic," exposing the widest + set of options, while not requiring derived classes to be so general. +""" + +import copy +import difflib +import re +import string +import types + +import wx + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger + +dbg = Logger() +##dbg(enable=0) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## Constants for identifying control keys and classes of keys: + +WXK_CTRL_A = (ord('A')+1) - ord('A') ## These keys are not already defined in wx +WXK_CTRL_C = (ord('C')+1) - ord('A') +WXK_CTRL_S = (ord('S')+1) - ord('A') +WXK_CTRL_V = (ord('V')+1) - ord('A') +WXK_CTRL_X = (ord('X')+1) - ord('A') +WXK_CTRL_Z = (ord('Z')+1) - ord('A') + +nav = ( + wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, + wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT + ) + +control = ( + wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, + WXK_CTRL_X, WXK_CTRL_Z + ) + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## Constants for masking. This is where mask characters +## are defined. +## maskchars used to identify valid mask characters from all others +## #- allow numeric 0-9 only +## A- allow uppercase only. Combine with forceupper to force lowercase to upper +## a- allow lowercase only. Combine with forcelower to force upper to lowercase +## X- allow any character (string.letters, string.punctuation, string.digits) +## Note: locale settings affect what "uppercase", lowercase, etc comprise. +## +maskchars = ("#","A","a","X","C","N", '&') + +months = '(01|02|03|04|05|06|07|08|09|10|11|12)' +charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)' +charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, + 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} + +days = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)' +hours = '(0\d| \d|1[012])' +milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)' +minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ +16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ +36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ +56|57|58|59)""" +seconds = minutes +am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde' + +states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',') + +state_names = ['Alabama','Alaska','Arizona','Arkansas', + 'California','Colorado','Connecticut', + 'Delaware','District of Columbia', + 'Florida','Georgia','Hawaii', + 'Idaho','Illinois','Indiana','Iowa', + 'Kansas','Kentucky','Louisiana', + 'Maine','Maryland','Massachusetts','Michigan', + 'Minnesota','Mississippi','Missouri','Montana', + 'Nebraska','Nevada','New Hampshire','New Jersey', + 'New Mexico','New York','North Carolina','North Dakokta', + 'Ohio','Oklahoma','Oregon', + 'Pennsylvania','Puerto Rico','Rhode Island', + 'South Carolina','South Dakota', + 'Tennessee','Texas','Utah', + 'Vermont','Virginia', + 'Washington','West Virginia', + 'Wisconsin','Wyoming'] + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +## The following dictionary defines the current set of autoformats: + +masktags = { + "USPHONEFULLEXT": { + 'mask': "(###) ###-#### x:###", + 'formatcodes': 'F^->', + 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", + 'description': "Phone Number w/opt. ext" + }, + "USPHONETIGHTEXT": { + 'mask': "###-###-#### x:###", + 'formatcodes': 'F^->', + 'validRegex': "^\d{3}-\d{3}-\d{4}", + 'description': "Phone Number\n (w/hyphens and opt. ext)" + }, + "USPHONEFULL": { + 'mask': "(###) ###-####", + 'formatcodes': 'F^->', + 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", + 'description': "Phone Number only" + }, + "USPHONETIGHT": { + 'mask': "###-###-####", + 'formatcodes': 'F^->', + 'validRegex': "^\d{3}-\d{3}-\d{4}", + 'description': "Phone Number\n(w/hyphens)" + }, + "USSTATE": { + 'mask': "AA", + 'formatcodes': 'F!V', + 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), + 'choices': states, + 'choiceRequired': True, + 'description': "US State Code" + }, + "USSTATENAME": { + 'mask': "ACCCCCCCCCCCCCCCCCCC", + 'formatcodes': 'F_', + 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'), + 'choices': state_names, + 'choiceRequired': True, + 'description': "US State Name" + }, + + "USDATETIMEMMDDYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "US Date + Time" + }, + "USDATETIMEMMDDYYYY-HHMMSS": { + 'mask': "##-##-#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "US Date + Time\n(w/hypens)" + }, + "USDATE24HRTIMEMMDDYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "US Date + 24Hr (Military) Time" + }, + "USDATE24HRTIMEMMDDYYYY-HHMMSS": { + 'mask': "##-##-#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "US Date + 24Hr Time\n(w/hypens)" + }, + "USDATETIMEMMDDYYYY/HHMM": { + 'mask': "##/##/#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "US Date + Time\n(without seconds)" + }, + "USDATE24HRTIMEMMDDYYYY/HHMM": { + 'mask': "##/##/#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes, + 'description': "US Date + 24Hr Time\n(without seconds)" + }, + "USDATETIMEMMDDYYYY-HHMM": { + 'mask': "##-##-#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "US Date + Time\n(w/hypens and w/o secs)" + }, + "USDATE24HRTIMEMMDDYYYY-HHMM": { + 'mask': "##-##-#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes, + 'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" + }, + "USDATEMMDDYYYY/": { + 'mask': "##/##/####", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/' + '\d{4}', + 'description': "US Date\n(MMDDYYYY)" + }, + "USDATEMMDDYY/": { + 'mask': "##/##/##", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '/' + days + '/\d\d', + 'description': "US Date\n(MMDDYY)" + }, + "USDATEMMDDYYYY-": { + 'mask': "##-##-####", + 'formatcodes': 'DF', + 'validRegex': '^' + months + '-' + days + '-' +'\d{4}', + 'description': "MM-DD-YYYY" + }, + + "EUDATEYYYYMMDD/": { + 'mask': "####/##/##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days, + 'description': "YYYY/MM/DD" + }, + "EUDATEYYYYMMDD.": { + 'mask': "####.##.##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days, + 'description': "YYYY.MM.DD" + }, + "EUDATEDDMMYYYY/": { + 'mask': "##/##/####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4}', + 'description': "DD/MM/YYYY" + }, + "EUDATEDDMMYYYY.": { + 'mask': "##.##.####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4}', + 'description': "DD.MM.YYYY" + }, + "EUDATEDDMMMYYYY.": { + 'mask': "##.CCC.####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}', + 'description': "DD.Month.YYYY" + }, + "EUDATEDDMMMYYYY/": { + 'mask': "##/CCC/####", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}', + 'description': "DD/Month/YYYY" + }, + + "EUDATETIMEYYYYMMDD/HHMMSS": { + 'mask': "####/##/## ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "YYYY/MM/DD HH:MM:SS" + }, + "EUDATETIMEYYYYMMDD.HHMMSS": { + 'mask': "####.##.## ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "YYYY.MM.DD HH:MM:SS" + }, + "EUDATETIMEDDMMYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "DD/MM/YYYY HH:MM:SS" + }, + "EUDATETIMEDDMMYYYY.HHMMSS": { + 'mask': "##.##.#### ##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "DD.MM.YYYY HH:MM:SS" + }, + + "EUDATETIMEYYYYMMDD/HHMM": { + 'mask': "####/##/## ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M', + 'description': "YYYY/MM/DD HH:MM" + }, + "EUDATETIMEYYYYMMDD.HHMM": { + 'mask': "####.##.## ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M', + 'description': "YYYY.MM.DD HH:MM" + }, + "EUDATETIMEDDMMYYYY/HHMM": { + 'mask': "##/##/#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "DD/MM/YYYY HH:MM" + }, + "EUDATETIMEDDMMYYYY.HHMM": { + 'mask': "##.##.#### ##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'DF!', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', + 'description': "DD.MM.YYYY HH:MM" + }, + + "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { + 'mask': "####/##/## ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds, + 'description': "YYYY/MM/DD 24Hr Time" + }, + "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { + 'mask': "####.##.## ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds, + 'description': "YYYY.MM.DD 24Hr Time" + }, + "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { + 'mask': "##/##/#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "DD/MM/YYYY 24Hr Time" + }, + "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { + 'mask': "##.##.#### ##:##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, + 'description': "DD.MM.YYYY 24Hr Time" + }, + "EUDATE24HRTIMEYYYYMMDD/HHMM": { + 'mask': "####/##/## ##:##", + 'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes, + 'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEYYYYMMDD.HHMM": { + 'mask': "####.##.## ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes, + 'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEDDMMYYYY/HHMM": { + 'mask': "##/##/#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes, + 'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" + }, + "EUDATE24HRTIMEDDMMYYYY.HHMM": { + 'mask': "##.##.#### ##:##", + 'formatcodes': 'DF', + 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes, + 'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" + }, + + "TIMEHHMMSS": { + 'mask': "##:##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'TF!', + 'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', + 'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" + }, + "TIMEHHMM": { + 'mask': "##:## AM", + 'excludeChars': am_pm_exclude, + 'formatcodes': 'TF!', + 'validRegex': '^' + hours + ':' + minutes + ' (A|P)M', + 'description': "HH:MM (A|P)M\n(see TimeCtrl)" + }, + "24HRTIMEHHMMSS": { + 'mask': "##:##:##", + 'formatcodes': 'TF', + 'validRegex': '^' + milhours + ':' + minutes + ':' + seconds, + 'description': "24Hr HH:MM:SS\n(see TimeCtrl)" + }, + "24HRTIMEHHMM": { + 'mask': "##:##", + 'formatcodes': 'TF', + 'validRegex': '^' + milhours + ':' + minutes, + 'description': "24Hr HH:MM\n(see TimeCtrl)" + }, + "USSOCIALSEC": { + 'mask': "###-##-####", + 'formatcodes': 'F', + 'validRegex': "\d{3}-\d{2}-\d{4}", + 'description': "Social Sec#" + }, + "CREDITCARD": { + 'mask': "####-####-####-####", + 'formatcodes': 'F', + 'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", + 'description': "Credit Card" + }, + "EXPDATEMMYY": { + 'mask': "##/##", + 'formatcodes': "F", + 'validRegex': "^" + months + "/\d\d", + 'description': "Expiration MM/YY" + }, + "USZIP": { + 'mask': "#####", + 'formatcodes': 'F', + 'validRegex': "^\d{5}", + 'description': "US 5-digit zip code" + }, + "USZIPPLUS4": { + 'mask': "#####-####", + 'formatcodes': 'F', + 'validRegex': "\d{5}-(\s{4}|\d{4})", + 'description': "US zip+4 code" + }, + "PERCENT": { + 'mask': "0.##", + 'formatcodes': 'F', + 'validRegex': "^0.\d\d", + 'description': "Percentage" + }, + "AGE": { + 'mask': "###", + 'formatcodes': "F", + 'validRegex': "^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]", + 'description': "Age" + }, + "EMAIL": { + 'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + 'excludeChars': " \\/*&%$#!+='\"", + 'formatcodes': "F>", + 'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$", + 'description': "Email address" + }, + "IPADDR": { + 'mask': "###.###.###.###", + 'formatcodes': 'F_Sr', + 'validRegex': "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}", + 'description': "IP Address\n(see IpAddrCtrl)" + } + } + +# build demo-friendly dictionary of descriptions of autoformats +autoformats = [] +for key, value in masktags.items(): + autoformats.append((key, value['description'])) +autoformats.sort() + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class Field: + valid_params = { + 'index': None, ## which field of mask; set by parent control. + 'mask': "", ## mask chars for this field + 'extent': (), ## (edit start, edit_end) of field; set by parent control. + 'formatcodes': "", ## codes indicating formatting options for the control + 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given + 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits + 'decimalChar': '.', ## used with numeric fields; indicates what char separates integer from fraction + 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard + 'useParensForNegatives': False, ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. + 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar + 'excludeChars': "", ## optional string of chars to exclude even if main mask type does + 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't + 'validRegex': "", ## optional regular expression to use to validate the control + 'validRange': (), ## Optional hi-low range for numerics + 'choices': [], ## Optional list for character expressions + 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list + 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search + 'autoSelect': False, ## Set to True to try auto-completion on each keystroke: + 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl + 'validRequired': False, ## Set to True to disallow input that results in an invalid value + 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID + 'description': "", ## primarily for autoformats, but could be useful elsewhere + } + + # This list contains all parameters that when set at the control level should + # propagate down to each field: + propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', + 'compareNoCase', 'emptyInvalid', 'validRequired') + + def __init__(self, **kwargs): + """ + This is the "constructor" for setting up parameters for fields. + a field_index of -1 is used to indicate "the entire control." + """ +#### dbg('Field::Field', indent=1) + # Validate legitimate set of parameters: + for key in kwargs.keys(): + if key not in Field.valid_params.keys(): +#### dbg(indent=0) + raise TypeError('invalid parameter "%s"' % (key)) + + # Set defaults for each parameter for this instance, and fully + # populate initial parameter list for configuration: + for key, value in Field.valid_params.items(): + setattr(self, '_' + key, copy.copy(value)) + if not kwargs.has_key(key): + kwargs[key] = copy.copy(value) + + self._autoCompleteIndex = -1 + self._SetParameters(**kwargs) + self._ValidateParameters(**kwargs) + +#### dbg(indent=0) + + + def _SetParameters(self, **kwargs): + """ + This function can be used to set individual or multiple parameters for + a masked edit field parameter after construction. + """ +## dbg(suspend=1) +## dbg('maskededit.Field::_SetParameters', indent=1) + # Validate keyword arguments: + for key in kwargs.keys(): + if key not in Field.valid_params.keys(): +## dbg(indent=0, suspend=0) + raise AttributeError('invalid keyword argument "%s"' % key) + + if self._index is not None: dbg('field index:', self._index) +## dbg('parameters:', indent=1) + for key, value in kwargs.items(): +## dbg('%s:' % key, value) + pass +## dbg(indent=0) + + + old_fillChar = self._fillChar # store so we can change choice lists accordingly if it changes + + # First, Assign all parameters specified: + for key in Field.valid_params.keys(): + if kwargs.has_key(key): + setattr(self, '_' + key, kwargs[key] ) + + if kwargs.has_key('formatcodes'): # (set/changed) + self._forceupper = '!' in self._formatcodes + self._forcelower = '^' in self._formatcodes + self._groupdigits = ',' in self._formatcodes + self._okSpaces = '_' in self._formatcodes + self._padZero = '0' in self._formatcodes + self._autofit = 'F' in self._formatcodes + self._insertRight = 'r' in self._formatcodes + self._allowInsert = '>' in self._formatcodes + self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes + self._moveOnFieldFull = not '<' in self._formatcodes + self._selectOnFieldEntry = 'S' in self._formatcodes + + if kwargs.has_key('groupChar'): + self._groupChar = kwargs['groupChar'] + if kwargs.has_key('decimalChar'): + self._decimalChar = kwargs['decimalChar'] + if kwargs.has_key('shiftDecimalChar'): + self._shiftDecimalChar = kwargs['shiftDecimalChar'] + + if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'): + self._regexMask = 'V' in self._formatcodes and self._validRegex + + if kwargs.has_key('fillChar'): + self._old_fillChar = old_fillChar +#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) + + if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed) + self._isInt = isInteger(self._mask) +## dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) + +## dbg(indent=0, suspend=0) + + + def _ValidateParameters(self, **kwargs): + """ + This function can be used to validate individual or multiple parameters for + a masked edit field parameter after construction. + """ +## dbg(suspend=1) +## dbg('maskededit.Field::_ValidateParameters', indent=1) + if self._index is not None: dbg('field index:', self._index) +#### dbg('parameters:', indent=1) +## for key, value in kwargs.items(): +#### dbg('%s:' % key, value) +#### dbg(indent=0) +#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) + + # Verify proper numeric format params: + if self._groupdigits and self._groupChar == self._decimalChar: +## dbg(indent=0, suspend=0) + raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar)) + + + # Now go do validation, semantic and inter-dependency parameter processing: + if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed) + + self._compareChoices = [choice.strip() for choice in self._choices] + + if self._compareNoCase and self._choices: + self._compareChoices = [item.lower() for item in self._compareChoices] + + if kwargs.has_key('choices'): + self._autoCompleteIndex = -1 + + + if kwargs.has_key('validRegex'): # (set/changed) + if self._validRegex: + try: + if self._compareNoCase: + self._filter = re.compile(self._validRegex, re.IGNORECASE) + else: + self._filter = re.compile(self._validRegex) + except: +## dbg(indent=0, suspend=0) + raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex)) + else: + self._filter = None + + if kwargs.has_key('validRange'): # (set/changed) + self._hasRange = False + self._rangeHigh = 0 + self._rangeLow = 0 + if self._validRange: + if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]: +## dbg(indent=0, suspend=0) + raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' + % (str(self._index), repr(self._validRange)) ) + + self._hasRange = True + self._rangeLow = self._validRange[0] + self._rangeHigh = self._validRange[1] + + if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)): # (set/changed) + self._hasList = False + if self._choices and type(self._choices) not in (types.TupleType, types.ListType): +## dbg(indent=0, suspend=0) + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + elif len( self._choices) > 0: + for choice in self._choices: + if type(choice) not in (types.StringType, types.UnicodeType): +## dbg(indent=0, suspend=0) + raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) + + length = len(self._mask) +## dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) + if len(self._choices) and length: + if len(self._choices[0]) > length: + # changed mask without respecifying choices; readjust the width as appropriate: + self._choices = [choice.strip() for choice in self._choices] + if self._alignRight: + self._choices = [choice.rjust( length ) for choice in self._choices] + else: + self._choices = [choice.ljust( length ) for choice in self._choices] +## dbg('aligned choices:', self._choices) + + if hasattr(self, '_template'): + # Verify each choice specified is valid: + for choice in self._choices: + if self.IsEmpty(choice) and not self._validRequired: + # allow empty values even if invalid, (just colored differently) + continue + if not self.IsValid(choice): +## dbg(indent=0, suspend=0) + raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) + self._hasList = True + +#### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) +#### dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) + if kwargs.has_key('fillChar') and len(self._choices) > 0: + if kwargs['fillChar'] != ' ': + self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices] + else: + self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices] +## dbg('updated choices:', self._choices) + + + if kwargs.has_key('autoSelect') and kwargs['autoSelect']: + if not self._hasList: +## dbg('no list to auto complete; ignoring "autoSelect=True"') + self._autoSelect = False + + # reset field validity assumption: + self._valid = True +## dbg(indent=0, suspend=0) + + + def _GetParameter(self, paramname): + """ + Routine for retrieving the value of any given parameter + """ + if Field.valid_params.has_key(paramname): + return getattr(self, '_' + paramname) + else: + TypeError('Field._GetParameter: invalid parameter "%s"' % key) + + + def IsEmpty(self, slice): + """ + Indicates whether the specified slice is considered empty for the + field. + """ +## dbg('Field::IsEmpty("%s")' % slice, indent=1) + if not hasattr(self, '_template'): +## dbg(indent=0) + raise AttributeError('_template') + +## dbg('self._template: "%s"' % self._template) +## dbg('self._defaultValue: "%s"' % str(self._defaultValue)) + if slice == self._template and not self._defaultValue: +## dbg(indent=0) + return True + + elif slice == self._template: + empty = True + for pos in range(len(self._template)): +#### dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) + if slice[pos] not in (' ', self._fillChar): + empty = False + break +## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) + return empty + else: +## dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) + return False + + + def IsValid(self, slice): + """ + Indicates whether the specified slice is considered a valid value for the + field. + """ +## dbg(suspend=1) +## dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) + valid = True # assume true to start + + if self.IsEmpty(slice): +## dbg(indent=0, suspend=0) + if self._emptyInvalid: + return False + else: + return True + + elif self._hasList and self._choiceRequired: +## dbg("(member of list required)") + # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): + if self._fillChar != ' ': + slice = slice.replace(self._fillChar, ' ') +## dbg('updated slice:"%s"' % slice) + compareStr = slice.strip() + + if self._compareNoCase: + compareStr = compareStr.lower() + valid = compareStr in self._compareChoices + + elif self._hasRange and not self.IsEmpty(slice): +## dbg('validating against range') + try: + # allow float as well as int ranges (int comparisons for free.) + valid = self._rangeLow <= float(slice) <= self._rangeHigh + except: + valid = False + + elif self._validRegex and self._filter: +## dbg('validating against regex') + valid = (re.match( self._filter, slice) is not None) + + if valid and self._validFunc: +## dbg('validating against supplied function') + valid = self._validFunc(slice) +## dbg('valid?', valid, indent=0, suspend=0) + return valid + + + def _AdjustField(self, slice): + """ 'Fixes' an integer field. Right or left-justifies, as required.""" +## dbg('Field::_AdjustField("%s")' % slice, indent=1) + length = len(self._mask) +#### dbg('length(self._mask):', length) +#### dbg('self._useParensForNegatives?', self._useParensForNegatives) + if self._isInt: + if self._useParensForNegatives: + signpos = slice.find('(') + right_signpos = slice.find(')') + intStr = slice.replace('(', '').replace(')', '') # drop sign, if any + else: + signpos = slice.find('-') + intStr = slice.replace( '-', '' ) # drop sign, if any + right_signpos = -1 + + intStr = intStr.replace(' ', '') # drop extra spaces + intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars + intStr = string.replace(intStr,"-","") # drop sign, if any + intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots +#### dbg('intStr:"%s"' % intStr) + start, end = self._extent + field_len = end - start + if not self._padZero and len(intStr) != field_len and intStr.strip(): + intStr = str(long(intStr)) +#### dbg('raw int str: "%s"' % intStr) +#### dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) + if self._groupdigits: + new = '' + cnt = 1 + for i in range(len(intStr)-1, -1, -1): + new = intStr[i] + new + if (cnt) % 3 == 0: + new = self._groupChar + new + cnt += 1 + if new and new[0] == self._groupChar: + new = new[1:] + if len(new) <= length: + # expanded string will still fit and leave room for sign: + intStr = new + # else... leave it without the commas... + +## dbg('padzero?', self._padZero) +## dbg('len(intStr):', len(intStr), 'field length:', length) + if self._padZero and len(intStr) < length: + intStr = '0' * (length - len(intStr)) + intStr + if signpos != -1: # we had a sign before; restore it + if self._useParensForNegatives: + intStr = '(' + intStr[1:] + if right_signpos != -1: + intStr += ')' + else: + intStr = '-' + intStr[1:] + elif signpos != -1 and slice[0:signpos].strip() == '': # - was before digits + if self._useParensForNegatives: + intStr = '(' + intStr + if right_signpos != -1: + intStr += ')' + else: + intStr = '-' + intStr + elif right_signpos != -1: + # must have had ')' but '(' was before field; re-add ')' + intStr += ')' + slice = intStr + + slice = slice.strip() # drop extra spaces + + if self._alignRight: ## Only if right-alignment is enabled + slice = slice.rjust( length ) + else: + slice = slice.ljust( length ) + if self._fillChar != ' ': + slice = slice.replace(' ', self._fillChar) +## dbg('adjusted slice: "%s"' % slice, indent=0) + return slice + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class MaskedEditMixin: + """ + This class allows us to abstract the masked edit functionality that could + be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.) + """ + valid_ctrl_params = { + 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control + 'autoformat': "", ## optional auto-format code to set format from masktags dictionary + 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask + 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year + 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete + 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control + 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently, + ## from field validation constraints + 'emptyBackgroundColour': "White", + 'validBackgroundColour': "White", + 'invalidBackgroundColour': "Yellow", + 'foregroundColour': "Black", + 'signedForegroundColour': "Red", + 'demo': False} + + + def __init__(self, name = 'MaskedEdit', **kwargs): + """ + This is the "constructor" for setting up the mixin variable parameters for the composite class. + """ + + self.name = name + + # set up flag for doing optional things to base control if possible + if not hasattr(self, 'controlInitialized'): + self.controlInitialized = False + + # Set internal state var for keeping track of whether or not a character + # action results in a modification of the control, since .SetValue() + # doesn't modify the base control's internal state: + self.modified = False + self._previous_mask = None + + # Validate legitimate set of parameters: + for key in kwargs.keys(): + if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): + raise TypeError('%s: invalid parameter "%s"' % (name, key)) + + ## Set up dictionary that can be used by subclasses to override or add to default + ## behavior for individual characters. Derived subclasses needing to change + ## default behavior for keys can either redefine the default functions for the + ## common keys or add functions for specific keys to this list. Each function + ## added should take the key event as argument, and return False if the key + ## requires no further processing. + ## + ## Initially populated with navigation and function control keys: + self._keyhandlers = { + # default navigation keys and handlers: + wx.WXK_BACK: self._OnErase, + wx.WXK_LEFT: self._OnArrow, + wx.WXK_RIGHT: self._OnArrow, + wx.WXK_UP: self._OnAutoCompleteField, + wx.WXK_DOWN: self._OnAutoCompleteField, + wx.WXK_TAB: self._OnChangeField, + wx.WXK_HOME: self._OnHome, + wx.WXK_END: self._OnEnd, + wx.WXK_RETURN: self._OnReturn, + wx.WXK_PRIOR: self._OnAutoCompleteField, + wx.WXK_NEXT: self._OnAutoCompleteField, + + # default function control keys and handlers: + wx.WXK_DELETE: self._OnErase, + WXK_CTRL_A: self._OnCtrl_A, + WXK_CTRL_C: self._OnCtrl_C, + WXK_CTRL_S: self._OnCtrl_S, + WXK_CTRL_V: self._OnCtrl_V, + WXK_CTRL_X: self._OnCtrl_X, + WXK_CTRL_Z: self._OnCtrl_Z, + } + + ## bind standard navigational and control keycodes to this instance, + ## so that they can be augmented and/or changed in derived classes: + self._nav = list(nav) + self._control = list(control) + + ## Dynamically evaluate and store string constants for mask chars + ## so that locale settings can be made after this module is imported + ## and the controls created after that is done can allow the + ## appropriate characters: + self.maskchardict = { + '#': string.digits, + 'A': string.uppercase, + 'a': string.lowercase, + 'X': string.letters + string.punctuation + string.digits, + 'C': string.letters, + 'N': string.letters + string.digits, + '&': string.punctuation + } + + ## self._ignoreChange is used by MaskedComboBox, because + ## of the hack necessary to determine the selection; it causes + ## EVT_TEXT messages from the combobox to be ignored if set. + self._ignoreChange = False + + # These are used to keep track of previous value, for undo functionality: + self._curValue = None + self._prevValue = None + + self._valid = True + + # Set defaults for each parameter for this instance, and fully + # populate initial parameter list for configuration: + for key, value in MaskedEditMixin.valid_ctrl_params.items(): + setattr(self, '_' + key, copy.copy(value)) + if not kwargs.has_key(key): +#### dbg('%s: "%s"' % (key, repr(value))) + kwargs[key] = copy.copy(value) + + # Create a "field" that holds global parameters for control constraints + self._ctrl_constraints = self._fields[-1] = Field(index=-1) + self.SetCtrlParameters(**kwargs) + + + + def SetCtrlParameters(self, **kwargs): + """ + This public function can be used to set individual or multiple masked edit + parameters after construction. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::SetCtrlParameters', indent=1) +#### dbg('kwargs:', indent=1) +## for key, value in kwargs.items(): +#### dbg(key, '=', value) +#### dbg(indent=0) + + # Validate keyword arguments: + constraint_kwargs = {} + ctrl_kwargs = {} + for key, value in kwargs.items(): + key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling + if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): +## dbg(indent=0, suspend=0) + raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name)) + elif key in Field.valid_params.keys(): + constraint_kwargs[key] = value + else: + ctrl_kwargs[key] = value + + mask = None + reset_args = {} + + if ctrl_kwargs.has_key('autoformat'): + autoformat = ctrl_kwargs['autoformat'] + else: + autoformat = None + + # handle "parochial name" backward compatibility: + if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys(): + autoformat = autoformat.replace('MILTIME', '24HRTIME') + + if autoformat != self._autoformat and autoformat in masktags.keys(): +## dbg('autoformat:', autoformat) + self._autoformat = autoformat + mask = masktags[self._autoformat]['mask'] + # gather rest of any autoformat parameters: + for param, value in masktags[self._autoformat].items(): + if param == 'mask': continue # (must be present; already accounted for) + constraint_kwargs[param] = value + + elif autoformat and not autoformat in masktags.keys(): + raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat)) + else: +## dbg('autoformat not selected') + if kwargs.has_key('mask'): + mask = kwargs['mask'] +## dbg('mask:', mask) + + ## Assign style flags + if mask is None: +## dbg('preserving previous mask') + mask = self._previous_mask # preserve previous mask + else: +## dbg('mask (re)set') + reset_args['reset_mask'] = mask + constraint_kwargs['mask'] = mask + + # wipe out previous fields; preserve new control-level constraints + self._fields = {-1: self._ctrl_constraints} + + + if ctrl_kwargs.has_key('fields'): + # do field parameter type validation, and conversion to internal dictionary + # as appropriate: + fields = ctrl_kwargs['fields'] + if type(fields) in (types.ListType, types.TupleType): + for i in range(len(fields)): + field = fields[i] + if not isinstance(field, Field): +## dbg(indent=0, suspend=0) + raise AttributeError('invalid type for field parameter: %s' % repr(field)) + self._fields[i] = field + + elif type(fields) == types.DictionaryType: + for index, field in fields.items(): + if not isinstance(field, Field): +## dbg(indent=0, suspend=0) + raise AttributeError('invalid type for field parameter: %s' % repr(field)) + self._fields[index] = field + else: +## dbg(indent=0, suspend=0) + raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields)) + + # Assign constraint parameters for entire control: +#### dbg('control constraints:', indent=1) +## for key, value in constraint_kwargs.items(): +#### dbg('%s:' % key, value) +#### dbg(indent=0) + + # determine if changing parameters that should affect the entire control: + for key in MaskedEditMixin.valid_ctrl_params.keys(): + if key in ( 'mask', 'fields' ): continue # (processed separately) + if ctrl_kwargs.has_key(key): + setattr(self, '_' + key, ctrl_kwargs[key]) + + # Validate color parameters, converting strings to named colors and validating + # result if appropriate: + for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', + 'foregroundColour', 'signedForegroundColour'): + if ctrl_kwargs.has_key(key): + if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType): + c = wx.NamedColour(ctrl_kwargs[key]) + if c.Get() == (-1, -1, -1): + raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) + else: + # replace attribute with wxColour object: + setattr(self, '_' + key, c) + # attach a python dynamic attribute to wxColour for debug printouts + c._name = ctrl_kwargs[key] + + elif type(ctrl_kwargs[key]) != type(wx.BLACK): + raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) + + +## dbg('self._retainFieldValidation:', self._retainFieldValidation) + if not self._retainFieldValidation: + # Build dictionary of any changing parameters which should be propagated to the + # component fields: + for arg in Field.propagating_params: +#### dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) +#### dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) + reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg) +#### dbg('reset_args[%s]?' % arg, reset_args[arg]) + + # Set the control-level constraints: + self._ctrl_constraints._SetParameters(**constraint_kwargs) + + # This routine does the bulk of the interdependent parameter processing, determining + # the field extents of the mask if changed, resetting parameters as appropriate, + # determining the overall template value for the control, etc. + self._configure(mask, **reset_args) + + # now that we've propagated the field constraints and mask portions to the + # various fields, validate the constraints + self._ctrl_constraints._ValidateParameters(**constraint_kwargs) + + # Validate that all choices for given fields are at least of the + # necessary length, and that they all would be valid pastes if pasted + # into their respective fields: +#### dbg('validating choices') + self._validateChoices() + + + self._autofit = self._ctrl_constraints._autofit + self._isNeg = False + + self._isDate = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask) + self._isTime = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask) + if self._isDate: + # Set _dateExtent, used in date validation to locate date in string; + # always set as though year will be 4 digits, even if mask only has + # 2 digits, so we can always properly process the intended year for + # date validation (leap years, etc.) + if self._mask.find('CCC') != -1: self._dateExtent = 11 + else: self._dateExtent = 10 + + self._4digityear = len(self._mask) > 8 and self._mask[9] == '#' + + if self._isDate and self._autoformat: + # Auto-decide datestyle: + if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY' + elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD' + elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD' + elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY' + elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY' + + # Give derived controls a chance to react to parameter changes before + # potentially changing current value of the control. + self._OnCtrlParametersChanged() + + if self.controlInitialized: + # Then the base control is available for configuration; + # take action on base control based on new settings, as appropriate. + if kwargs.has_key('useFixedWidthFont'): + # Set control font - fixed width by default + self._setFont() + + if reset_args.has_key('reset_mask'): +## dbg('reset mask') + curvalue = self._GetValue() + if curvalue.strip(): + try: +## dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) + self._SetInitialValue(self._GetValue()) + except Exception, e: +## dbg('exception caught:', e) +## dbg("current value doesn't work; attempting to reset to template") + self._SetInitialValue() + else: +## dbg('attempting to _SetInitialValue() with template') + self._SetInitialValue() + + elif kwargs.has_key('useParensForNegatives'): + newvalue = self._getSignedValue()[0] + + if newvalue is not None: + # Adjust for new mask: + if len(newvalue) < len(self._mask): + newvalue += ' ' + elif len(newvalue) > len(self._mask): + if newvalue[-1] in (' ', ')'): + newvalue = newvalue[:-1] + +## dbg('reconfiguring value for parens:"%s"' % newvalue) + self._SetValue(newvalue) + + if self._prevValue != newvalue: + self._prevValue = newvalue # disallow undo of sign type + + if self._autofit: +## dbg('setting client size to:', self._CalcSize()) + size = self._CalcSize() + self.SetSizeHints(size) + self.SetClientSize(size) + + # Set value/type-specific formatting + self._applyFormatting() +## dbg(indent=0, suspend=0) + + def SetMaskParameters(self, **kwargs): + """ old name for this function """ + return self.SetCtrlParameters(**kwargs) + + + def GetCtrlParameter(self, paramname): + """ + Routine for retrieving the value of any given parameter + """ + if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')): + return getattr(self, '_' + paramname.replace('Color', 'Colour')) + elif Field.valid_params.has_key(paramname): + return self._ctrl_constraints._GetParameter(paramname) + else: + TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname)) + + def GetMaskParameter(self, paramname): + """ old name for this function """ + return self.GetCtrlParameter(paramname) + + +## This idea worked, but Boa was unable to use this solution... +## def _attachMethod(self, func): +## import new +## setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) +## +## +## def _DefinePropertyFunctions(exposed_params): +## for param in exposed_params: +## propname = param[0].upper() + param[1:] +## +## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) +## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) +## self._attachMethod(locals()['Set%s' % propname]) +## self._attachMethod(locals()['Get%s' % propname]) +## +## if param.find('Colour') != -1: +## # add non-british spellings, for backward-compatibility +## propname.replace('Colour', 'Color') +## +## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) +## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) +## self._attachMethod(locals()['Set%s' % propname]) +## self._attachMethod(locals()['Get%s' % propname]) +## + + + def SetFieldParameters(self, field_index, **kwargs): + """ + Routine provided to modify the parameters of a given field. + Because changes to fields can affect the overall control, + direct access to the fields is prevented, and the control + is always "reconfigured" after setting a field parameter. + """ + if field_index not in self._field_indices: + raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) + # set parameters as requested: + self._fields[field_index]._SetParameters(**kwargs) + + # Possibly reprogram control template due to resulting changes, and ensure + # control-level params are still propagated to fields: + self._configure(self._previous_mask) + self._fields[field_index]._ValidateParameters(**kwargs) + + if self.controlInitialized: + if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'): + self._SetInitialValue() + + if self._autofit: + size = self._CalcSize() + self.SetSizeHints(size) + self.SetClientSize(size) + + # Set value/type-specific formatting + self._applyFormatting() + + + def GetFieldParameter(self, field_index, paramname): + """ + Routine provided for getting a parameter of an individual field. + """ + if field_index not in self._field_indices: + raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) + elif Field.valid_params.has_key(paramname): + return self._fields[field_index]._GetParameter(paramname) + else: + TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname)) + + + def _SetKeycodeHandler(self, keycode, func): + """ + This function adds and/or replaces key event handling functions + used by the control. 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._decimalChar = self._ctrl_constraints._decimalChar + self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar + + self._isFloat = isFloatingPoint(s) and not self._ctrl_constraints._validRegex + self._isInt = isInteger(s) and not self._ctrl_constraints._validRegex + self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt) + self._useParens = self._ctrl_constraints._useParensForNegatives + self._isNeg = False +#### dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) +#### dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s), +## 'ctrl regex:', self._ctrl_constraints._validRegex) + + if self._signOk and s[0] != ' ': + s = ' ' + s + if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ': + self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue + self._signpos = 0 + + if self._useParens: + s += ' ' + self._ctrl_constraints._defaultValue += ' ' + + # Now, go build up a dictionary of booleans, indexed by position, + # indicating whether or not a given position is masked or not + ismasked = {} + i = 0 + while i < len(s): + if s[i] == '\\': # if escaped character: + ismasked[i] = False # mark position as not a mask char + if i+1 < len(s): # if another char follows... + s = s[:i] + s[i+1:] # elide the '\' + if i+2 < len(s) and s[i+1] == '\\': + # if next char also a '\', char is a literal '\' + s = s[:i] + s[i+1:] # elide the 2nd '\' as well + else: # else if special char, mark position accordingly + ismasked[i] = s[i] in maskchars +#### dbg('ismasked[%d]:' % i, ismasked[i], s) + i += 1 # increment to next char +#### dbg('ismasked:', ismasked) +## dbg('new mask: "%s"' % s, indent=0) + + return s, ismasked + + + def _calcFieldExtents(self): + """ + Subroutine responsible for establishing/configuring field instances with + indices and editable extents appropriate to the specified mask, and building + the lookup table mapping each position to the corresponding field. + """ + self._lookupField = {} + if self._mask: + + ## Create dictionary of positions,characters in mask + self.maskdict = {} + for charnum in range( len( self._mask)): + self.maskdict[charnum] = self._mask[charnum:charnum+1] + + # For the current mask, create an ordered list of field extents + # and a dictionary of positions that map to field indices: + + if self._signOk: start = 1 + else: start = 0 + + if self._isFloat: + # Skip field "discovery", and just construct a 2-field control with appropriate + # constraints for a floating-point entry. + + # .setdefault always constructs 2nd argument even if not needed, so we do this + # the old-fashioned way... + if not self._fields.has_key(0): + self._fields[0] = Field() + if not self._fields.has_key(1): + self._fields[1] = Field() + + self._decimalpos = string.find( self._mask, '.') +## dbg('decimal pos =', self._decimalpos) + + formatcodes = self._fields[0]._GetParameter('formatcodes') + if 'R' not in formatcodes: formatcodes += 'R' + self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos), + mask=self._mask[start:self._decimalpos], formatcodes=formatcodes) + end = len(self._mask) + if self._signOk and self._useParens: + end -= 1 + self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end), + mask=self._mask[self._decimalpos+1:end]) + + for i in range(self._decimalpos+1): + self._lookupField[i] = 0 + + for i in range(self._decimalpos+1, len(self._mask)+1): + self._lookupField[i] = 1 + + elif self._isInt: + # Skip field "discovery", and just construct a 1-field control with appropriate + # constraints for a integer entry. + if not self._fields.has_key(0): + self._fields[0] = Field(index=0) + end = len(self._mask) + if self._signOk and self._useParens: + end -= 1 + self._fields[0]._SetParameters(index=0, extent=(start, end), + mask=self._mask[start:end]) + for i in range(len(self._mask)+1): + self._lookupField[i] = 0 + else: + # generic control; parse mask to figure out where the fields are: + field_index = 0 + pos = 0 + i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point: + if i < len(self._mask): # no editable chars! + for j in range(pos, i+1): + self._lookupField[j] = field_index + pos = i # figure out field for 1st editable space: + + while i <= len(self._mask): +#### dbg('searching: outer field loop: i = ', i) + if self._isMaskChar(i): +#### dbg('1st char is mask char; recording edit_start=', i) + edit_start = i + # Skip to end of editable part of current field: + while i < len(self._mask) and self._isMaskChar(i): + self._lookupField[i] = field_index + i += 1 +#### dbg('edit_end =', i) + edit_end = i + self._lookupField[i] = field_index +#### dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) + if not self._fields.has_key(field_index): + kwargs = Field.valid_params.copy() + kwargs['index'] = field_index + kwargs['extent'] = (edit_start, edit_end) + kwargs['mask'] = self._mask[edit_start:edit_end] + self._fields[field_index] = Field(**kwargs) + else: + self._fields[field_index]._SetParameters( + index=field_index, + extent=(edit_start, edit_end), + mask=self._mask[edit_start:edit_end]) + pos = i + i = self._findNextEntry(pos, adjustInsert=False) # go to next field: + if i > pos: + for j in range(pos, i+1): + self._lookupField[j] = field_index + if i >= len(self._mask): + break # if past end, we're done + else: + field_index += 1 +#### dbg('next field:', field_index) + + indices = self._fields.keys() + indices.sort() + self._field_indices = indices[1:] +#### dbg('lookupField map:', indent=1) +## for i in range(len(self._mask)): +#### dbg('pos %d:' % i, self._lookupField[i]) +#### dbg(indent=0) + + # Verify that all field indices specified are valid for mask: + for index in self._fields.keys(): + if index not in [-1] + self._lookupField.values(): + raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask)) + + + def _calcTemplate(self, reset_fillchar, reset_default): + """ + Subroutine for processing current fillchars and default values for + whole control and individual fields, constructing the resulting + overall template, and adjusting the current value as necessary. + """ + default_set = False + if self._ctrl_constraints._defaultValue: + default_set = True + else: + for field in self._fields.values(): + if field._defaultValue and not reset_default: + default_set = True +## dbg('default set?', default_set) + + # Determine overall new template for control, and keep track of previous + # values, so that current control value can be modified as appropriate: + if self.controlInitialized: curvalue = list(self._GetValue()) + else: curvalue = None + + if hasattr(self, '_fillChar'): old_fillchars = self._fillChar + else: old_fillchars = None + + if hasattr(self, '_template'): old_template = self._template + else: old_template = None + + self._template = "" + + self._fillChar = {} + reset_value = False + + for field in self._fields.values(): + field._template = "" + + for pos in range(len(self._mask)): +#### dbg('pos:', pos) + field = self._FindField(pos) +#### dbg('field:', field._index) + start, end = field._extent + + if pos == 0 and self._signOk: + self._template = ' ' # always make 1st 1st position blank, regardless of fillchar + elif self._isFloat and pos == self._decimalpos: + self._template += self._decimalChar + elif self._isMaskChar(pos): + if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar: + fillChar = field._fillChar + else: + fillChar = self._ctrl_constraints._fillChar + self._fillChar[pos] = fillChar + + # Replace any current old fillchar with new one in current value; + # if action required, set reset_value flag so we can take that action + # after we're all done + if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue: + if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar: + reset_value = True + curvalue[pos] = fillChar + + if not field._defaultValue and not self._ctrl_constraints._defaultValue: +#### dbg('no default value') + self._template += fillChar + field._template += fillChar + + elif field._defaultValue and not reset_default: +#### dbg('len(field._defaultValue):', len(field._defaultValue)) +#### dbg('pos-start:', pos-start) + if len(field._defaultValue) > pos-start: +#### dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) + self._template += field._defaultValue[pos-start] + field._template += field._defaultValue[pos-start] + else: +#### dbg('field default not long enough; using fillChar') + self._template += fillChar + field._template += fillChar + else: + if len(self._ctrl_constraints._defaultValue) > pos: +#### dbg('using control default') + self._template += self._ctrl_constraints._defaultValue[pos] + field._template += self._ctrl_constraints._defaultValue[pos] + else: +#### dbg('ctrl default not long enough; using fillChar') + self._template += fillChar + field._template += fillChar +#### dbg('field[%d]._template now "%s"' % (field._index, field._template)) +#### dbg('self._template now "%s"' % self._template) + else: + self._template += self._mask[pos] + + self._fields[-1]._template = self._template # (for consistency) + + if curvalue: # had an old value, put new one back together + newvalue = string.join(curvalue, "") + else: + newvalue = None + + if default_set: + self._defaultValue = self._template +## dbg('self._defaultValue:', self._defaultValue) + if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): +#### dbg(indent=0) + raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name)) + + # if no fillchar change, but old value == old template, replace it: + if newvalue == old_template: + newvalue = self._template + reset_value = True + else: + self._defaultValue = None + + if reset_value: +## dbg('resetting value to: "%s"' % newvalue) + pos = self._GetInsertionPoint() + sel_start, sel_to = self._GetSelection() + self._SetValue(newvalue) + self._SetInsertionPoint(pos) + self._SetSelection(sel_start, sel_to) + + + def _propagateConstraints(self, **reset_args): + """ + Subroutine for propagating changes to control-level constraints and + formatting to the individual fields as appropriate. + """ + parent_codes = self._ctrl_constraints._formatcodes + parent_includes = self._ctrl_constraints._includeChars + parent_excludes = self._ctrl_constraints._excludeChars + for i in self._field_indices: + field = self._fields[i] + inherit_args = {} + if len(self._field_indices) == 1: + inherit_args['formatcodes'] = parent_codes + inherit_args['includeChars'] = parent_includes + inherit_args['excludeChars'] = parent_excludes + else: + field_codes = current_codes = field._GetParameter('formatcodes') + for c in parent_codes: + if c not in field_codes: field_codes += c + if field_codes != current_codes: + inherit_args['formatcodes'] = field_codes + + include_chars = current_includes = field._GetParameter('includeChars') + for c in parent_includes: + if not c in include_chars: include_chars += c + if include_chars != current_includes: + inherit_args['includeChars'] = include_chars + + exclude_chars = current_excludes = field._GetParameter('excludeChars') + for c in parent_excludes: + if not c in exclude_chars: exclude_chars += c + if exclude_chars != current_excludes: + inherit_args['excludeChars'] = exclude_chars + + if reset_args.has_key('defaultValue') and reset_args['defaultValue']: + inherit_args['defaultValue'] = "" # (reset for field) + + for param in Field.propagating_params: +#### dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) +#### dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) + if reset_args.has_key(param): + inherit_args[param] = self.GetCtrlParameter(param) +#### dbg('inherit_args[%s]' % param, inherit_args[param]) + + if inherit_args: + field._SetParameters(**inherit_args) + field._ValidateParameters(**inherit_args) + + + def _validateChoices(self): + """ + Subroutine that validates that all choices for given fields are at + least of the necessary length, and that they all would be valid pastes + if pasted into their respective fields. + """ + for field in self._fields.values(): + if field._choices: + index = field._index + if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices: +## dbg('skipping (duplicate) choice validation of field 0') + continue +#### dbg('checking for choices for field', field._index) + start, end = field._extent + field_length = end - start +#### dbg('start, end, length:', start, end, field_length) + for choice in field._choices: +#### dbg('testing "%s"' % choice) + valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) + if not valid_paste: +#### dbg(indent=0) + raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name)) + elif replace_to > end: +#### dbg(indent=0) + raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name)) +#### dbg(choice, 'valid in field', index) + + + def _configure(self, mask, **reset_args): + """ + This function sets flags for automatic styling options. It is + called whenever a control or field-level parameter is set/changed. + + This routine does the bulk of the interdependent parameter processing, determining + the field extents of the mask if changed, resetting parameters as appropriate, + determining the overall template value for the control, etc. + + reset_args is supplied if called from control's .SetCtrlParameters() + routine, and indicates which if any parameters which can be + overridden by individual fields have been reset by request for the + whole control. + + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) + + # Preprocess specified mask to expand {n} syntax, handle escaped + # mask characters, etc and build the resulting positionally keyed + # dictionary for which positions are mask vs. template characters: + self._mask, self.ismasked = self._processMask(mask) + self._masklength = len(self._mask) +#### dbg('processed mask:', self._mask) + + # Preserve original mask specified, for subsequent reprocessing + # if parameters change. +## dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) + self._previous_mask = mask # save unexpanded mask for next time + # Set expanded mask and extent of field -1 to width of entire control: + self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength)) + + # Go parse mask to determine where each field is, construct field + # instances as necessary, configure them with those extents, and + # build lookup table mapping each position for control to its corresponding + # field. +#### dbg('calculating field extents') + + self._calcFieldExtents() + + + # Go process defaultValues and fillchars to construct the overall + # template, and adjust the current value as necessary: + reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar'] + reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue'] + +#### dbg('calculating template') + self._calcTemplate(reset_fillchar, reset_default) + + # Propagate control-level formatting and character constraints to each + # field if they don't already have them; if only one field, propagate + # control-level validation constraints to field as well: +#### dbg('propagating constraints') + self._propagateConstraints(**reset_args) + + + if self._isFloat and self._fields[0]._groupChar == self._decimalChar: + raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % + (self._fields[0]._groupChar, self._decimalChar) ) + +#### dbg('fields:', indent=1) +## for i in [-1] + self._field_indices: +#### dbg('field %d:' % i, self._fields[i].__dict__) +#### dbg(indent=0) + + # Set up special parameters for numeric control, if appropriate: + if self._signOk: + self._signpos = 0 # assume it starts here, but it will move around on floats + signkeys = ['-', '+', ' '] + if self._useParens: + signkeys += ['(', ')'] + for key in signkeys: + keycode = ord(key) + if not self._keyhandlers.has_key(keycode): + self._SetKeyHandler(key, self._OnChangeSign) + + + + if self._isFloat or self._isInt: + if self.controlInitialized: + value = self._GetValue() +#### dbg('value: "%s"' % value, 'len(value):', len(value), +## 'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) + if len(value) < len(self._ctrl_constraints._mask): + newvalue = value + if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1: + newvalue += ' ' + if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1: + newvalue = ' ' + newvalue + if len(newvalue) < len(self._ctrl_constraints._mask): + if self._ctrl_constraints._alignRight: + newvalue = newvalue.rjust(len(self._ctrl_constraints._mask)) + else: + newvalue = newvalue.ljust(len(self._ctrl_constraints._mask)) +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._SetValue(newvalue) + except Exception, e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + + elif len(value) > len(self._ctrl_constraints._mask): + newvalue = value + if not self._useParens and newvalue[-1] == ' ': + newvalue = newvalue[:-1] + if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask): + newvalue = newvalue[1:] + if not self._signOk: + newvalue, signpos, right_signpos = self._getSignedValue(newvalue) + +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._SetValue(newvalue) + except Exception, e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + elif not self._signOk and ('(' in value or '-' in value): + newvalue, signpos, right_signpos = self._getSignedValue(value) +## dbg('old value: "%s"' % value) +## dbg('new value: "%s"' % newvalue) + try: + self._SetValue(newvalue) + except e: +## dbg('exception raised:', e, 'resetting to initial value') + self._SetInitialValue() + + # Replace up/down arrow default handling: + # make down act like tab, up act like shift-tab: + +#### dbg('Registering numeric navigation and control handlers (if not already set)') + if not self._keyhandlers.has_key(wx.WXK_DOWN): + self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) + if not self._keyhandlers.has_key(wx.WXK_UP): + self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) + + # On ., truncate contents right of cursor to decimal point (if any) + # leaves cusor after decimal point if floating point, otherwise at 0. + if not self._keyhandlers.has_key(ord(self._decimalChar)): + self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint) + if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)): + self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards) + + # Allow selective insert of groupchar in numbers: + if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)): + self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar) + +## dbg(indent=0, suspend=0) + + + def _SetInitialValue(self, value=""): + """ + fills the control with the generated or supplied default value. + It will also set/reset the font if necessary and apply + formatting to the control at this time. + """ +## dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) + if not value: + self._prevValue = self._curValue = self._template + # don't apply external validation rules in this case, as template may + # not coincide with "legal" value... + try: + self._SetValue(self._curValue) # note the use of "raw" ._SetValue()... + except Exception, e: +## dbg('exception thrown:', e, indent=0) + raise + else: + # Otherwise apply validation as appropriate to passed value: +#### dbg('value = "%s", length:' % value, len(value)) + self._prevValue = self._curValue = value + try: + self.SetValue(value) # use public (validating) .SetValue() + except Exception, e: +## dbg('exception thrown:', e, indent=0) + raise + + + # Set value/type-specific formatting + self._applyFormatting() +## dbg(indent=0) + + + def _calcSize(self, size=None): + """ Calculate automatic size if allowed; must be called after the base control is instantiated""" +#### dbg('MaskedEditMixin::_calcSize', indent=1) + cont = (size is None or size == wx.DefaultSize) + + if cont and self._autofit: + sizing_text = 'M' * self._masklength + if wx.Platform != "__WXMSW__": # give it a little extra space + sizing_text += 'M' + if wx.Platform == "__WXMAC__": # give it even a little more... + sizing_text += 'M' +#### dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) + w, h = self.GetTextExtent(sizing_text) + size = (w+4, self.GetClientSize().height) +#### dbg('size:', size, indent=0) + return size + + + def _setFont(self): + """ Set the control's font typeface -- pass the font name as str.""" +#### dbg('MaskedEditMixin::_setFont', indent=1) + if not self._useFixedWidthFont: + self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) + else: + font = self.GetFont() # get size, weight, etc from current font + + # Set to teletype font (guaranteed to be mappable to all wxWindows + # platforms: + self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(), + font.GetWeight(), font.GetUnderlined()) +#### dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) + + self.SetFont(self._font) +#### dbg(indent=0) + + + def _OnTextChange(self, event): + """ + Handler for EVT_TEXT event. + self._Change() is provided for subclasses, and may return False to + skip this method logic. This function returns True if the event + detected was a legitimate event, or False if it was a "bogus" + EVT_TEXT event. (NOTE: There is currently an issue with calling + .SetValue from within the EVT_CHAR handler that causes duplicate + EVT_TEXT events for the same change.) + """ + newvalue = self._GetValue() +## dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) + bValid = False + if self._ignoreChange: # ie. if an "intermediate text change event" +## dbg(indent=0) + return bValid + + ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue + ## call is generating two (2) EVT_TEXT events. + ## This is the only mechanism I can find to mask this problem: + if newvalue == self._curValue: +## dbg('ignoring bogus text change event', indent=0) + pass + else: +## dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue)) + if self._Change(): + if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1: +## dbg('clearing self._isNeg') + self._isNeg = False + text, self._signpos, self._right_signpos = self._getSignedValue() + self._CheckValid() # Recolor control as appropriate +## dbg('calling event.Skip()') + event.Skip() + bValid = True + self._prevValue = self._curValue # save for undo + self._curValue = newvalue # Save last seen value for next iteration +## dbg(indent=0) + return bValid + + + def _OnKeyDown(self, event): + """ + This function allows the control to capture Ctrl-events like Ctrl-tab, + that are not normally seen by the "cooked" EVT_CHAR routine. + """ + # Get keypress value, adjusted by control options (e.g. convert to upper etc) + key = event.GetKeyCode() + if key in self._nav and event.ControlDown(): + # then this is the only place we will likely see these events; + # process them now: +## dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') + self._OnChar(event) + return + # else allow regular EVT_CHAR key processing + event.Skip() + + + def _OnChar(self, event): + """ + This is the engine of MaskedEdit controls. It examines each keystroke, + decides if it's allowed, where it should go or what action to take. + """ +## dbg('MaskedEditMixin::_OnChar', indent=1) + + # Get keypress value, adjusted by control options (e.g. convert to upper etc) + key = event.GetKeyCode() + orig_pos = self._GetInsertionPoint() + orig_value = self._GetValue() +## dbg('keycode = ', key) +## dbg('current pos = ', orig_pos) +## dbg('current selection = ', self._GetSelection()) + + if not self._Keypress(key): +## dbg(indent=0) + return + + # If no format string for this control, or the control is marked as "read-only", + # skip the rest of the special processing, and just "do the standard thing:" + if not self._mask or not self._IsEditable(): + event.Skip() +## dbg(indent=0) + return + + # Process navigation and control keys first, with + # position/selection unadulterated: + if key in self._nav + self._control: + if self._keyhandlers.has_key(key): + keep_processing = self._keyhandlers[key](event) + if self._GetValue() != orig_value: + self.modified = True + if not keep_processing: +## dbg(indent=0) + return + self._applyFormatting() +## dbg(indent=0) + return + + # Else... adjust the position as necessary for next input key, + # and determine resulting selection: + pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) + + keep_processing = True + # Capture user past end of format field + if pos > len(self.maskdict): +## dbg("field length exceeded:",pos) + keep_processing = False + + if keep_processing: + if self._isMaskChar(pos): ## Get string of allowed characters for validation + okchars = self._getAllowedChars(pos) + else: +## dbg('Not a valid position: pos = ', pos,"chars=",maskchars) + okchars = "" + + key = self._adjustKey(pos, key) # apply formatting constraints to key: + + if self._keyhandlers.has_key(key): + # there's an override for default behavior; use override function instead +## dbg('using supplied key handler:', self._keyhandlers[key]) + keep_processing = self._keyhandlers[key](event) + if self._GetValue() != orig_value: + self.modified = True + if not keep_processing: +## dbg(indent=0) + return + # else skip default processing, but do final formatting + if key < wx.WXK_SPACE or key > 255: +## dbg('key < WXK_SPACE or key > 255') + event.Skip() # non alphanumeric + keep_processing = False + else: + field = self._FindField(pos) +## dbg("key ='%s'" % chr(key)) + if chr(key) == ' ': +## dbg('okSpaces?', field._okSpaces) + pass + + + if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars: + keep_processing = False + + if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ): +## dbg("key allowed by mask") + # insert key into candidate new value, but don't change control yet: + oldstr = self._GetValue() + newstr, newpos, new_select_to, match_field, match_index = self._insertKey( + chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True) +## dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr) + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): +## dbg('not valid; checking to see if adjusted string is:') + keep_processing = False + if self._isFloat and newstr != self._template: + newstr = self._adjustFloat(newstr) +## dbg('adjusted str:', newstr) + if self.IsValid(newstr): +## dbg("it is!") + keep_processing = True + wx.CallAfter(self._SetInsertionPoint, self._decimalpos) + if not keep_processing: +## dbg("key disallowed by validation") + if not wx.Validator_IsSilent() and orig_pos == pos: + wx.Bell() + + if keep_processing: + unadjusted = newstr + + # special case: adjust date value as necessary: + if self._isDate and newstr != self._template: + newstr = self._adjustDate(newstr) +## dbg('adjusted newstr:', newstr) + + if newstr != orig_value: + self.modified = True + + wx.CallAfter(self._SetValue, newstr) + + # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: + if not self.IsDefault() and self._isDate and self._4digityear: + year2dig = self._dateExtent - 2 + if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]: + newpos = pos+2 + + wx.CallAfter(self._SetInsertionPoint, newpos) + + if match_field is not None: +## dbg('matched field') + self._OnAutoSelect(match_field, match_index) + + if new_select_to != newpos: +## dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) + wx.CallAfter(self._SetSelection, newpos, new_select_to) + else: + newfield = self._FindField(newpos) + if newfield != field and newfield._selectOnFieldEntry: +## dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) + wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1]) + keep_processing = False + + elif keep_processing: +## dbg('char not allowed') + keep_processing = False + if (not wx.Validator_IsSilent()) and orig_pos == pos: + wx.Bell() + + self._applyFormatting() + + # Move to next insertion point + if keep_processing and key not in self._nav: + pos = self._GetInsertionPoint() + next_entry = self._findNextEntry( pos ) + if pos != next_entry: +## dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) + wx.CallAfter(self._SetInsertionPoint, next_entry ) + + if self._isTemplateChar(pos): + self._AdjustField(pos) +## dbg(indent=0) + + + def _FindFieldExtent(self, pos=None, getslice=False, value=None): + """ returns editable extent of field corresponding to + position pos, and, optionally, the contents of that field + in the control or the value specified. + Template chars are bound to the preceding field. + For masks beginning with template chars, these chars are ignored + when calculating the current field. + + Eg: with template (###) ###-####, + >>> self._FindFieldExtent(pos=0) + 1, 4 + >>> self._FindFieldExtent(pos=1) + 1, 4 + >>> self._FindFieldExtent(pos=5) + 1, 4 + >>> self._FindFieldExtent(pos=6) + 6, 9 + >>> self._FindFieldExtent(pos=10) + 10, 14 + etc. + """ +## dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) + + field = self._FindField(pos) + if not field: + if getslice: + return None, None, "" + else: + return None, None + edit_start, edit_end = field._extent + if getslice: + if value is None: value = self._GetValue() + slice = value[edit_start:edit_end] +## dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) +## dbg(indent=0) + return edit_start, edit_end, slice + else: +## dbg('edit_start:', edit_start, 'edit_end:', edit_end) +## dbg(indent=0) + return edit_start, edit_end + + + def _FindField(self, pos=None): + """ + Returns the field instance in which pos resides. + Template chars are bound to the preceding field. + For masks beginning with template chars, these chars are ignored + when calculating the current field. + + """ +#### dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) + if pos is None: pos = self._GetInsertionPoint() + elif pos < 0 or pos > self._masklength: + raise IndexError('position %s out of range of control' % str(pos)) + + if len(self._fields) == 0: +## dbg(indent=0) + return None + + # else... +#### dbg(indent=0) + return self._fields[self._lookupField[pos]] + + + def ClearValue(self): + """ Blanks the current control value by replacing it with the default value.""" +## dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") + self._SetValue( self._template ) + self._SetInsertionPoint(0) + self.Refresh() + + + def _baseCtrlEventHandler(self, event): + """ + This function is used whenever a key should be handled by the base control. + """ + event.Skip() + return False + + + def _OnUpNumeric(self, event): + """ + Makes up-arrow act like shift-tab should; ie. take you to start of + previous field. + """ +## dbg('MaskedEditMixin::_OnUpNumeric', indent=1) + event.m_shiftDown = 1 +## dbg('event.ShiftDown()?', event.ShiftDown()) + self._OnChangeField(event) +## dbg(indent=0) + + + def _OnArrow(self, event): + """ + Used in response to left/right navigation keys; makes these actions skip + over mask template chars. + """ +## dbg("MaskedEditMixin::_OnArrow", indent=1) + pos = self._GetInsertionPoint() + keycode = event.GetKeyCode() + sel_start, sel_to = self._GetSelection() + entry_end = self._goEnd(getPosOnly=True) + if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN): + if( ( not self._isTemplateChar(pos) and pos+1 > entry_end) + or ( self._isTemplateChar(pos) and pos >= entry_end) ): +## dbg("can't advance", indent=0) + return False + elif self._isTemplateChar(pos): + self._AdjustField(pos) + elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): +## dbg('adjusting field') + self._AdjustField(pos) + + # treat as shifted up/down arrows as tab/reverse tab: + if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN): + # remove "shifting" and treat as (forward) tab: + event.m_shiftDown = False + keep_processing = self._OnChangeField(event) + + elif self._FindField(pos)._selectOnFieldEntry: + if( keycode in (wx.WXK_UP, wx.WXK_LEFT) + and sel_start != 0 + and self._isTemplateChar(sel_start-1) + and sel_start != self._masklength + and not self._signOk and not self._useParens): + + # call _OnChangeField to handle "ctrl-shifted event" + # (which moves to previous field and selects it.) + event.m_shiftDown = True + event.m_ControlDown = True + keep_processing = self._OnChangeField(event) + elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT) + and sel_to != self._masklength + and self._isTemplateChar(sel_to)): + + # when changing field to the right, ensure don't accidentally go left instead + event.m_shiftDown = False + keep_processing = self._OnChangeField(event) + else: + # treat arrows as normal, allowing selection + # as appropriate: +## dbg('using base ctrl event processing') + event.Skip() + else: + if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT) + or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ): + if not wx.Validator_IsSilent(): + wx.Bell() + else: + # treat arrows as normal, allowing selection + # as appropriate: +## dbg('using base event processing') + event.Skip() + + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def _OnCtrl_S(self, event): + """ Default Ctrl-S handler; prints value information if demo enabled. """ +## dbg("MaskedEditMixin::_OnCtrl_S") + if self._demo: + print 'MaskedEditMixin.GetValue() = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue()) + print "Valid? => " + str(self.IsValid()) + print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True)) + return False + + + def _OnCtrl_X(self, event=None): + """ Handles ctrl-x keypress in control and Cut operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_X", indent=1) + self.Cut() +## dbg(indent=0) + return False + + def _OnCtrl_C(self, event=None): + """ Handles ctrl-C keypress in control and Copy operation on context menu. + Uses base control handling. Should return False to skip other processing.""" + self.Copy() + return False + + def _OnCtrl_V(self, event=None): + """ Handles ctrl-V keypress in control and Paste operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_V", indent=1) + self.Paste() +## dbg(indent=0) + return False + + def _OnCtrl_Z(self, event=None): + """ Handles ctrl-Z keypress in control and Undo operation on context menu. + Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) + self.Undo() +## dbg(indent=0) + return False + + def _OnCtrl_A(self,event=None): + """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ + end = self._goEnd(getPosOnly=True) + if not event or event.ShiftDown(): + wx.CallAfter(self._SetInsertionPoint, 0) + wx.CallAfter(self._SetSelection, 0, self._masklength) + else: + wx.CallAfter(self._SetInsertionPoint, 0) + wx.CallAfter(self._SetSelection, 0, end) + return False + + + def _OnErase(self, event=None): + """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" +## dbg("MaskedEditMixin::_OnErase", indent=1) + sel_start, sel_to = self._GetSelection() ## check for a range of selected text + + if event is None: # called as action routine from Cut() operation. + key = wx.WXK_DELETE + else: + key = event.GetKeyCode() + + field = self._FindField(sel_to) + start, end = field._extent + value = self._GetValue() + oldstart = sel_start + + # If trying to erase beyond "legal" bounds, disallow operation: + if( (sel_to == 0 and key == wx.WXK_BACK) + or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK) + or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight) + or (self._signOk and self._useParens + and sel_start == sel_to + and sel_to == self._masklength - 1 + and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + + if( field._insertRight # an insert-right field + and value[start:end] != self._template[start:end] # and field not empty + and sel_start >= start # and selection starts in field + and ((sel_to == sel_start # and no selection + and sel_to == end # and cursor at right edge + and key in (wx.WXK_BACK, wx.WXK_DELETE)) # and either delete or backspace key + or # or + (key == wx.WXK_BACK # backspacing + and (sel_to == end # and selection ends at right edge + or sel_to < end and field._allowInsert)) ) ): # or allow right insert at any point in field + +## dbg('delete left') + # if backspace but left of cursor is empty, adjust cursor right before deleting + while( key == wx.WXK_BACK + and sel_start == sel_to + and sel_start < end + and value[start:sel_start] == self._template[start:sel_start]): + sel_start += 1 + sel_to = sel_start + +## dbg('sel_start, start:', sel_start, start) + + if sel_start == sel_to: + keep = sel_start -1 + else: + keep = sel_start + newfield = value[start:keep] + value[sel_to:end] + + # handle sign char moving from outside field into the field: + move_sign_into_field = False + if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('): + signchar = value[0] + newfield = signchar + newfield + move_sign_into_field = True +## dbg('cut newfield: "%s"' % newfield) + + # handle what should fill in from the left: + left = "" + for i in range(start, end - len(newfield)): + if field._padZero: + left += '0' + elif( self._signOk and self._isNeg and i == 1 + and ((self._useParens and newfield.find('(') == -1) + or (not self._useParens and newfield.find('-') == -1)) ): + left += ' ' + else: + left += self._template[i] # this can produce strange results in combination with default values... + newfield = left + newfield +## dbg('filled newfield: "%s"' % newfield) + + newstr = value[:start] + newfield + value[end:] + + # (handle sign located in "mask position" in front of field prior to delete) + if move_sign_into_field: + newstr = ' ' + newstr[1:] + pos = sel_to + else: + # handle erasure of (left) sign, moving selection accordingly... + if self._signOk and sel_start == 0: + newstr = value = ' ' + value[1:] + sel_start += 1 + + if field._allowInsert and sel_start >= start: + # selection (if any) falls within current insert-capable field: + select_len = sel_to - sel_start + # determine where cursor should end up: + if key == wx.WXK_BACK: + if select_len == 0: + newpos = sel_start -1 + else: + newpos = sel_start + erase_to = sel_to + else: + newpos = sel_start + if sel_to == sel_start: + erase_to = sel_to + 1 + else: + erase_to = sel_to + + if self._isTemplateChar(newpos) and select_len == 0: + if self._signOk: + if value[newpos] in ('(', '-'): + newpos += 1 # don't move cusor + newstr = ' ' + value[newpos:] + elif value[newpos] == ')': + # erase right sign, but don't move cursor; (matching left sign handled later) + newstr = value[:newpos] + ' ' + else: + # no deletion; just move cursor + newstr = value + else: + # no deletion; just move cursor + newstr = value + else: + if erase_to > end: erase_to = end + erase_len = erase_to - newpos + + left = value[start:newpos] +## dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) + right = value[erase_to:end] + self._template[end-erase_len:end] + pos_adjust = 0 + if field._alignRight: + rstripped = right.rstrip() + if rstripped != right: + pos_adjust = len(right) - len(rstripped) + right = rstripped + + if not field._insertRight and value[-1] == ')' and end == self._masklength - 1: + # need to shift ) into the field: + right = right[:-1] + ')' + value = value[:-1] + ' ' + + newfield = left+right + if pos_adjust: + newfield = newfield.rjust(end-start) + newpos += pos_adjust +## dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) + newstr = value[:start] + newfield + value[end:] + + pos = newpos + + else: + if sel_start == sel_to: +## dbg("current sel_start, sel_to:", sel_start, sel_to) + if key == wx.WXK_BACK: + sel_start, sel_to = sel_to-1, sel_to-1 +## dbg("new sel_start, sel_to:", sel_start, sel_to) + + if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''): + # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: + newchar = '0' + else: + newchar = self._template[sel_to] ## get an original template character to "clear" the current char +## dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) + + if self._isTemplateChar(sel_to): + if sel_to == 0 and self._signOk and value[sel_to] == '-': # erasing "template" sign char + newstr = ' ' + value[1:] + sel_to += 1 + elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('): + # allow "change sign" by removing both parens: + newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' ' + else: + newstr = value + newpos = sel_to + else: + if field._insertRight and sel_start == sel_to: + # force non-insert-right behavior, by selecting char to be replaced: + sel_to += 1 + newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value) + + else: + # selection made + newstr = self._eraseSelection(value, sel_start, sel_to) + + pos = sel_start # put cursor back at beginning of selection + + if self._signOk and self._useParens: + # account for resultant unbalanced parentheses: + left_signpos = newstr.find('(') + right_signpos = newstr.find(')') + + if left_signpos == -1 and right_signpos != -1: + # erased left-sign marker; get rid of right sign marker: + newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:] + + elif left_signpos != -1 and right_signpos == -1: + # erased right-sign marker; get rid of left-sign marker: + newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:] + +## dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) +## dbg("newstr:'%s'" % newstr, 'pos:', pos) + + # if erasure results in an invalid field, disallow it: +## dbg('field._validRequired?', field._validRequired) +## dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) + if field._validRequired and not field.IsValid(newstr[start:end]): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + + # if erasure results in an invalid value, disallow it: + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + +## dbg('setting value (later) to', newstr) + wx.CallAfter(self._SetValue, newstr) +## dbg('setting insertion point (later) to', pos) + wx.CallAfter(self._SetInsertionPoint, pos) +## dbg(indent=0) + if newstr != value: + self.modified = True + return False + + + def _OnEnd(self,event): + """ Handles End keypress in control. Should return False to skip other processing. """ +## dbg("MaskedEditMixin::_OnEnd", indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + if not event.ControlDown(): + end = self._masklength # go to end of control + if self._signOk and self._useParens: + end = end - 1 # account for reserved char at end + else: + end_of_input = self._goEnd(getPosOnly=True) + sel_start, sel_to = self._GetSelection() + if sel_to < pos: sel_to = pos + field = self._FindField(sel_to) + field_end = self._FindField(end_of_input) + + # pick different end point if either: + # - cursor not in same field + # - or at or past last input already + # - or current selection = end of current field: +#### dbg('field != field_end?', field != field_end) +#### dbg('sel_to >= end_of_input?', sel_to >= end_of_input) + if field != field_end or sel_to >= end_of_input: + edit_start, edit_end = field._extent +#### dbg('edit_end:', edit_end) +#### dbg('sel_to:', sel_to) +#### dbg('sel_to == edit_end?', sel_to == edit_end) +#### dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) + + if sel_to == edit_end and field._index < self._field_indices[-1]: + edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end)) # go to end of next field: + end = edit_end +## dbg('end moved to', end) + + elif sel_to == edit_end and field._index == self._field_indices[-1]: + # already at edit end of last field; select to end of control: + end = self._masklength +## dbg('end moved to', end) + else: + end = edit_end # select to end of current field +## dbg('end moved to ', end) + else: + # select to current end of input + end = end_of_input + + +#### dbg('pos:', pos, 'end:', end) + + if event.ShiftDown(): + if not event.ControlDown(): +## dbg("shift-end; select to end of control") + pass + else: +## dbg("shift-ctrl-end; select to end of non-whitespace") + pass + wx.CallAfter(self._SetInsertionPoint, pos) + wx.CallAfter(self._SetSelection, pos, end) + else: + if not event.ControlDown(): +## dbg('go to end of control:') + pass + wx.CallAfter(self._SetInsertionPoint, end) + wx.CallAfter(self._SetSelection, end, end) + +## dbg(indent=0) + return False + + + def _OnReturn(self, event): + """ + Changes the event to look like a tab event, so we can then call + event.Skip() on it, and have the parent form "do the right thing." + """ +## dbg('MaskedEditMixin::OnReturn') + event.m_keyCode = wx.WXK_TAB + event.Skip() + + + def _OnHome(self,event): + """ Handles Home keypress in control. Should return False to skip other processing.""" +## dbg("MaskedEditMixin::_OnHome", indent=1) + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + sel_start, sel_to = self._GetSelection() + + # There are 5 cases here: + + # 1) shift: select from start of control to end of current + # selection. + if event.ShiftDown() and not event.ControlDown(): +## dbg("shift-home; select to start of control") + start = 0 + end = sel_start + + # 2) no shift, no control: move cursor to beginning of control. + elif not event.ControlDown(): +## dbg("home; move to start of control") + start = 0 + end = 0 + + # 3) No shift, control: move cursor back to beginning of field; if + # there already, go to beginning of previous field. + # 4) shift, control, start of selection not at beginning of control: + # move sel_start back to start of field; if already there, go to + # start of previous field. + elif( event.ControlDown() + and (not event.ShiftDown() + or (event.ShiftDown() and sel_start > 0) ) ): + if len(self._field_indices) > 1: + field = self._FindField(sel_start) + start, ignore = field._extent + if sel_start == start and field._index != self._field_indices[0]: # go to start of previous field: + start, ignore = self._FindFieldExtent(sel_start-1) + elif sel_start == start: + start = 0 # go to literal beginning if edit start + # not at that point + end_of_field = True + + else: + start = 0 + + if not event.ShiftDown(): +## dbg("ctrl-home; move to beginning of field") + end = start + else: +## dbg("shift-ctrl-home; select to beginning of field") + end = sel_to + + else: + # 5) shift, control, start of selection at beginning of control: + # unselect by moving sel_to backward to beginning of current field; + # if already there, move to start of previous field. + start = sel_start + if len(self._field_indices) > 1: + # find end of previous field: + field = self._FindField(sel_to) + if sel_to > start and field._index != self._field_indices[0]: + ignore, end = self._FindFieldExtent(field._extent[0]-1) + else: + end = start + end_of_field = True + else: + end = start + end_of_field = False +## dbg("shift-ctrl-home; unselect to beginning of field") + +## dbg('queuing new sel_start, sel_to:', (start, end)) + wx.CallAfter(self._SetInsertionPoint, start) + wx.CallAfter(self._SetSelection, start, end) +## dbg(indent=0) + return False + + + def _OnChangeField(self, event): + """ + Primarily handles TAB events, but can be used for any key that + designer wants to change fields within a masked edit control. + NOTE: at the moment, although coded to handle shift-TAB and + control-shift-TAB, these events are not sent to the controls + by the framework. + """ +## dbg('MaskedEditMixin::_OnChangeField', indent = 1) + # determine end of current field: + pos = self._GetInsertionPoint() +## dbg('current pos:', pos) + sel_start, sel_to = self._GetSelection() + + if self._masklength < 0: # no fields; process tab normally + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + event.Skip() + #else: do nothing +## dbg(indent=0) + return False + + + if event.ShiftDown(): + + # "Go backward" + + # NOTE: doesn't yet work with SHIFT-tab under wx; the control + # never sees this event! (But I've coded for it should it ever work, + # and it *does* work for '.' in IpAddrCtrl.) + field = self._FindField(pos) + index = field._index + field_start = field._extent[0] + if pos < field_start: +## dbg('cursor before 1st field; cannot change to a previous field') + if not wx.Validator_IsSilent(): + wx.Bell() + return False + + if event.ControlDown(): +## dbg('queuing select to beginning of field:', field_start, pos) + wx.CallAfter(self._SetInsertionPoint, field_start) + wx.CallAfter(self._SetSelection, field_start, pos) +## dbg(indent=0) + return False + + elif index == 0: + # We're already in the 1st field; process shift-tab normally: + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to previous ctrl') + event.Skip() + else: +## dbg('position at beginning') + wx.CallAfter(self._SetInsertionPoint, field_start) +## dbg(indent=0) + return False + else: + # find beginning of previous field: + begin_prev = self._FindField(field_start-1)._extent[0] + self._AdjustField(pos) +## dbg('repositioning to', begin_prev) + wx.CallAfter(self._SetInsertionPoint, begin_prev) + if self._FindField(begin_prev)._selectOnFieldEntry: + edit_start, edit_end = self._FindFieldExtent(begin_prev) +## dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) + wx.CallAfter(self._SetInsertionPoint, edit_start) + wx.CallAfter(self._SetSelection, edit_start, edit_end) +## dbg(indent=0) + return False + + else: + # "Go forward" + field = self._FindField(sel_to) + field_start, field_end = field._extent + if event.ControlDown(): +## dbg('queuing select to end of field:', pos, field_end) + wx.CallAfter(self._SetInsertionPoint, pos) + wx.CallAfter(self._SetSelection, pos, field_end) +## dbg(indent=0) + return False + else: + if pos < field_start: +## dbg('cursor before 1st field; go to start of field') + wx.CallAfter(self._SetInsertionPoint, field_start) + if field._selectOnFieldEntry: + wx.CallAfter(self._SetSelection, field_start, field_end) + else: + wx.CallAfter(self._SetSelection, field_start, field_start) + return False + # else... +## dbg('end of current field:', field_end) +## dbg('go to next field') + if field_end == self._fields[self._field_indices[-1]]._extent[1]: + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + event.Skip() + else: +## dbg('position at end') + wx.CallAfter(self._SetInsertionPoint, field_end) +## dbg(indent=0) + return False + else: + # we have to find the start of the next field + next_pos = self._findNextEntry(field_end) + if next_pos == field_end: +## dbg('already in last field') + self._AdjustField(pos) + if event.GetKeyCode() == wx.WXK_TAB: +## dbg('tab to next ctrl') + event.Skip() + #else: do nothing +## dbg(indent=0) + return False + else: + self._AdjustField( pos ) + + # move cursor to appropriate point in the next field and select as necessary: + field = self._FindField(next_pos) + edit_start, edit_end = field._extent + if field._selectOnFieldEntry: +## dbg('move to ', next_pos) + wx.CallAfter(self._SetInsertionPoint, next_pos) + edit_start, edit_end = self._FindFieldExtent(next_pos) +## dbg('queuing select', edit_start, edit_end) + wx.CallAfter(self._SetSelection, edit_start, edit_end) + else: + if field._insertRight: + next_pos = field._extent[1] +## dbg('move to ', next_pos) + wx.CallAfter(self._SetInsertionPoint, next_pos) +## dbg(indent=0) + return False + + + def _OnDecimalPoint(self, event): +## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) + + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + + if self._isFloat: ## handle float value, move to decimal place +## dbg('key == Decimal tab; decimal pos:', self._decimalpos) + value = self._GetValue() + if pos < self._decimalpos: + clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:] +## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) + newstr = self._adjustFloat(clipped_text) + else: + newstr = self._adjustFloat(value) + wx.CallAfter(self._SetValue, newstr) + fraction = self._fields[1] + start, end = fraction._extent + wx.CallAfter(self._SetInsertionPoint, start) + if fraction._selectOnFieldEntry: +## dbg('queuing selection after decimal point to:', (start, end)) + wx.CallAfter(self._SetSelection, start, end) + keep_processing = False + + if self._isInt: ## handle integer value, truncate from current position +## dbg('key == Integer decimal event') + value = self._GetValue() + clipped_text = value[0:pos] +## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) + newstr = self._adjustInt(clipped_text) +## dbg('newstr: "%s"' % newstr) + wx.CallAfter(self._SetValue, newstr) + newpos = len(newstr.rstrip()) + if newstr.find(')') != -1: + newpos -= 1 # (don't move past right paren) + wx.CallAfter(self._SetInsertionPoint, newpos) + keep_processing = False +## dbg(indent=0) + + + def _OnChangeSign(self, event): +## dbg('MaskedEditMixin::_OnChangeSign', indent=1) + key = event.GetKeyCode() + pos = self._adjustPos(self._GetInsertionPoint(), key) + value = self._eraseSelection() + integer = self._fields[0] + start, end = integer._extent + +#### dbg('adjusted pos:', pos) + if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos): + cursign = self._isNeg +## dbg('cursign:', cursign) + if chr(key) in ('-','(', ')'): + self._isNeg = (not self._isNeg) ## flip value + else: + self._isNeg = False +## dbg('isNeg?', self._isNeg) + + text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value) +## dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) + if text is None: + text = value + + if self._isNeg and self._signpos is not None and self._signpos != -1: + if self._useParens and self._right_signpos is not None: + text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:] + else: + text = text[:self._signpos] + '-' + text[self._signpos+1:] + else: +#### dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) + if self._useParens: + text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:] + else: + text = text[:self._signpos] + ' ' + text[self._signpos+1:] +## dbg('clearing self._isNeg') + self._isNeg = False + + wx.CallAfter(self._SetValue, text) + wx.CallAfter(self._applyFormatting) +## dbg('pos:', pos, 'signpos:', self._signpos) + if pos == self._signpos or integer.IsEmpty(text[start:end]): + wx.CallAfter(self._SetInsertionPoint, self._signpos+1) + else: + wx.CallAfter(self._SetInsertionPoint, pos) + + keep_processing = False + else: + keep_processing = True +## dbg(indent=0) + return keep_processing + + + def _OnGroupChar(self, event): + """ + This handler is only registered if the mask is a numeric mask. + It allows the insertion of ',' or '.' if appropriate. + """ +## dbg('MaskedEditMixin::_OnGroupChar', indent=1) + keep_processing = True + pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) + sel_start, sel_to = self._GetSelection() + groupchar = self._fields[0]._groupChar + if not self._isCharAllowed(groupchar, pos, checkRegex=True): + keep_processing = False + if not wx.Validator_IsSilent(): + wx.Bell() + + if keep_processing: + newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() ) +## dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) + if self._ctrl_constraints._validRequired and not self.IsValid(newstr): + keep_processing = False + if not wx.Validator_IsSilent(): + wx.Bell() + + if keep_processing: + wx.CallAfter(self._SetValue, newstr) + wx.CallAfter(self._SetInsertionPoint, newpos) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def _findNextEntry(self,pos, adjustInsert=True): + """ Find the insertion point for the next valid entry character position.""" + if self._isTemplateChar(pos): # if changing fields, pay attn to flag + adjustInsert = adjustInsert + else: # else within a field; flag not relevant + adjustInsert = False + + while self._isTemplateChar(pos) and pos < self._masklength: + pos += 1 + + # if changing fields, and we've been told to adjust insert point, + # look at new field; if empty and right-insert field, + # adjust to right edge: + if adjustInsert and pos < self._masklength: + field = self._FindField(pos) + start, end = field._extent + slice = self._GetValue()[start:end] + if field._insertRight and field.IsEmpty(slice): + pos = end + return pos + + + def _findNextTemplateChar(self, pos): + """ Find the position of the next non-editable character in the mask.""" + while not self._isTemplateChar(pos) and pos < self._masklength: + pos += 1 + return pos + + + def _OnAutoCompleteField(self, event): +## dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) + pos = self._GetInsertionPoint() + field = self._FindField(pos) + edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) + + match_index = None + keycode = event.GetKeyCode() + + if field._fillChar != ' ': + text = slice.replace(field._fillChar, '') + else: + text = slice + text = text.strip() + keep_processing = True # (assume True to start) +## dbg('field._hasList?', field._hasList) + if field._hasList: +## dbg('choices:', field._choices) +## dbg('compareChoices:', field._compareChoices) + choices, choice_required = field._compareChoices, field._choiceRequired + if keycode in (wx.WXK_PRIOR, wx.WXK_UP): + direction = -1 + else: + direction = 1 + match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex) + if( match_index is None + and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ): + # Select the 1st thing from the list: + match_index = 0 + + if( match_index is not None + and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] + or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown()) + or (keycode == wx.WXK_DOWN and partial_match) ) ): + + # We're allowed to auto-complete: +## dbg('match found') + value = self._GetValue() + newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:] +## dbg('setting value to "%s"' % newvalue) + self._SetValue(newvalue) + self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip()))) + self._OnAutoSelect(field, match_index) + self._CheckValid() # recolor as appopriate + + + if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT): + # treat as left right arrow if unshifted, tab/shift tab if shifted. + if event.ShiftDown(): + if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT): + # remove "shifting" and treat as (forward) tab: + event.m_shiftDown = False + keep_processing = self._OnChangeField(event) + else: + keep_processing = self._OnArrow(event) + # else some other key; keep processing the key + +## dbg('keep processing?', keep_processing, indent=0) + return keep_processing + + + def _OnAutoSelect(self, field, match_index = None): + """ + Function called if autoselect feature is enabled and entire control + is selected: + """ +## dbg('MaskedEditMixin::OnAutoSelect', field._index) + if match_index is not None: + field._autoCompleteIndex = match_index + + + def _autoComplete(self, direction, choices, value, compareNoCase, current_index): + """ + This function gets called in response to Auto-complete events. + It attempts to find a match to the specified value against the + list of choices; if exact match, the index of then next + appropriate value in the list, based on the given direction. + If not an exact match, it will return the index of the 1st value from + the choice list for which the partial value can be extended to match. + If no match found, it will return None. + The function returns a 2-tuple, with the 2nd element being a boolean + that indicates if partial match was necessary. + """ +## dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) + if value is None: +## dbg('nothing to match against', indent=0) + return (None, False) + + partial_match = False + + if compareNoCase: + value = value.lower() + + last_index = len(choices) - 1 + if value in choices: +## dbg('"%s" in', choices) + if current_index is not None and choices[current_index] == value: + index = current_index + else: + index = choices.index(value) + +## dbg('matched "%s" (%d)' % (choices[index], index)) + if direction == -1: +## dbg('going to previous') + if index == 0: index = len(choices) - 1 + else: index -= 1 + else: + if index == len(choices) - 1: index = 0 + else: index += 1 +## dbg('change value to "%s" (%d)' % (choices[index], index)) + match = index + else: + partial_match = True + value = value.strip() +## dbg('no match; try to auto-complete:') + match = None +## dbg('searching for "%s"' % value) + if current_index is None: + indices = range(len(choices)) + if direction == -1: + indices.reverse() + else: + if direction == 1: + indices = range(current_index +1, len(choices)) + range(current_index+1) +## dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) + else: + indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1) +## dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices) +#### dbg('indices:', indices) + for index in indices: + choice = choices[index] + if choice.find(value, 0) == 0: +## dbg('match found:', choice) + match = index + break + else: dbg('choice: "%s" - no match' % choice) + if match is not None: +## dbg('matched', match) + pass + else: +## dbg('no match found') + pass +## dbg(indent=0) + return (match, partial_match) + + + def _AdjustField(self, pos): + """ + This function gets called by default whenever the cursor leaves a field. + The pos argument given is the char position before leaving that field. + By default, floating point, integer and date values are adjusted to be + legal in this function. Derived classes may override this function + to modify the value of the control in a different way when changing fields. + + NOTE: these change the value immediately, and restore the cursor to + the passed location, so that any subsequent code can then move it + based on the operation being performed. + """ + newvalue = value = self._GetValue() + field = self._FindField(pos) + start, end, slice = self._FindFieldExtent(getslice=True) + newfield = field._AdjustField(slice) + newvalue = value[:start] + newfield + value[end:] + + if self._isFloat and newvalue != self._template: + newvalue = self._adjustFloat(newvalue) + + if self._ctrl_constraints._isInt and value != self._template: + newvalue = self._adjustInt(value) + + if self._isDate and value != self._template: + newvalue = self._adjustDate(value, fixcentury=True) + if self._4digityear: + year2dig = self._dateExtent - 2 + if pos == year2dig and value[year2dig] != newvalue[year2dig]: + pos = pos+2 + + if newvalue != value: + self._SetValue(newvalue) + self._SetInsertionPoint(pos) + + + def _adjustKey(self, pos, key): + """ Apply control formatting to the key (e.g. convert to upper etc). """ + field = self._FindField(pos) + if field._forceupper and key in range(97,123): + key = ord( chr(key).upper()) + + if field._forcelower and key in range(97,123): + key = ord( chr(key).lower()) + + return key + + + def _adjustPos(self, pos, key): + """ + Checks the current insertion point position and adjusts it if + necessary to skip over non-editable characters. + """ +## dbg('_adjustPos', pos, key, indent=1) + sel_start, sel_to = self._GetSelection() + # If a numeric or decimal mask, and negatives allowed, reserve the + # first space for sign, and last one if using parens. + if( self._signOk + and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) ) + or self._useParens and pos == self._masklength -1)): +## dbg('adjusted pos:', pos, indent=0) + return pos + + if key not in self._nav: + field = self._FindField(pos) + +## dbg('field._insertRight?', field._insertRight) + if field._insertRight: # if allow right-insert + start, end = field._extent + slice = self._GetValue()[start:end].strip() + field_len = end - start + if pos == end: # if cursor at right edge of field + # if not filled or supposed to stay in field, keep current position +#### dbg('pos==end') +#### dbg('len (slice):', len(slice)) +#### dbg('field_len?', field_len) +#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) +#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) + if len(slice) == field_len and field._moveOnFieldFull: + # move cursor to next field: + pos = self._findNextEntry(pos) + self._SetInsertionPoint(pos) + if pos < sel_to: + self._SetSelection(pos, sel_to) # restore selection + else: + self._SetSelection(pos, pos) # remove selection + else: # leave cursor alone + pass + else: + # if at start of control, move to right edge + if (sel_to == sel_start + and (self._isTemplateChar(pos) or (pos == start and len(slice)+ 1 < field_len)) + and pos != end): + pos = end # move to right edge +## elif sel_start <= start and sel_to == end: +## # select to right edge of field - 1 (to replace char) +## pos = end - 1 +## self._SetInsertionPoint(pos) +## # restore selection +## self._SetSelection(sel_start, pos) + + elif self._signOk and sel_start == 0: # if selected to beginning and signed, + # adjust to past reserved sign position: + pos = self._fields[0]._extent[0] + self._SetInsertionPoint(pos) + # restore selection + self._SetSelection(pos, sel_to) + else: + pass # leave position/selection alone + + # else make sure the user is not trying to type over a template character + # If they are, move them to the next valid entry position + elif self._isTemplateChar(pos): + if( not field._moveOnFieldFull + and (not self._signOk + or (self._signOk + and field._index == 0 + and pos > 0) ) ): # don't move to next field without explicit cursor movement + pass + else: + # find next valid position + pos = self._findNextEntry(pos) + self._SetInsertionPoint(pos) + if pos < sel_to: # restore selection + self._SetSelection(pos, sel_to) +## dbg('adjusted pos:', pos, indent=0) + return pos + + + def _adjustFloat(self, candidate=None): + """ + 'Fixes' an floating point control. Collapses spaces, right-justifies, etc. + """ +## dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) + lenInt,lenFraction = [len(s) for s in self._mask.split('.')] ## Get integer, fraction lengths + + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) + intStr, fracStr = value.split(self._decimalChar) + + intStr = self._fields[0]._AdjustField(intStr) +## dbg('adjusted intStr: "%s"' % intStr) + lenInt = len(intStr) + fracStr = fracStr + ('0'*(lenFraction-len(fracStr))) # add trailing spaces to decimal + +## dbg('intStr "%(intStr)s"' % locals()) +## dbg('lenInt:', lenInt) + + intStr = string.rjust( intStr[-lenInt:], lenInt) +## dbg('right-justifed intStr = "%(intStr)s"' % locals()) + newvalue = intStr + self._decimalChar + fracStr + + if self._signOk: + if len(newvalue) < self._masklength: + newvalue = ' ' + newvalue + signedvalue = self._getSignedValue(newvalue)[0] + if signedvalue is not None: newvalue = signedvalue + + # Finally, align string with decimal position, left-padding with + # fillChar: + newdecpos = newvalue.find(self._decimalChar) + if newdecpos < self._decimalpos: + padlen = self._decimalpos - newdecpos + newvalue = string.join([' ' * padlen] + [newvalue] ,'') + + if self._signOk and self._useParens: + if newvalue.find('(') != -1: + newvalue = newvalue[:-1] + ')' + else: + newvalue = newvalue[:-1] + ' ' + +## dbg('newvalue = "%s"' % newvalue) + if candidate is None: + wx.CallAfter(self._SetValue, newvalue) +## dbg(indent=0) + return newvalue + + + def _adjustInt(self, candidate=None): + """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" +## dbg("MaskedEditMixin::_adjustInt", candidate) + lenInt = self._masklength + if candidate is None: value = self._GetValue() + else: value = candidate + + intStr = self._fields[0]._AdjustField(value) + intStr = intStr.strip() # drop extra spaces +## dbg('adjusted field: "%s"' % intStr) + + if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1: + if self._useParens: + intStr = '(' + intStr + ')' + else: + intStr = '-' + intStr + elif self._isNeg and intStr.find('-') != -1 and self._useParens: + intStr = intStr.replace('-', '(') + + if( self._signOk and ((self._useParens and intStr.find('(') == -1) + or (not self._useParens and intStr.find('-') == -1))): + intStr = ' ' + intStr + if self._useParens: + intStr += ' ' # space for right paren position + + elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1: + # ensure closing right paren: + intStr += ')' + + if self._fields[0]._alignRight: ## Only if right-alignment is enabled + intStr = intStr.rjust( lenInt ) + else: + intStr = intStr.ljust( lenInt ) + + if candidate is None: + wx.CallAfter(self._SetValue, intStr ) + return intStr + + + def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False): + """ + 'Fixes' a date control, expanding the year if it can. + Applies various self-formatting options. + """ +## dbg("MaskedEditMixin::_adjustDate", indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate +## dbg('text=', text) + if self._datestyle == "YMD": + year_field = 0 + else: + year_field = 2 + +## dbg('getYear: "%s"' % getYear(text, self._datestyle)) + year = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars + month = getMonth( text, self._datestyle) + day = getDay( text, self._datestyle) +## dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) + + yearVal = None + yearstart = self._dateExtent - 4 + if( len(year) < 4 + and (fixcentury + or force4digit_year + or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ') + or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ): + ## user entered less than four digits and changing fields or past point where we could + ## enter another digit: + try: + yearVal = int(year) + except: +## dbg('bad year=', year) + year = text[yearstart:self._dateExtent] + + if len(year) < 4 and yearVal: + if len(year) == 2: + # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the + # years pass... + now = wx.DateTime_Now() + century = (now.GetYear() /100) * 100 # "this century" + twodig_year = now.GetYear() - century # "this year" (2 digits) + # if separation between today's 2-digit year and typed value > 50, + # assume last century, + # else assume this century. + # + # Eg: if 2003 and yearVal == 30, => 2030 + # if 2055 and yearVal == 80, => 2080 + # if 2010 and yearVal == 96, => 1996 + # + if abs(yearVal - twodig_year) > 50: + yearVal = (century - 100) + yearVal + else: + yearVal = century + yearVal + year = str( yearVal ) + else: # pad with 0's to make a 4-digit year + year = "%04d" % yearVal + if self._4digityear or force4digit_year: + text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:] +## dbg('newdate: "%s"' % text, indent=0) + return text + + + def _goEnd(self, getPosOnly=False): + """ Moves the insertion point to the end of user-entry """ +## dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) + text = self._GetValue() +#### dbg('text: "%s"' % text) + i = 0 + if len(text.rstrip()): + for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1): +#### dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) + if self._isMaskChar(i): + char = text[i] +#### dbg("text[%d]: '%s'" % (i, char)) + if char != ' ': + i += 1 + break + + if i == 0: + pos = self._goHome(getPosOnly=True) + else: + pos = min(i,self._masklength) + + field = self._FindField(pos) + start, end = field._extent + if field._insertRight and pos < end: + pos = end +## dbg('next pos:', pos) +## dbg(indent=0) + if getPosOnly: + return pos + else: + self._SetInsertionPoint(pos) + + + def _goHome(self, getPosOnly=False): + """ Moves the insertion point to the beginning of user-entry """ +## dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) + text = self._GetValue() + for i in range(self._masklength): + if self._isMaskChar(i): + break + pos = max(i, 0) +## dbg(indent=0) + if getPosOnly: + return pos + else: + self._SetInsertionPoint(max(i,0)) + + + + def _getAllowedChars(self, pos): + """ Returns a string of all allowed user input characters for the provided + mask character plus control options + """ + maskChar = self.maskdict[pos] + okchars = self.maskchardict[maskChar] ## entry, get mask approved characters + field = self._FindField(pos) + if okchars and field._okSpaces: ## Allow spaces? + okchars += " " + if okchars and field._includeChars: ## any additional included characters? + okchars += field._includeChars +#### dbg('okchars[%d]:' % pos, okchars) + return okchars + + + def _isMaskChar(self, pos): + """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) + """ + if pos < self._masklength: + return self.ismasked[pos] + else: + return False + + + def _isTemplateChar(self,Pos): + """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) + """ + if Pos < self._masklength: + return not self._isMaskChar(Pos) + else: + return False + + + def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False): + """ Returns True if character is allowed at the specific position, otherwise False.""" +## dbg('_isCharAllowed', char, pos, checkRegex, indent=1) + field = self._FindField(pos) + right_insert = False + + if self.controlInitialized: + sel_start, sel_to = self._GetSelection() + else: + sel_start, sel_to = pos, pos + + if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight: + start, end = field._extent + field_len = end - start + if self.controlInitialized: + value = self._GetValue() + fstr = value[start:end].strip() + if field._padZero: + while fstr and fstr[0] == '0': + fstr = fstr[1:] + input_len = len(fstr) + if self._signOk and '-' in fstr or '(' in fstr: + input_len -= 1 # sign can move out of field, so don't consider it in length + else: + value = self._template + input_len = 0 # can't get the current "value", so use 0 + + + # if entire field is selected or position is at end and field is not full, + # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar: + if( (sel_start, sel_to) == field._extent + or (pos == end and input_len < field_len)): + pos = end - 1 +## dbg('pos = end - 1 = ', pos, 'right_insert? 1') + right_insert = True + elif( field._allowInsert and sel_start == sel_to + and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar)) + and input_len < field_len ): + pos = sel_to - 1 # where character will go +## dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') + right_insert = True + # else leave pos alone... + else: +## dbg('pos stays ', pos, 'right_insert? 0') + pass + + if self._isTemplateChar( pos ): ## if a template character, return empty +## dbg('%d is a template character; returning False' % pos, indent=0) + return False + + if self._isMaskChar( pos ): + okChars = self._getAllowedChars(pos) + + if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)): + okChars += self._fields[0]._groupChar + + if self._signOk: + if self._isInt or (self._isFloat and pos < self._decimalpos): + okChars += '-' + if self._useParens: + okChars += '(' + elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)): + okChars += ')' + +#### dbg('%s in %s?' % (char, okChars), char in okChars) + approved = char in okChars + + if approved and checkRegex: +## dbg("checking appropriate regex's") + value = self._eraseSelection(self._GetValue()) + if right_insert: + at = pos+1 + else: + at = pos + if allowAutoSelect: + newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True) + else: + newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value) +## dbg('newvalue: "%s"' % newvalue) + + fields = [self._FindField(pos)] + [self._ctrl_constraints] + for field in fields: # includes fields[-1] == "ctrl_constraints" + if field._regexMask and field._filter: +## dbg('checking vs. regex') + start, end = field._extent + slice = newvalue[start:end] + approved = (re.match( field._filter, slice) is not None) +## dbg('approved?', approved) + if not approved: break +## dbg(indent=0) + return approved + else: +## dbg('%d is a !???! character; returning False', indent=0) + return False + + + def _applyFormatting(self): + """ Apply formatting depending on the control's state. + Need to find a way to call this whenever the value changes, in case the control's + value has been changed or set programatically. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_applyFormatting', indent=1) + + # Handle negative numbers + if self._signOk: + text, signpos, right_signpos = self._getSignedValue() +## dbg('text: "%s", signpos:' % text, signpos) + if not text or text[signpos] not in ('-','('): + self._isNeg = False +## dbg('no valid sign found; new sign:', self._isNeg) + if text and signpos != self._signpos: + self._signpos = signpos + elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('): +## dbg('setting _isNeg to True') + self._isNeg = True +## dbg('self._isNeg:', self._isNeg) + + if self._signOk and self._isNeg: + fc = self._signedForegroundColour + else: + fc = self._foregroundColour + + if hasattr(fc, '_name'): + c =fc._name + else: + c = fc +## dbg('setting foreground to', c) + self.SetForegroundColour(fc) + + if self._valid: +## dbg('valid') + if self.IsEmpty(): + bc = self._emptyBackgroundColour + else: + bc = self._validBackgroundColour + else: +## dbg('invalid') + bc = self._invalidBackgroundColour + if hasattr(bc, '_name'): + c =bc._name + else: + c = bc +## dbg('setting background to', c) + self.SetBackgroundColour(bc) + self._Refresh() +## dbg(indent=0, suspend=0) + + + def _getAbsValue(self, candidate=None): + """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). + """ +## dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate + right_signpos = text.find(')') + + if self._isInt: + if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ': + signpos = text.find('-') + if signpos == -1: +## dbg('no - found; searching for (') + signpos = text.find('(') + elif signpos != -1: +## dbg('- found at', signpos) + pass + + if signpos == -1: +## dbg('signpos still -1') +## dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) + if len(text) < self._masklength: + text = ' ' + text + if len(text) < self._masklength: + text += ' ' + if len(text) > self._masklength and text[-1] in (')', ' '): + text = text[:-1] + else: +## dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) +## dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) + signpos = len(text) - (len(text.lstrip()) + 1) + + if self._useParens and not text.strip(): + signpos -= 1 # empty value; use penultimate space +## dbg('signpos:', signpos) + if signpos >= 0: + text = text[:signpos] + ' ' + text[signpos+1:] + + else: + if self._signOk: + signpos = 0 + text = self._template[0] + text[1:] + else: + signpos = -1 + + if right_signpos != -1: + if self._signOk: + text = text[:right_signpos] + ' ' + text[right_signpos+1:] + elif len(text) > self._masklength: + text = text[:right_signpos] + text[right_signpos+1:] + right_signpos = -1 + + + elif self._useParens and self._signOk: + # figure out where it ought to go: + right_signpos = self._masklength - 1 # initial guess + if not self._ctrl_constraints._alignRight: +## dbg('not right-aligned') + if len(text.strip()) == 0: + right_signpos = signpos + 1 + elif len(text.strip()) < self._masklength: + right_signpos = len(text.rstrip()) +## dbg('right_signpos:', right_signpos) + + groupchar = self._fields[0]._groupChar + try: + value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', '')) + except: +## dbg('invalid number', indent=0) + return None, signpos, right_signpos + + else: # float value + try: + groupchar = self._fields[0]._groupChar + value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', '')) +## dbg('value:', value) + except: + value = None + + if value < 0 and value is not None: + signpos = text.find('-') + if signpos == -1: + signpos = text.find('(') + + text = text[:signpos] + self._template[signpos] + text[signpos+1:] + else: + # look forwards up to the decimal point for the 1st non-digit +## dbg('decimal pos:', self._decimalpos) +## dbg('text: "%s"' % text) + if self._signOk: + signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1) + # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET + if len(text) >= signpos+1 and text[signpos+1] in ('-','('): + signpos += 1 + else: + signpos = -1 +## dbg('signpos:', signpos) + + if self._useParens: + if self._signOk: + right_signpos = self._masklength - 1 + text = text[:right_signpos] + ' ' + if text[signpos] == '(': + text = text[:signpos] + ' ' + text[signpos+1:] + else: + right_signpos = text.find(')') + if right_signpos != -1: + text = text[:-1] + right_signpos = -1 + + if value is None: +## dbg('invalid number') + text = None + +## dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) +## dbg(indent=0) + return text, signpos, right_signpos + + + def _getSignedValue(self, candidate=None): + """ Return a signed value by adding a "-" prefix if the value + is set to negative, or a space if positive. + """ +## dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) + if candidate is None: text = self._GetValue() + else: text = candidate + + + abstext, signpos, right_signpos = self._getAbsValue(text) + if self._signOk: + if abstext is None: +## dbg(indent=0) + return abstext, signpos, right_signpos + + if self._isNeg or text[signpos] in ('-', '('): + if self._useParens: + sign = '(' + else: + sign = '-' + else: + sign = ' ' + if abstext[signpos] not in string.digits: + text = abstext[:signpos] + sign + abstext[signpos+1:] + else: + # this can happen if value passed is too big; sign assumed to be + # in position 0, but if already filled with a digit, prepend sign... + text = sign + abstext + if self._useParens and text.find('(') != -1: + text = text[:right_signpos] + ')' + text[right_signpos+1:] + else: + text = abstext +## dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) +## dbg(indent=0) + return text, signpos, right_signpos + + + def GetPlainValue(self, candidate=None): + """ Returns control's value stripped of the template text. + plainvalue = MaskedEditMixin.GetPlainValue() + """ +## dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) + + if candidate is None: text = self._GetValue() + else: text = candidate + + if self.IsEmpty(): +## dbg('returned ""', indent=0) + return "" + else: + plain = "" + for idx in range( min(len(self._template), len(text)) ): + if self._mask[idx] in maskchars: + plain += text[idx] + + if self._isFloat or self._isInt: +## dbg('plain so far: "%s"' % plain) + plain = plain.replace('(', '-').replace(')', ' ') +## dbg('plain after sign regularization: "%s"' % plain) + + if self._signOk and self._isNeg and plain.count('-') == 0: + # must be in reserved position; add to "plain value" + plain = '-' + plain.strip() + + if self._fields[0]._alignRight: + lpad = plain.count(',') + plain = ' ' * lpad + plain.replace(',','') + else: + plain = plain.replace(',','') +## dbg('plain after pad and group:"%s"' % plain) + +## dbg('returned "%s"' % plain.rstrip(), indent=0) + return plain.rstrip() + + + def IsEmpty(self, value=None): + """ + Returns True if control is equal to an empty value. + (Empty means all editable positions in the template == fillChar.) + """ + if value is None: value = self._GetValue() + if value == self._template and not self._defaultValue: +#### dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") + return True # (all mask chars == fillChar by defn) + elif value == self._template: + empty = True + for pos in range(len(self._template)): +#### dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) +#### dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) + if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]): + empty = False +#### dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) + return empty + else: +#### dbg("IsEmpty? 0 (value doesn't match template)") + return False + + + def IsDefault(self, value=None): + """ + Returns True if the value specified (or the value of the control if not specified) + is equal to the default value. + """ + if value is None: value = self._GetValue() + return value == self._template + + + def IsValid(self, value=None): + """ Indicates whether the value specified (or the current value of the control + if not specified) is considered valid.""" +#### dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) + if value is None: value = self._GetValue() + ret = self._CheckValid(value) +#### dbg(indent=0) + return ret + + + def _eraseSelection(self, value=None, sel_start=None, sel_to=None): + """ Used to blank the selection when inserting a new character. """ +## dbg("MaskedEditMixin::_eraseSelection", indent=1) + if value is None: value = self._GetValue() + if sel_start is None or sel_to is None: + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg('value: "%s"' % value) +## dbg("current sel_start, sel_to:", sel_start, sel_to) + + newvalue = list(value) + for i in range(sel_start, sel_to): + if self._signOk and newvalue[i] in ('-', '(', ')'): +## dbg('found sign (%s) at' % newvalue[i], i) + + # balance parentheses: + if newvalue[i] == '(': + right_signpos = value.find(')') + if right_signpos != -1: + newvalue[right_signpos] = ' ' + + elif newvalue[i] == ')': + left_signpos = value.find('(') + if left_signpos != -1: + newvalue[left_signpos] = ' ' + + newvalue[i] = ' ' + + elif self._isMaskChar(i): + field = self._FindField(i) + if field._padZero: + newvalue[i] = '0' + else: + newvalue[i] = self._template[i] + + value = string.join(newvalue,"") +## dbg('new value: "%s"' % value) +## dbg(indent=0) + return value + + + def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False): + """ Handles replacement of the character at the current insertion point.""" +## dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) + + text = self._eraseSelection(value) + field = self._FindField(pos) + start, end = field._extent + newtext = "" + newpos = pos + + if pos != sel_start and sel_start == sel_to: + # adjustpos must have moved the position; make selection match: + sel_start = sel_to = pos + +## dbg('field._insertRight?', field._insertRight) + if( field._insertRight # field allows right insert + and ((sel_start, sel_to) == field._extent # and whole field selected + or (sel_start == sel_to # or nothing selected + and (sel_start == end # and cursor at right edge + or (field._allowInsert # or field allows right-insert + and sel_start < end # next to other char in field: + and text[sel_start] != field._fillChar) ) ) ) ): +## dbg('insertRight') + fstr = text[start:end] + erasable_chars = [field._fillChar, ' '] + + if field._padZero: + erasable_chars.append('0') + + erased = '' +#### dbg("fstr[0]:'%s'" % fstr[0]) +#### dbg('field_index:', field._index) +#### dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) +#### dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", +## self._signOk and field._index == 0 and fstr[0] in ('-','(')) + if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')): + erased = fstr[0] +#### dbg('value: "%s"' % text) +#### dbg('fstr: "%s"' % fstr) +#### dbg("erased: '%s'" % erased) + field_sel_start = sel_start - start + field_sel_to = sel_to - start +## dbg('left fstr: "%s"' % fstr[1:field_sel_start]) +## dbg('right fstr: "%s"' % fstr[field_sel_to:end]) + fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end] + if field._alignRight and sel_start != sel_to: + field_len = end - start +## pos += (field_len - len(fstr)) # move cursor right by deleted amount + pos = sel_to +## dbg('setting pos to:', pos) + if field._padZero: + fstr = '0' * (field_len - len(fstr)) + fstr + else: + fstr = fstr.rjust(field_len) # adjust the field accordingly +## dbg('field str: "%s"' % fstr) + + newtext = text[:start] + fstr + text[end:] + if erased in ('-', '(') and self._signOk: + newtext = erased + newtext[1:] +## dbg('newtext: "%s"' % newtext) + + if self._signOk and field._index == 0: + start -= 1 # account for sign position + +#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull) +#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) + if( field._moveOnFieldFull and pos == end + and len(fstr.lstrip()) == end-start): # if field now full + newpos = self._findNextEntry(end) # go to next field + else: + newpos = pos # else keep cursor at current position + + if not newtext: +## dbg('not newtext') + if newpos != pos: +## dbg('newpos:', newpos) + pass + if self._signOk and self._useParens: + old_right_signpos = text.find(')') + + if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start: + # inserting within a left-insert-capable field + field_len = end - start + before = text[start:sel_start] + after = text[sel_to:end].strip() +#### dbg("current field:'%s'" % text[start:end]) +#### dbg("before:'%s'" % before, "after:'%s'" % after) + new_len = len(before) + len(after) + 1 # (for inserted char) +#### dbg('new_len:', new_len) + + if new_len < field_len: + retained = after + self._template[end-(field_len-new_len):end] + elif new_len > end-start: + retained = after[1:] + else: + retained = after + + left = text[0:start] + before +#### dbg("left:'%s'" % left, "retained:'%s'" % retained) + right = retained + text[end:] + else: + left = text[0:pos] + right = text[pos+1:] + + newtext = left + char + right + + if self._signOk and self._useParens: + # Balance parentheses: + left_signpos = newtext.find('(') + + if left_signpos == -1: # erased '('; remove ')' + right_signpos = newtext.find(')') + if right_signpos != -1: + newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:] + + elif old_right_signpos != -1: + right_signpos = newtext.find(')') + + if right_signpos == -1: # just replaced right-paren + if newtext[pos] == ' ': # we just erased '); erase '(' + newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:] + else: # replaced with digit; move ') over + if self._ctrl_constraints._alignRight or self._isFloat: + newtext = newtext[:-1] + ')' + else: + rstripped_text = newtext.rstrip() + right_signpos = len(rstripped_text) +## dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) + newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:] + + if( field._insertRight # if insert-right field (but we didn't start at right edge) + and field._moveOnFieldFull # and should move cursor when full + and len(newtext[start:end].strip()) == end-start): # and field now full + newpos = self._findNextEntry(end) # go to next field +## dbg('newpos = nextentry =', newpos) + else: +## dbg('pos:', pos, 'newpos:', pos+1) + newpos = pos+1 + + + if allowAutoSelect: + new_select_to = newpos # (default return values) + match_field = None + match_index = None + + if field._autoSelect: + match_index, partial_match = self._autoComplete(1, # (always forward) + field._compareChoices, + newtext[start:end], + compareNoCase=field._compareNoCase, + current_index = field._autoCompleteIndex-1) + if match_index is not None and partial_match: + matched_str = newtext[start:end] + newtext = newtext[:start] + field._choices[match_index] + newtext[end:] + new_select_to = end + match_field = field + if field._insertRight: + # adjust position to just after partial match in field + newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip())) + + elif self._ctrl_constraints._autoSelect: + match_index, partial_match = self._autoComplete( + 1, # (always forward) + self._ctrl_constraints._compareChoices, + newtext, + self._ctrl_constraints._compareNoCase, + current_index = self._ctrl_constraints._autoCompleteIndex - 1) + if match_index is not None and partial_match: + matched_str = newtext + newtext = self._ctrl_constraints._choices[match_index] + new_select_to = self._ctrl_constraints._extent[1] + match_field = self._ctrl_constraints + if self._ctrl_constraints._insertRight: + # adjust position to just after partial match in control: + newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip())) + +## dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) +## dbg(indent=0) + return newtext, newpos, new_select_to, match_field, match_index + else: +## dbg('newtext: "%s"' % newtext, 'newpos:', newpos) +## dbg(indent=0) + return newtext, newpos + + + def _OnFocus(self,event): + """ + This event handler is currently necessary to work around new default + behavior as of wxPython2.3.3; + The TAB key auto selects the entire contents of the wxTextCtrl *after* + the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection + *here*, because it hasn't happened yet. So to prevent this behavior, and + preserve the correct selection when the focus event is not due to tab, + we need to pull the following trick: + """ +## dbg('MaskedEditMixin::_OnFocus') + wx.CallAfter(self._fixSelection) + event.Skip() + self.Refresh() + + + def _CheckValid(self, candidate=None): + """ + This is the default validation checking routine; It verifies that the + current value of the control is a "valid value," and has the side + effect of coloring the control appropriately. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) + oldValid = self._valid + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value: "%s"' % value) + oldvalue = value + valid = True # assume True + + if not self.IsDefault(value) and self._isDate: ## Date type validation + valid = self._validateDate(value) +## dbg("valid date?", valid) + + elif not self.IsDefault(value) and self._isTime: + valid = self._validateTime(value) +## dbg("valid time?", valid) + + elif not self.IsDefault(value) and (self._isInt or self._isFloat): ## Numeric type + valid = self._validateNumeric(value) +## dbg("valid Number?", valid) + + if valid: # and not self.IsDefault(value): ## generic validation accounts for IsDefault() + ## valid so far; ensure also allowed by any list or regex provided: + valid = self._validateGeneric(value) +## dbg("valid value?", valid) + +## dbg('valid?', valid) + + if not candidate: + self._valid = valid + self._applyFormatting() + if self._valid != oldValid: +## dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) +## dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) + pass +## dbg(indent=0, suspend=0) + return valid + + + def _validateGeneric(self, candidate=None): + """ Validate the current value using the provided list or Regex filter (if any). + """ + if candidate is None: + text = self._GetValue() + else: + text = candidate + + valid = True # assume True + for i in [-1] + self._field_indices: # process global constraints first: + field = self._fields[i] + start, end = field._extent + slice = text[start:end] + valid = field.IsValid(slice) + if not valid: + break + + return valid + + + def _validateNumeric(self, candidate=None): + """ Validate that the value is within the specified range (if specified.)""" + if candidate is None: value = self._GetValue() + else: value = candidate + try: + groupchar = self._fields[0]._groupChar + if self._isFloat: + number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', '')) + else: + number = long( value.replace(groupchar, '').replace('(', '-').replace(')', '')) + if value.strip(): + if self._fields[0]._alignRight: + require_digit_at = self._fields[0]._extent[1]-1 + else: + require_digit_at = self._fields[0]._extent[0] +## dbg('require_digit_at:', require_digit_at) +## dbg("value[rda]: '%s'" % value[require_digit_at]) + if value[require_digit_at] not in list(string.digits): + valid = False + return valid + # else... +## dbg('number:', number) + if self._ctrl_constraints._hasRange: + valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh + else: + valid = True + groupcharpos = value.rfind(groupchar) + if groupcharpos != -1: # group char present +## dbg('groupchar found at', groupcharpos) + if self._isFloat and groupcharpos > self._decimalpos: + # 1st one found on right-hand side is past decimal point +## dbg('groupchar in fraction; illegal') + valid = False + elif self._isFloat: + integer = value[:self._decimalpos].strip() + else: + integer = value.strip() +## dbg("integer:'%s'" % integer) + if integer[0] in ('-', '('): + integer = integer[1:] + if integer[-1] == ')': + integer = integer[:-1] + + parts = integer.split(groupchar) +## dbg('parts:', parts) + for i in range(len(parts)): + if i == 0 and abs(int(parts[0])) > 999: +## dbg('group 0 too long; illegal') + valid = False + break + elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]): +## dbg('group %i (%s) not right size; illegal' % (i, parts[i])) + valid = False + break + except ValueError: +## dbg('value not a valid number') + valid = False + return valid + + + def _validateDate(self, candidate=None): + """ Validate the current date value using the provided Regex filter. + Generally used for character types.BufferType + """ +## dbg('MaskedEditMixin::_validateDate', indent=1) + if candidate is None: value = self._GetValue() + else: value = candidate +## dbg('value = "%s"' % value) + text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it +## dbg('text =', text) + valid = True # assume True until proven otherwise + + try: + # replace fillChar in each field with space: + datestr = text[0:self._dateExtent] + for i in range(3): + field = self._fields[i] + start, end = field._extent + fstr = datestr[start:end] + fstr.replace(field._fillChar, ' ') + datestr = datestr[:start] + fstr + datestr[end:] + + year, month, day = getDateParts( datestr, self._datestyle) + year = int(year) +## dbg('self._dateExtent:', self._dateExtent) + if self._dateExtent == 11: + month = charmonths_dict[month.lower()] + else: + month = int(month) + day = int(day) +## dbg('year, month, day:', year, month, day) + + except ValueError: +## dbg('cannot convert string to integer parts') + valid = False + except KeyError: +## dbg('cannot convert string to integer month') + valid = False + + if valid: + # use wxDateTime to unambiguously try to parse the date: + # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, + # rather than 1-12, so handle accordingly: + if month > 12: + valid = False + else: + month -= 1 + try: +## dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) + dateHandler = wx.DateTimeFromDMY(day,month,year) +## dbg("succeeded") + dateOk = True + except: +## dbg('cannot convert string to valid date') + dateOk = False + if not dateOk: + valid = False + + if valid: + # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, + # so we eliminate them here: + timeStr = text[self._dateExtent+1:].strip() ## time portion of the string + if timeStr: +## dbg('timeStr: "%s"' % timeStr) + try: + checkTime = dateHandler.ParseTime(timeStr) + valid = checkTime == len(timeStr) + except: + valid = False + if not valid: +## dbg('cannot convert string to valid time') + pass + if valid: dbg('valid date') +## dbg(indent=0) + return valid + + + def _validateTime(self, candidate=None): + """ Validate the current time value using the provided Regex filter. + Generally used for character types.BufferType + """ +## dbg('MaskedEditMixin::_validateTime', indent=1) + # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, + # so we eliminate them here: + if candidate is None: value = self._GetValue().strip() + else: value = candidate.strip() +## dbg('value = "%s"' % value) + valid = True # assume True until proven otherwise + + dateHandler = wx.DateTime_Today() + try: + checkTime = dateHandler.ParseTime(value) +## dbg('checkTime:', checkTime, 'len(value)', len(value)) + valid = checkTime == len(value) + except: + valid = False + + if not valid: +## dbg('cannot convert string to valid time') + pass + if valid: dbg('valid time') +## dbg(indent=0) + return valid + + + def _OnKillFocus(self,event): + """ Handler for EVT_KILL_FOCUS event. + """ +## dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) + if self._mask and self._IsEditable(): + self._AdjustField(self._GetInsertionPoint()) + self._CheckValid() ## Call valid handler + + self._LostFocus() ## Provided for subclass use + event.Skip() +## dbg(indent=0) + + + def _fixSelection(self): + """ + This gets called after the TAB traversal selection is made, if the + focus event was due to this, but before the EVT_LEFT_* events if + the focus shift was due to a mouse event. + + The trouble is that, a priori, there's no explicit notification of + why the focus event we received. However, the whole reason we need to + do this is because the default behavior on TAB traveral in a wxTextCtrl is + now to select the entire contents of the window, something we don't want. + So we can *now* test the selection range, and if it's "the whole text" + we can assume the cause, change the insertion point to the start of + the control, and deselect. + """ +## dbg('MaskedEditMixin::_fixSelection', indent=1) + if not self._mask or not self._IsEditable(): +## dbg(indent=0) + return + + sel_start, sel_to = self._GetSelection() +## dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) + + if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space) + and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ): + # This isn't normally allowed, and so assume we got here by the new + # "tab traversal" behavior, so we need to reset the selection + # and insertion point: +## dbg('entire text selected; resetting selection to start of control') + self._goHome() + field = self._FindField(self._GetInsertionPoint()) + edit_start, edit_end = field._extent + if field._selectOnFieldEntry: + self._SetInsertionPoint(edit_start) + self._SetSelection(edit_start, edit_end) + + elif field._insertRight: + self._SetInsertionPoint(edit_end) + self._SetSelection(edit_end, edit_end) + + elif (self._isFloat or self._isInt): + + text, signpos, right_signpos = self._getAbsValue() + if text is None or text == self._template: + integer = self._fields[0] + edit_start, edit_end = integer._extent + + if integer._selectOnFieldEntry: +## dbg('select on field entry:') + self._SetInsertionPoint(edit_start) + self._SetSelection(edit_start, edit_end) + + elif integer._insertRight: +## dbg('moving insertion point to end') + self._SetInsertionPoint(edit_end) + self._SetSelection(edit_end, edit_end) + else: +## dbg('numeric ctrl is empty; start at beginning after sign') + self._SetInsertionPoint(signpos+1) ## Move past minus sign space if signed + self._SetSelection(signpos+1, signpos+1) + + elif sel_start > self._goEnd(getPosOnly=True): +## dbg('cursor beyond the end of the user input; go to end of it') + self._goEnd() + else: +## dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) + pass +## dbg(indent=0) + + + def _Keypress(self,key): + """ Method provided to override OnChar routine. Return False to force + a skip of the 'normal' OnChar process. Called before class OnChar. + """ + return True + + + def _LostFocus(self): + """ Method provided for subclasses. _LostFocus() is called after + the class processes its EVT_KILL_FOCUS event code. + """ + pass + + + def _OnDoubleClick(self, event): + """ selects field under cursor on dclick.""" + pos = self._GetInsertionPoint() + field = self._FindField(pos) + start, end = field._extent + self._SetInsertionPoint(start) + self._SetSelection(start, end) + + + def _Change(self): + """ Method provided for subclasses. Called by internal EVT_TEXT + handler. Return False to override the class handler, True otherwise. + """ + return True + + + def _Cut(self): + """ + Used to override the default Cut() method in base controls, instead + copying the selection to the clipboard and then blanking the selection, + leaving only the mask in the selected area behind. + Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the + derived control because the mixin functions can't override a method of + a sibling class. + """ +## dbg("MaskedEditMixin::_Cut", indent=1) + value = self._GetValue() +## dbg('current value: "%s"' % value) + sel_start, sel_to = self._GetSelection() ## check for a range of selected text +## dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) + do = wxTextDataObject() + do.SetText(value[sel_start:sel_to].strip()) + wxTheClipboard.Open() + wxTheClipboard.SetData(do) + wxTheClipboard.Close() + + if sel_to - sel_start != 0: + self._OnErase() +## dbg(indent=0) + + +# WS Note: overriding Copy is no longer necessary given that you +# can no longer select beyond the last non-empty char in the control. +# +## def _Copy( self ): +## """ +## Override the wxTextCtrl's .Copy function, with our own +## that does validation. Need to strip trailing spaces. +## """ +## sel_start, sel_to = self._GetSelection() +## select_len = sel_to - sel_start +## textval = wxTextCtrl._GetValue(self) +## +## do = wxTextDataObject() +## do.SetText(textval[sel_start:sel_to].strip()) +## wxTheClipboard.Open() +## wxTheClipboard.SetData(do) +## wxTheClipboard.Close() + + + def _getClipboardContents( self ): + """ Subroutine for getting the current contents of the clipboard. + """ + do = wxTextDataObject() + wxTheClipboard.Open() + success = wxTheClipboard.GetData(do) + wxTheClipboard.Close() + + if not success: + return None + else: + # Remove leading and trailing spaces before evaluating contents + return do.GetText().strip() + + + def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False): + """ + Used by paste routine and field choice validation to see + if a given slice of paste text is legal for the area in question: + returns validity, replacement text, and extent of paste in + template. + """ +## dbg(suspend=1) +## dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) + select_length = sel_to - sel_start + maxlength = select_length +## dbg('sel_to - sel_start:', maxlength) + if maxlength == 0: + maxlength = self._masklength - sel_start + item = 'control' + else: + item = 'selection' +## dbg('maxlength:', maxlength) + length_considered = len(paste_text) + if length_considered > maxlength: +## dbg('paste text will not fit into the %s:' % item, indent=0) + if raise_on_invalid: +## dbg(indent=0, suspend=0) + if item == 'control': + raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) + else: + raise ValueError('"%s" will not fit into the selection' % paste_text) + else: +## dbg(indent=0, suspend=0) + return False, None, None + + text = self._template +## dbg('length_considered:', length_considered) + + valid_paste = True + replacement_text = "" + replace_to = sel_start + i = 0 + while valid_paste and i < length_considered and replace_to < self._masklength: + if paste_text[i:] == self._template[replace_to:length_considered]: + # remainder of paste matches template; skip char-by-char analysis +## dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) + replacement_text += paste_text[i:] + replace_to = i = length_considered + continue + # else: + char = paste_text[i] + field = self._FindField(replace_to) + if not field._compareNoCase: + if field._forceupper: char = char.upper() + elif field._forcelower: char = char.lower() + +## dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) +## dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) + if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True): + replacement_text += char +## dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) +## dbg("replacement_text:", '"'+replacement_text+'"') + i += 1 + replace_to += 1 + elif( char == self._template[replace_to] + or (self._signOk and + ( (i == 0 and (char == '-' or (self._useParens and char == '('))) + or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ): + replacement_text += char +## dbg("'%(char)s' == template(%(replace_to)d)" % locals()) +## dbg("replacement_text:", '"'+replacement_text+'"') + i += 1 + replace_to += 1 + else: + next_entry = self._findNextEntry(replace_to, adjustInsert=False) + if next_entry == replace_to: + valid_paste = False + else: + replacement_text += self._template[replace_to:next_entry] +## dbg("skipping template; next_entry =", next_entry) +## dbg("replacement_text:", '"'+replacement_text+'"') + replace_to = next_entry # so next_entry will be considered on next loop + + if not valid_paste and raise_on_invalid: +## dbg('raising exception', indent=0, suspend=0) + raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name)) + + elif i < len(paste_text): + valid_paste = False + if raise_on_invalid: +## dbg('raising exception', indent=0, suspend=0) + raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) + +## dbg('valid_paste?', valid_paste) + if valid_paste: +## dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) + pass +## dbg(indent=0, suspend=0) + return valid_paste, replacement_text, replace_to + + + def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): + """ + Used to override the base control's .Paste() function, + with our own that does validation. + Note: _Paste must be called from a Paste() override in the + derived control because the mixin functions can't override a + method of a sibling class. + """ +## dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) + if value is None: + paste_text = self._getClipboardContents() + else: + paste_text = value + + if paste_text is not None: +## dbg('paste text: "%s"' % paste_text) + # (conversion will raise ValueError if paste isn't legal) + sel_start, sel_to = self._GetSelection() +## dbg('selection:', (sel_start, sel_to)) + + # special case: handle allowInsert fields properly + field = self._FindField(sel_start) + edit_start, edit_end = field._extent + new_pos = None + if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end: + new_pos = sel_start + len(paste_text) # store for subsequent positioning + paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip() +## dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) + sel_to = sel_start + len(paste_text) + + # Another special case: paste won't fit, but it's a right-insert field where entire + # non-empty value is selected, and there's room if the selection is expanded leftward: + if( len(paste_text) > sel_to - sel_start + and field._insertRight + and sel_start > edit_start + and sel_to >= edit_end + and not self._GetValue()[edit_start:sel_start].strip() ): + # text won't fit within selection, but left of selection is empty; + # check to see if we can expand selection to accomodate the value: + empty_space = sel_start - edit_start + amount_needed = len(paste_text) - (sel_to - sel_start) + if amount_needed <= empty_space: + sel_start -= amount_needed +## dbg('expanded selection to:', (sel_start, sel_to)) + + + # another special case: deal with signed values properly: + if self._signOk: + signedvalue, signpos, right_signpos = self._getSignedValue() + paste_signpos = paste_text.find('-') + if paste_signpos == -1: + paste_signpos = paste_text.find('(') + + # if paste text will result in signed value: +#### dbg('paste_signpos != -1?', paste_signpos != -1) +#### dbg('sel_start:', sel_start, 'signpos:', signpos) +#### dbg('field._insertRight?', field._insertRight) +#### dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) + if paste_signpos != -1 and (sel_start <= signpos + or (field._insertRight and sel_start - len(paste_text) <= signpos)): + signed = True + else: + signed = False + # remove "sign" from paste text, so we can auto-adjust for sign type after paste: + paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','') +## dbg('unsigned paste text: "%s"' % paste_text) + else: + signed = False + + # another special case: deal with insert-right fields when selection is empty and + # cursor is at end of field: +#### dbg('field._insertRight?', field._insertRight) +#### dbg('sel_start == edit_end?', sel_start == edit_end) +#### dbg('sel_start', sel_start, 'sel_to', sel_to) + if field._insertRight and sel_start == edit_end and sel_start == sel_to: + sel_start -= len(paste_text) + if sel_start < 0: + sel_start = 0 +## dbg('adjusted selection:', (sel_start, sel_to)) + + try: + valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) + except: +## dbg('exception thrown', indent=0) + raise + + if not valid_paste: +## dbg('paste text not legal for the selection or portion of the control following the cursor;') + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + return False + # else... + text = self._eraseSelection() + + new_text = text[:sel_start] + replacement_text + text[replace_to:] + if new_text: + new_text = string.ljust(new_text,self._masklength) + if signed: + new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text) + if new_text: + if self._useParens: + new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:] + else: + new_text = new_text[:signpos] + '-' + new_text[signpos+1:] + if not self._isNeg: + self._isNeg = 1 + +## dbg("new_text:", '"'+new_text+'"') + + if not just_return_value: + if new_text != self._GetValue(): + self.modified = True + if new_text == '': + self.ClearValue() + else: + wx.CallAfter(self._SetValue, new_text) + if new_pos is None: + new_pos = sel_start + len(replacement_text) + wx.CallAfter(self._SetInsertionPoint, new_pos) + else: +## dbg(indent=0) + return new_text + elif just_return_value: +## dbg(indent=0) + return self._GetValue() +## dbg(indent=0) + + def _Undo(self): + """ Provides an Undo() method in base controls. """ +## dbg("MaskedEditMixin::_Undo", indent=1) + value = self._GetValue() + prev = self._prevValue +## dbg('current value: "%s"' % value) +## dbg('previous value: "%s"' % prev) + if prev is None: +## dbg('no previous value', indent=0) + return + + elif value != prev: + # Determine what to select: (relies on fixed-length strings) + # (This is a lot harder than it would first appear, because + # of mask chars that stay fixed, and so break up the "diff"...) + + # Determine where they start to differ: + i = 0 + length = len(value) # (both are same length in masked control) + + while( value[:i] == prev[:i] ): + i += 1 + sel_start = i - 1 + + + # handle signed values carefully, so undo from signed to unsigned or vice-versa + # works properly: + if self._signOk: + text, signpos, right_signpos = self._getSignedValue(candidate=prev) + if self._useParens: + if prev[signpos] == '(' and prev[right_signpos] == ')': + self._isNeg = True + else: + self._isNeg = False + # eliminate source of "far-end" undo difference if using balanced parens: + value = value.replace(')', ' ') + prev = prev.replace(')', ' ') + elif prev[signpos] == '-': + self._isNeg = True + else: + self._isNeg = False + + # Determine where they stop differing in "undo" result: + sm = difflib.SequenceMatcher(None, a=value, b=prev) + i, j, k = sm.find_longest_match(sel_start, length, sel_start, length) +## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) + + if k == 0: # no match found; select to end + sel_to = length + else: + code_5tuples = sm.get_opcodes() + for op, i1, i2, j1, j2 in code_5tuples: +## dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) + pass + + diff_found = False + # look backward through operations needed to produce "previous" value; + # first change wins: + for next_op in range(len(code_5tuples)-1, -1, -1): + op, i1, i2, j1, j2 = code_5tuples[next_op] +## dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) + if op == 'insert' and prev[j1:j2] != self._template[j1:j2]: +## dbg('insert found: selection =>', (j1, j2)) + sel_start = j1 + sel_to = j2 + diff_found = True + break + elif op == 'delete' and value[i1:i2] != self._template[i1:i2]: + field = self._FindField(i2) + edit_start, edit_end = field._extent + if field._insertRight and i2 == edit_end: + sel_start = i2 + sel_to = i2 + else: + sel_start = i1 + sel_to = j1 +## dbg('delete found: selection =>', (sel_start, sel_to)) + diff_found = True + break + elif op == 'replace': +## dbg('replace found: selection =>', (j1, j2)) + sel_start = j1 + sel_to = j2 + diff_found = True + break + + + if diff_found: + # now go forwards, looking for earlier changes: + for next_op in range(len(code_5tuples)): + op, i1, i2, j1, j2 = code_5tuples[next_op] + field = self._FindField(i1) + if op == 'equal': + continue + elif op == 'replace': +## dbg('setting sel_start to', i1) + sel_start = i1 + break + elif op == 'insert' and not value[i1:i2]: +## dbg('forward %s found' % op) + if prev[j1:j2].strip(): +## dbg('item to insert non-empty; setting sel_start to', j1) + sel_start = j1 + break + elif not field._insertRight: +## dbg('setting sel_start to inserted space:', j1) + sel_start = j1 + break + elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip(): + continue + else: + # we've got what we need + break + + + if not diff_found: +## dbg('no insert,delete or replace found (!)') + # do "left-insert"-centric processing of difference based on l.c.s.: + if i == j and j != sel_start: # match starts after start of selection + sel_to = sel_start + (j-sel_start) # select to start of match + else: + sel_to = j # (change ends at j) + + + # There are several situations where the calculated difference is + # not what we want to select. If changing sign, or just adding + # group characters, we really don't want to highlight the characters + # changed, but instead leave the cursor where it is. + # Also, there a situations in which the difference can be ambiguous; + # Consider: + # + # current value: 11234 + # previous value: 1111234 + # + # Where did the cursor actually lie and which 1s were selected on the delete + # operation? + # + # Also, difflib can "get it wrong;" Consider: + # + # current value: " 128.66" + # previous value: " 121.86" + # + # difflib produces the following opcodes, which are sub-optimal: + # equal value[0:9] ( 12) prev[0:9] ( 12) + # insert value[9:9] () prev[9:11] (1.) + # equal value[9:10] (8) prev[11:12] (8) + # delete value[10:11] (.) prev[12:12] () + # equal value[11:12] (6) prev[12:13] (6) + # delete value[12:13] (6) prev[13:13] () + # + # This should have been: + # equal value[0:9] ( 12) prev[0:9] ( 12) + # replace value[9:11] (8.6) prev[9:11] (1.8) + # equal value[12:13] (6) prev[12:13] (6) + # + # But it didn't figure this out! + # + # To get all this right, we use the previous selection recorded to help us... + + if (sel_start, sel_to) != self._prevSelection: +## dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) + + prev_sel_start, prev_sel_to = self._prevSelection + field = self._FindField(sel_start) + + if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')') + or self._curValue[sel_start] in ('-', '(', ')')): + # change of sign; leave cursor alone... + sel_start, sel_to = self._prevSelection + + elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar + or self._prevValue[sel_start:sel_to] == field._groupChar): + # do not highlight grouping changes + sel_start, sel_to = self._prevSelection + + else: + calc_select_len = sel_to - sel_start + prev_select_len = prev_sel_to - prev_sel_start + +## dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) +## dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) + + if prev_select_len >= calc_select_len: + # old selection was bigger; trust it: + sel_start, sel_to = self._prevSelection + + elif( sel_to > prev_sel_to # calculated select past last selection + and prev_sel_to < len(self._template) # and prev_sel_to not at end of control + and sel_to == len(self._template) ): # and calculated selection goes to end of control + + i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length) +## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) + if k > 0: + # difflib must not have optimized opcodes properly; + sel_to = j + + else: + # look for possible ambiguous diff: + + # if last change resulted in no selection, test from resulting cursor position: + if prev_sel_start == prev_sel_to: + calc_select_len = sel_to - sel_start + field = self._FindField(prev_sel_start) + + # determine which way to search from last cursor position for ambiguous change: + if field._insertRight: + test_sel_start = prev_sel_start + test_sel_to = prev_sel_start + calc_select_len + else: + test_sel_start = prev_sel_start - calc_select_len + test_sel_to = prev_sel_start + else: + test_sel_start, test_sel_to = prev_sel_start, prev_sel_to + +## dbg('test selection:', (test_sel_start, test_sel_to)) +## dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to]) +## dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to]) + + # if calculated selection spans characters, and same characters + # "before" the previous insertion point are present there as well, + # select the ones related to the last known selection instead. + if( sel_start != sel_to + and test_sel_to < len(self._template) + and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ): + + sel_start, sel_to = test_sel_start, test_sel_to + +## dbg('sel_start, sel_to:', sel_start, sel_to) +## dbg('previous value: "%s"' % self._prevValue) + self._SetValue(self._prevValue) + self._SetInsertionPoint(sel_start) + self._SetSelection(sel_start, sel_to) + else: +## dbg('no difference between previous value') + pass +## dbg(indent=0) + + + def _OnClear(self, event): + """ Provides an action for context menu delete operation """ + self.ClearValue() + + + def _OnContextMenu(self, event): +## dbg('MaskedEditMixin::OnContextMenu()', indent=1) + menu = wxMenu() + menu.Append(wxID_UNDO, "Undo", "") + menu.AppendSeparator() + menu.Append(wxID_CUT, "Cut", "") + menu.Append(wxID_COPY, "Copy", "") + menu.Append(wxID_PASTE, "Paste", "") + menu.Append(wxID_CLEAR, "Delete", "") + menu.AppendSeparator() + menu.Append(wxID_SELECTALL, "Select All", "") + + EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z) + EVT_MENU(menu, wxID_CUT, self._OnCtrl_X) + EVT_MENU(menu, wxID_COPY, self._OnCtrl_C) + EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V) + EVT_MENU(menu, wxID_CLEAR, self._OnClear) + EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A) + + # ## WSS: The base control apparently handles + # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE + # and wxID_CLEAR menu items even if the menu is one + # we created. However, it doesn't do undo properly, + # so we're keeping track of previous values ourselves. + # Therefore, we have to override the default update for + # that item on the menu: + EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI) + self._contextMenu = menu + + self.PopupMenu(menu, event.GetPosition()) + menu.Destroy() + self._contextMenu = None +## dbg(indent=0) + + def _UndoUpdateUI(self, event): + if self._prevValue is None or self._prevValue == self._curValue: + self._contextMenu.Enable(wxID_UNDO, False) + else: + self._contextMenu.Enable(wxID_UNDO, True) + + + def _OnCtrlParametersChanged(self): + """ + Overridable function to allow derived classes to take action as a + result of parameter changes prior to possibly changing the value + of the control. + """ + pass + + ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +# ## TRICKY BIT: to avoid a ton of boiler-plate, and to +# ## automate the getter/setter generation for each valid +# ## control parameter so we never forget to add the +# ## functions when adding parameters, this loop +# ## programmatically adds them to the class: +# ## (This makes it easier for Designers like Boa to +# ## deal with masked controls.) +# +# ## To further complicate matters, this is done with an +# ## extra level of inheritance, so that "general" classes like +# ## MaskedTextCtrl can have all possible attributes, +# ## while derived classes, like TimeCtrl and MaskedNumCtrl +# ## can prevent exposure of those optional attributes of their base +# ## class that do not make sense for their derivation. Therefore, +# ## we define +# ## BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) +# ## and +# ## MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). +# ## +# ## This allows us to then derive: +# ## MaskedNumCtrl( BaseMaskedTextCtrl ) +# ## +# ## and not have to expose all the same accessor functions for the +# ## derived control when they don't all make sense for it. +# ## +class MaskedEditAccessorsMixin: + + # Define the default set of attributes exposed by the most generic masked controls: + exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys() + exposed_basectrl_params.remove('index') + exposed_basectrl_params.remove('extent') + exposed_basectrl_params.remove('foregroundColour') # (base class already has this) + + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +## these are helper subroutines: + +def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): + """ addseparators = add separator character every three numerals if True + """ + fmt0 = fmtstring.split('.') + fmt1 = fmt0[0] + fmt2 = fmt0[1] + val = origvalue.split('.')[0].strip() + ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) + if neg: + ret = '-' + ret[1:] + return (ret,len(fmt1)) + + +def isDateType( fmtstring ): + """ Checks the mask and returns True if it fits an allowed + date or datetime format. + """ + dateMasks = ("^##/##/####", + "^##-##-####", + "^##.##.####", + "^####/##/##", + "^####-##-##", + "^####.##.##", + "^##/CCC/####", + "^##.CCC.####", + "^##/##/##$", + "^##/##/## ", + "^##/CCC/##$", + "^##.CCC.## ",) + reString = "|".join(dateMasks) + filter = re.compile( reString) + if re.match(filter,fmtstring): return True + return False + +def isTimeType( fmtstring ): + """ Checks the mask and returns True if it fits an allowed + time format. + """ + reTimeMask = "^##:##(:##)?( (AM|PM))?" + filter = re.compile( reTimeMask ) + if re.match(filter,fmtstring): return True + return False + + +def isFloatingPoint( fmtstring): + filter = re.compile("[ ]?[#]+\.[#]+\n") + if re.match(filter,fmtstring+"\n"): return True + return False + + +def isInteger( fmtstring ): + filter = re.compile("[#]+\n") + if re.match(filter,fmtstring+"\n"): return True + return False + + +def getDateParts( dateStr, dateFmt ): + if len(dateStr) > 11: clip = dateStr[0:11] + else: clip = dateStr + if clip[-2] not in string.digits: + clip = clip[:-1] # (got part of time; drop it) + + dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') + slices = clip.split(dateSep) + if dateFmt == "MDY": + y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts + elif dateFmt == "DMY": + y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts + elif dateFmt == "YMD": + y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts + else: + y,m,d = None, None, None + if not y: + return None + else: + return y,m,d + + +def getDateSepChar(dateStr): + clip = dateStr[0:10] + dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') + return dateSep + + +def makeDate( year, month, day, dateFmt, dateStr): + sep = getDateSepChar( dateStr) + if dateFmt == "MDY": + return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts + elif dateFmt == "DMY": + return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts + elif dateFmt == "YMD": + return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts + else: + return none + + +def getYear(dateStr,dateFmt): + parts = getDateParts( dateStr, dateFmt) + return parts[0] + +def getMonth(dateStr,dateFmt): + parts = getDateParts( dateStr, dateFmt) + return parts[1] + +def getDay(dateStr,dateFmt): + parts = getDateParts( dateStr, dateFmt) + return parts[2] + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- +class test(wx.PySimpleApp): + def OnInit(self): + from wx.lib.rcsizer import RowColSizer + self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) + self.panel = wx.Panel( self.frame, -1) + self.sizer = RowColSizer() + self.labels = [] + self.editList = [] + rowcount = 4 + + id, id1 = wx.NewId(), wx.NewId() + self.command1 = wx.Button( self.panel, id, "&Close" ) + self.command2 = wx.Button( self.panel, id1, "&AutoFormats" ) + self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) + self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) + self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) +## self.panel.SetDefaultItem(self.command1 ) + self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) + + self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) + self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) + self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) + self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) + self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) + self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) + + + label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. +Note that all controls have been auto-sized by including F in the format code. +Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" + label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." + + self.label1 = wx.StaticText( self.panel, -1, label) + self.label2 = wx.StaticText( self.panel, -1, "Description") + self.label3 = wx.StaticText( self.panel, -1, "Mask Value") + self.label4 = wx.StaticText( self.panel, -1, "Format") + self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") + self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") + self.label7 = wx.StaticText( self.panel, -1, label2) + self.label7.SetForegroundColour("Blue") + self.label1.SetForegroundColour("Blue") + self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + + self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) + self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) + self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) + self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) + self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) + self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) + self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) + + # The following list is of the controls for the demo. Feel free to play around with + # the options! + controls = [ + #description mask excl format regexp range,list,initial + ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''), + ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''), + ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''), + ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''), + ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''), + ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'), + ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''), + ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'), + ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''), + ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '), + ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '), + ("Month selector", "XXX", "", 'F', "", (), + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), + ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (), + ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") + ] + + for control in controls: + self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) + + if control in controls[:]:#-2]: + newControl = MaskedTextCtrl( self.panel, -1, "", + mask = control[1], + excludeChars = control[2], + formatcodes = control[3], + includeChars = "", + validRegex = control[4], + validRange = control[5], + choices = control[6], + defaultValue = control[7], + demo = True) + if control[6]: newControl.SetCtrlParameters(choiceRequired = True) + else: + newControl = MaskedComboBox( self.panel, -1, "", + choices = control[7], + choiceRequired = True, + mask = control[1], + formatcodes = control[3], + excludeChars = control[2], + includeChars = "", + validRegex = control[4], + validRange = control[5], + demo = True) + self.editList.append( newControl ) + + self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) + rowcount += 1 + + self.sizer.AddGrowableCol(4) + + self.panel.SetSizer(self.sizer) + self.panel.SetAutoLayout(1) + + self.frame.Show(1) + self.MainLoop() + + return True + + def onClick(self, event): + self.frame.Close() + + def onClickPage(self, event): + self.page2 = test2(self.frame,-1,"") + self.page2.Show(True) + + def _onCheck1(self,event): + """ Set required value on/off """ + value = event.IsChecked() + if value: + for control in self.editList: + control.SetCtrlParameters(emptyInvalid=True) + control.Refresh() + else: + for control in self.editList: + control.SetCtrlParameters(emptyInvalid=False) + control.Refresh() + self.panel.Refresh() + + def _onCheck2(self,event): + """ Highlight empty values""" + value = event.IsChecked() + if value: + for control in self.editList: + control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') + control.Refresh() + else: + for control in self.editList: + control.SetCtrlParameters( emptyBackgroundColour = 'White') + control.Refresh() + self.panel.Refresh() + + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +class test2(wx.Frame): + def __init__(self, parent, id, caption): + wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) + from wx.lib.rcsizer import RowColSizer + self.panel = wx.Panel( self, -1) + self.sizer = RowColSizer() + self.labels = [] + self.texts = [] + rowcount = 4 + + label = """\ +All these controls have been created by passing a single parameter, the AutoFormat code. +The class contains an internal dictionary of types and formats (autoformats). +To see a great example of validations in action, try entering a bad email address, then tab out.""" + + self.label1 = wx.StaticText( self.panel, -1, label) + self.label2 = wx.StaticText( self.panel, -1, "Description") + self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") + self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") + self.label1.SetForegroundColour("Blue") + self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) + + self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) + self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) + self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) + self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) + + id, id1 = wx.NewId(), wx.NewId() + self.command1 = wx.Button( self.panel, id, "&Close") + self.command2 = wx.Button( self.panel, id1, "&Print Formats") + self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) + self.panel.SetDefaultItem(self.command1) + self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) + + # The following list is of the controls for the demo. Feel free to play around with + # the options! + controls = [ + ("Phone No","USPHONEFULLEXT"), + ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), + ("US Date MMDDYYYY","USDATEMMDDYYYY/"), + ("Time (with seconds)","TIMEHHMMSS"), + ("Military Time\n(without seconds)","24HRTIMEHHMM"), + ("Social Sec#","USSOCIALSEC"), + ("Credit Card","CREDITCARD"), + ("Expiration MM/YY","EXPDATEMMYY"), + ("Percentage","PERCENT"), + ("Person's Age","AGE"), + ("US Zip Code","USZIP"), + ("US Zip+4","USZIPPLUS4"), + ("Email Address","EMAIL"), + ("IP Address", "(derived control IpAddrCtrl)") + ] + + for control in controls: + self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) + self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) + if control in controls[:-1]: + self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", + autoformat = control[1], + demo = True), + row=rowcount,col=2,flag=wx.ALL,border=5) + else: + self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), + row=rowcount,col=2,flag=wx.ALL,border=5) + rowcount += 1 + + self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) + self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) + self.sizer.AddGrowableCol(3) + + self.panel.SetSizer(self.sizer) + self.panel.SetAutoLayout(1) + + def onClick(self, event): + self.Close() + + def onClickPrint(self, event): + for format in masktags.keys(): + sep = "+------------------------+" + print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) + +## ---------- ---------- ---------- ---------- ---------- ---------- ---------- + +if __name__ == "__main__": + app = test(False) + +i=1 +## +## Current Issues: +## =================================== +## +## 1. WS: For some reason I don't understand, the control is generating two (2) +## EVT_TEXT events for every one (1) .SetValue() of the underlying control. +## I've been unsuccessful in determining why or in my efforts to make just one +## occur. So, I've added a hack to save the last seen value from the +## control in the EVT_TEXT handler, and if *different*, call event.Skip() +## to propagate it down the event chain, and let the application see it. +## +## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the +## behavior of the underlying control that I can't fix. The problems are: +## a) The background coloring doesn't work in the text field of the control; +## instead, there's a only border around it that assumes the correct color. +## b) The control will not pass WXK_TAB to the event handler, no matter what +## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to +## indicate that we want these events. As a result, MaskedComboBox +## doesn't do the nice field-tabbing that MaskedTextCtrl does. +## c) Auto-complete had to be reimplemented for the control because programmatic +## setting of the value of the text field does not set up the auto complete +## the way that the control processing keystrokes does. (But I think I've +## implemented a fairly decent approximation.) Because of this the control +## also won't auto-complete on dropdown, and there's no event I can catch +## to work around this problem. +## d) There is no method provided for getting the selection; the hack I've +## implemented has its flaws, not the least of which is that due to the +## strategy that I'm using, the paste buffer is always replaced by the +## contents of the control's selection when in focus, on each keystroke; +## this makes it impossible to paste anything into a MaskedComboBox +## at the moment... :-( +## e) The other deficient behavior, likely induced by the workaround for (d), +## is that you can can't shift-left to select more than one character +## at a time. +## +## +## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their +## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in +## wxWindows, shift-tab won't take you backwards through the fields of +## a MaskedTextCtrl like it should. Until then Shifted arrow keys will +## work like shift-tab and tab ought to. +## + +## To-Do's: +## =============================## +## 1. Add Popup list for auto-completable fields that simulates combobox on individual +## fields. Example: City validates against list of cities, or zip vs zip code list. +## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" +## control. +## 3. Fix shift-left selection for MaskedComboBox. +## 5. Transform notion of "decimal control" to be less "entire control"-centric, +## so that monetary symbols can be included and still have the appropriate +## semantics. (Big job, as currently written, but would make control even +## more useful for business applications.) + + +## CHANGELOG: +## ==================== +## Version 1.6 +## 1. Reorganized masked controls into separate package, renamed things accordingly +## 2. Split actual controls out of this file into their own files. +## Version 1.5 +## (Reported) bugs fixed: +## 1. Crash ensues if you attempt to change the mask of a read-only +## MaskedComboBox after initial construction. +## 2. Changed strategy of defining Get/Set property functions so that +## these are now generated dynamically at runtime, rather than as +## part of the class definition. (This makes it possible to have +## more general base classes that have many more options for configuration +## without requiring that derivations support the same options.) +## 3. Fixed IsModified for _Paste() and _OnErase(). +## +## Enhancements: +## 1. Fixed "attribute function inheritance," since base control is more +## generic than subsequent derivations, not all property functions of a +## generic control should be exposed in those derivations. New strategy +## uses base control classes (eg. BaseMaskedTextCtrl) that should be +## used to derive new class types, and mixed with their own mixins to +## only expose those attributes from the generic masked controls that +## make sense for the derivation. (This makes Boa happier.) +## 2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less +## "parochial." +## +## Version 1.4 +## (Reported) bugs fixed: +## 1. Right-click menu allowed "cut" operation that destroyed mask +## (was implemented by base control) +## 2. MaskedComboBox didn't allow .Append() of mixed-case values; all +## got converted to lower case. +## 3. MaskedComboBox selection didn't deal with spaces in values +## properly when autocompleting, and didn't have a concept of "next" +## match for handling choice list duplicates. +## 4. Size of MaskedComboBox was always default. +## 5. Email address regexp allowed some "non-standard" things, and wasn't +## general enough. +## 6. Couldn't easily reset MaskedComboBox contents programmatically. +## 7. Couldn't set emptyInvalid during construction. +## 8. Under some versions of wxPython, readonly comboboxes can apparently +## return a GetInsertionPoint() result (655535), causing masked control +## to fail. +## 9. Specifying an empty mask caused the controls to traceback. +## 10. Can't specify float ranges for validRange. +## 11. '.' from within a the static portion of a restricted IP address +## destroyed the mask from that point rightward; tab when cursor is +## before 1st field takes cursor past that field. +## +## Enhancements: +## 12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) +## 13. Added auto-select option on char input for masked controls with +## choice lists. +## 14. Added '>' formatcode, allowing insert within a given or each field +## as appropriate, rather than requiring "overwrite". This makes single +## field controls that just have validation rules (eg. EMAIL) much more +## friendly. The same flag controls left shift when deleting vs just +## blanking the value, and for right-insert fields, allows right-insert +## at any non-blank (non-sign) position in the field. +## 15. Added option to use to indicate negative values for numeric controls. +## 16. Improved OnFocus handling of numeric controls. +## 17. Enhanced Home/End processing to allow operation on a field level, +## using ctrl key. +## 18. Added individual Get/Set functions for control parameters, for +## simplified integration with Boa Constructor. +## 19. Standardized "Colour" parameter names to match wxPython, with +## non-british spellings still supported for backward-compatibility. +## 20. Added '&' mask specification character for punctuation only (no letters +## or digits). +## 21. Added (in a separate file) wxMaskedCtrl() factory function to provide +## unified interface to the masked edit subclasses. +## +## +## Version 1.3 +## 1. Made it possible to configure grouping, decimal and shift-decimal characters, +## to make controls more usable internationally. +## 2. Added code to smart "adjust" value strings presented to .SetValue() +## for right-aligned numeric format controls if they are shorter than +## than the control width, prepending the missing portion, prepending control +## template left substring for the missing characters, so that setting +## numeric values is easier. +## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved +## for b-c), as this makes more sense. +## +## Version 1.2 +## 1. Fixed .SetValue() to replace the current value, rather than the current +## selection. Also changed it to generate ValueError if presented with +## either a value which doesn't follow the format or won't fit. Also made +## set value adjust numeric and date controls as if user entered the value. +## Expanded doc explaining how SetValue() works. +## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to +## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. +## 3. Made all date autoformats automatically pick implied "datestyle". +## 4. Added IsModified override, since base wxTextCtrl never reports modified if +## .SetValue used to change the value, which is what the masked edit controls +## use internally. +## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when +## using tab to "leave field" and auto-adjust. +## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, +## and bug in ._Paste() that didn't account for signs in signed masks either. +## 7. Fixed issues with _adjustPos for right-insert fields causing improper +## selection/replacement of values +## 8. Fixed _OnHome handler to properly handle extending current selection to +## beginning of control. +## 9. Exposed all (valid) autoformats to demo, binding descriptions to +## autoformats. +## 10. Fixed a couple of bugs in email regexp. +## 11. Made maskchardict an instance var, to make mask chars to be more +## amenable to international use. +## 12. Clarified meaning of '-' formatcode in doc. +## 13. Fixed a couple of coding bugs being flagged by Python2.1. +## 14. Fixed several issues with sign positioning, erasure and validity +## checking for "numeric" masked controls. +## 15. Added validation to IpAddrCtrl.SetValue(). +## +## Version 1.1 +## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) +## vs. literal font facename, and use wxTELETYPE as the font family +## if so specified. +## 2. Switched to use of dbg module vs. locally defined version. +## 3. Revamped entire control structure to use Field classes to hold constraint +## and formatting data, to make code more hierarchical, allow for more +## sophisticated masked edit construction. +## 4. Better strategy for managing options, and better validation on keywords. +## 5. Added 'V' format code, which requires that in order for a character +## to be accepted, it must result in a string that passes the validRegex. +## 6. Added 'S' format code which means "select entire field when navigating +## to new field." +## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) +## 8. Added '<' format code to allow fields to require explicit cursor movement +## to leave field. +## 9. Added validFunc option to other validation mechanisms, that allows derived +## classes to add dynamic validation constraints to the control. +## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also +## fixed failure to obey case conversion codes when pasting. +## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... +## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' +## 13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, +## individual field validation to prevent field values > 255, and require explicit +## tab/. to change fields. +## 14. Added handler for left double-click to select field under cursor. +## 15. Fixed handling for "Read-only" styles. +## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor +## attribute, for more consistent and controllable coloring. +## 17. Added retainFieldValidation parameter, allowing top-level constraints +## such as "validRequired" to be set independently of field-level equivalent. +## (needed in TimeCtrl for bounds constraints.) +## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed +## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, +## etc. +## 19. Fixed maskchar setting for upper/lowercase, to work in all locales. +## +## +## Version 1.0 +## 1. Decimal point behavior restored for decimal and integer type controls: +## decimal point now trucates the portion > 0. +## 2. Return key now works like the tab character and moves to the next field, +## provided no default button is set for the form panel on which the control +## resides. +## 3. Support added in _FindField() for subclasses controls (like timecontrol) +## to determine where the current insertion point is within the mask (i.e. +## which sub-'field'). See method documentation for more info and examples. +## 4. Added Field class and support for all constraints to be field-specific +## in addition to being globally settable for the control. +## Choices for each field are validated for length and pastability into +## the field in question, raising ValueError if not appropriate for the control. +## Also added selective additional validation based on individual field constraints. +## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all +## auto-complete fields with choice lists, supplying the 1st entry in +## the choice list if the field is empty, and cycling through the list in +## the appropriate direction if already a match. WXK_DOWN will also auto- +## complete if the field is partially completed and a match can be made. +## SHIFT-WXK_UP/DOWN will also take you to the next field after any +## auto-completion performed. +## 5. Added autoCompleteKeycodes=[] parameters for allowing further +## customization of the control. Any keycode supplied as a member +## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If +## requireFieldChoice is set, then a valid value from each non-empty +## choice list will be required for the value of the control to validate. +## 6. Fixed "auto-sizing" to be relative to the font actually used, rather +## than making assumptions about character width. +## 7. Fixed GetMaskParameter(), which was non-functional in previous version. +## 8. Fixed exceptions raised to provide info on which control had the error. +## 9. Fixed bug in choice management of MaskedComboBox. +## 10. Fixed bug in IpAddrCtrl causing traceback if field value was of +## the form '# #'. Modified control code for IpAddrCtrl so that '.' +## in the middle of a field clips the rest of that field, similar to +## decimal and integer controls. +## +## +## Version 0.0.7 +## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. +## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). +## 3. New support for selecting text within the control.(thanks Will Sadkin!) +## Shift-End and Shift-Home now select text as you would expect +## Control-Shift-End selects to the end of the mask string, even if value not entered. +## Control-A selects all *entered* text, Shift-Control-A selects everything in the control. +## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- +## for some reason I couldn't find the original email but thanks!!!) +## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome, +## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. +## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). +## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) +## 7. New mechanism for replacing default behavior for any given key, using +## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available +## for easier subclassing of the control. +## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs +## with insertion point/selection modification. Changed Ctrl-X to use standard "cut" +## semantics, erasing the selection, rather than erasing the entire control. +## 9. Added option for an "default value" (ie. the template) for use when a single fillChar +## is not desired in every position. Added IsDefault() function to mean "does the value +## equal the template?" and modified .IsEmpty() to mean "do all of the editable +## positions in the template == the fillChar?" +## 10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, +## now included. +## 11. MaskedComboBox now adds the capability to validate from list of valid values. +## Example: City validates against list of cities, or zip vs zip code list. +## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being +## passed to the next handler in the event chain, causing updates to the +## control to be invisible to the parent code. +## 13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within +## the control, that auto-reformats as you move between cells. +## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. +## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} +## 16. Fixed major bugs in date validation, due to the fact that +## wxDateTime.ParseDate is too liberal, and will accept any form that +## makes any kind of sense, regardless of the datestyle you specified +## for the control. Unfortunately, the strategy used to fix it only +## works for versions of wxPython post 2.3.3.1, as a C++ assert box +## seems to show up on an invalid date otherwise, instead of a catchable +## exception. +## 17. Enhanced date adjustment to automatically adjust heuristic based on +## current year, making last century/this century determination on +## 2-digit year based on distance between today's year and value; +## if > 50 year separation, assume last century (and don't assume last +## century is 20th.) +## 18. Added autoformats and support for including HHMMSS as well as HHMM for +## date times, and added similar time, and militaray time autoformats. +## 19. Enhanced tabbing logic so that tab takes you to the next field if the +## control is a multi-field control. +## 20. Added stub method called whenever the control "changes fields", that +## can be overridden by subclasses (eg. IpAddrCtrl.) +## 21. Changed a lot of code to be more functionally-oriented so side-effects +## aren't as problematic when maintaining code and/or adding features. +## Eg: IsValid() now does not have side-effects; it merely reflects the +## validity of the value of the control; to determine validity AND recolor +## the control, _CheckValid() should be used with a value argument of None. +## Similarly, made most reformatting function take an optional candidate value +## rather than just using the current value of the control, and only +## have them change the value of the control if a candidate is not specified. +## In this way, you can do validation *before* changing the control. +## 22. Changed validRequired to mean "disallow chars that result in invalid +## value." (Old meaning now represented by emptyInvalid.) (This was +## possible once I'd made the changes in (19) above.) +## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they +## can be set/modified/retrieved after construction. Removed individual +## parameter setting functions, in favor of this mechanism, so that +## all adjustment of the control based on changing parameter values can +## be handled in one place with unified mechanism. +## 24. Did a *lot* of testing and fixing re: numeric values. Added ability +## to type "grouping char" (ie. ',') and validate as appropriate. +## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. +## 26. Fixed assumption about "decimal or integer" masks so that they're only +## made iff there's no validRegex associated with the field. (This +## is so things like zipcodes which look like integers can have more +## restrictive validation (ie. must be 5 digits.) +## 27. Added a ton more doc strings to explain use and derivation requirements +## and did regularization of the naming conventions. +## 28. Fixed a range bug in _adjustKey preventing z from being handled properly. +## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to +## reformat the value and move the next field as appropriate. (shift-'.', +## ie. '>' moves to the previous field. + +## Version 0.0.6 +## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending +## in '0'. +## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the +## year position, the control will expand the value to four digits, using numerals below +## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). +## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} +## 3. revalid parameter renamed validRegex to conform to standard for all validation +## parameters (see 2 new ones below). +## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types. +## Allows the developer to specify a valid low/high range of values. +## 5. New optional init parameter = validList. Used for character types. Allows developer +## to send a list of values to the control to be used for specific validation. +## See the Last Name Only example - it is list restricted to Smith/Jones/Williams. +## 6. Date type fields now use wxDateTime's parser to validate the date and time. +## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing +## me toward this solution! +## 7. Date fields now automatically expand 2-digit years when it can. For example, +## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year +## date is entered it will be expanded in any case when the user tabs out of the +## field. +## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, +## SetSignedForeColor allow accessto override default class coloring behavior. +## 9. Documentation updated and improved. +## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. +## Two new options (checkboxes) - test highlight empty and disallow empty. +## 11. Home and End now work more intuitively, moving to the first and last user-entry +## value, respectively. +## 12. New class function: SetRequired(bool). Sets the control's entry required flag +## (i.e. disallow empty values if True). +## +## Version 0.0.5 +## 1. get_plainValue method renamed to GetPlainValue following the wxWindows +## StudlyCaps(tm) standard (thanks Paul Moore). ;) +## 2. New format code 'F' causes the control to auto-fit (auto-size) itself +## based on the length of the mask template. +## 3. Class now supports "autoformat" codes. These can be passed to the class +## on instantiation using the parameter autoformat="code". If the code is in +## the dictionary, it will self set the mask, formatting, and validation string. +## I have included a number of samples, but I am hoping that someone out there +## can help me to define a whole bunch more. +## 4. I have added a second page to the demo (as well as a second demo class, test2) +## to showcase how autoformats work. The way they self-format and self-size is, +## I must say, pretty cool. +## 5. Comments added and some internal cosmetic revisions re: matching the code +## standards for class submission. +## 6. Regex validation is now done in real time - field turns yellow immediately +## and stays yellow until the entered value is valid +## 7. Cursor now skips over template characters in a more intuitive way (before the +## next keypress). +## 8. Change, Keypress and LostFocus methods added for convenience of subclasses. +## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, +## and EVT_KILL_FOCUS, respectively. +## 9. Decimal and numeric handlers have been rewritten and now work more intuitively. +## +## Version 0.0.4 +## 1. New .IsEmpty() method returns True if the control's value is equal to the +## blank template string +## 2. Control now supports a new init parameter: revalid. Pass a regular expression +## that the value will have to match when the control loses focus. If invalid, +## the control's BackgroundColor will turn yellow, and an internal flag is set (see next). +## 3. Demo now shows revalid functionality. Try entering a partial value, such as a +## partial social security number. +## 4. New .IsValid() value returns True if the control is empty, or if the value matches +## the revalid expression. If not, .IsValid() returns False. +## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never +## presses the decimal point. +## 6. Cursor now goes to the beginning of the field if the user clicks in an +## "empty" field intead of leaving the insertion point in the middle of the +## field. +## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. +## 8. New formatcodes init parameter replaces other init params and adds functions. +## String passed to control on init controls: +## _ Allow spaces +## ! Force upper +## ^ Force lower +## R Show negative #s in red +## , Group digits +## - Signed numerals +## 0 Numeric fields get leading zeros +## 9. Ctrl-X in any field clears the current value. +## 10. Code refactored and made more modular (esp in OnChar method). Should be more +## easy to read and understand. +## 11. Demo enhanced. +## 12. Now has _doc_. +## +## Version 0.0.3 +## 1. GetPlainValue() now returns the value without the template characters; +## so, for example, a social security number (123-33-1212) would return as +## 123331212; also removes white spaces from numeric/decimal values, so +## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value. +## 2. Press '.' in an integer style masked control and truncate any trailing digits. +## 3. Code moderately refactored. Internal names improved for clarity. Additional +## internal documentation. +## 4. Home and End keys now supported to move cursor to beginning or end of field. +## 5. Un-signed integers and decimals now supported. +## 6. Cosmetic improvements to the demo. +## 7. Class renamed to MaskedTextCtrl. +## 8. Can now specify include characters that will override the basic +## controls: for example, includeChars = "@." for email addresses +## 9. Added mask character 'C' -> allow any upper or lowercase character +## 10. .SetSignColor(str:color) sets the foreground color for negative values +## in signed controls (defaults to red) +## 11. Overview documentation written. +## +## Version 0.0.2 +## 1. Tab now works properly when pressed in last position +## 2. Decimal types now work (e.g. #####.##) +## 3. Signed decimal or numeric values supported (i.e. negative numbers) +## 4. Negative decimal or numeric values now can show in red. +## 5. Can now specify an "exclude list" with the excludeChars parameter. +## See date/time formatted example - you can only enter A or P in the +## character mask space (i.e. AM/PM). +## 6. Backspace now works properly, including clearing data from a selected +## region but leaving template characters intact. Also delete key. +## 7. Left/right arrows now work properly. +## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3 +## diff --git a/wxPython/wx/lib/masked/numctrl.py b/wxPython/wx/lib/masked/numctrl.py new file mode 100644 index 0000000000..17c95888d7 --- /dev/null +++ b/wxPython/wx/lib/masked/numctrl.py @@ -0,0 +1,1616 @@ +#---------------------------------------------------------------------------- +# Name: wxPython.lib.masked.numctrl.py +# Author: Will Sadkin +# Created: 09/06/2003 +# Copyright: (c) 2003 by Will Sadkin +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# NOTE: +# This was written to provide a numeric edit control for wxPython that +# does things like right-insert (like a calculator), and does grouping, etc. +# (ie. the features of masked.TextCtrl), but allows Get/Set of numeric +# values, rather than text. +# +# Masked.NumCtrl permits integer, and floating point values to be set +# retrieved or set via .GetValue() and .SetValue() (type chosen based on +# fraction width, and provides an masked.EVT_NUM() event function for trapping +# changes to the control. +# +# It supports negative numbers as well as the naturals, and has the option +# of not permitting leading zeros or an empty control; if an empty value is +# not allowed, attempting to delete the contents of the control will result +# in a (selected) value of zero, thus preserving a legitimate numeric value. +# Similarly, replacing the contents of the control with '-' will result in +# a selected (absolute) value of -1. +# +# masked.NumCtrl also supports range limits, with the option of either +# enforcing them or simply coloring the text of the control if the limits +# are exceeded. +# +# masked.NumCtrl is intended to support fixed-point numeric entry, and +# is derived from BaseMaskedTextCtrl. As such, it supports a limited range +# of values to comply with a fixed-width entry mask. +#---------------------------------------------------------------------------- +# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for wx namespace +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMaskedEditMixin -> MaskedEditMixin +# o wxMaskedTextCtrl -> masked.TextCtrl +# o wxMaskedNumNumberUpdatedEvent -> masked.NumberUpdatedEvent +# o wxMaskedNumCtrl -> masked.NumCtrl +# + +""" +

+masked.NumCtrl: +

+

+Being derived from MaskedTextCtrl, the control only allows +fixed-point notation. That is, it has a fixed (though reconfigurable) +maximum width for the integer portion and optional fixed width +fractional portion. +

+Here's the API: +

+    masked.NumCtrl(
+         parent, id = -1,
+         value = 0,
+         pos = wx.DefaultPosition,
+         size = wx.DefaultSize,
+         style = 0,
+         validator = wx.DefaultValidator,
+         name = "masked.number",
+         integerWidth = 10,
+         fractionWidth = 0,
+         allowNone = False,
+         allowNegative = True,
+         useParensForNegatives = False,
+         groupDigits = False,
+         groupChar = ',',
+         decimalChar = '.',
+         min = None,
+         max = None,
+         limited = False,
+         selectOnEntry = True,
+         foregroundColour = "Black",
+         signedForegroundColour = "Red",
+         emptyBackgroundColour = "White",
+         validBackgroundColour = "White",
+         invalidBackgroundColour = "Yellow",
+         autoSize = True
+         )
+
+
    +
    value +
    If no initial value is set, the default will be zero, or + the minimum value, if specified. If an illegal string is specified, + a ValueError will result. (You can always later set the initial + value with SetValue() after instantiation of the control.) +
    +
    integerWidth +
    Indicates how many places to the right of any decimal point + should be allowed in the control. This will, perforce, limit + the size of the values that can be entered. This number need + not include space for grouping characters or the sign, if either + of these options are enabled, as the resulting underlying + mask is automatically by the control. The default of 10 + will allow any 32 bit integer value. The minimum value + for integerWidth is 1. +
    +
    fractionWidth +
    Indicates how many decimal places to show for numeric value. + If default (0), then the control will display and return only + integer or long values. +
    +
    allowNone +
    Boolean indicating whether or not the control is allowed to be + empty, representing a value of None for the control. +
    +
    allowNegative +
    Boolean indicating whether or not control is allowed to hold + negative numbers. +
    +
    useParensForNegatives +
    If true, this will cause negative numbers to be displayed with ()s + rather than -, (although '-' will still trigger a negative number.) +
    +
    groupDigits +
    Indicates whether or not grouping characters should be allowed and/or + inserted when leaving the control or the decimal character is entered. +
    +
    groupChar +
    What grouping character will be used if allowed. (By default ',') +
    +
    decimalChar +
    If fractionWidth is > 0, what character will be used to represent + the decimal point. (By default '.') +
    +
    min +
    The minimum value that the control should allow. This can be also be + adjusted with SetMin(). If the control is not limited, any value + below this bound will result in a background colored with the current + invalidBackgroundColour. If the min specified will not fit into the + control, the min setting will be ignored. +
    +
    max +
    The maximum value that the control should allow. This can be + adjusted with SetMax(). If the control is not limited, any value + above this bound will result in a background colored with the current + invalidBackgroundColour. If the max specified will not fit into the + control, the max setting will be ignored. +
    +
    limited +
    Boolean indicating whether the control prevents values from + exceeding the currently set minimum and maximum values (bounds). + If False and bounds are set, out-of-bounds values will + result in a background colored with the current invalidBackgroundColour. +
    +
    selectOnEntry +
    Boolean indicating whether or not the value in each field of the + control should be automatically selected (for replacement) when + that field is entered, either by cursor movement or tabbing. + This can be desirable when using these controls for rapid data entry. +
    +
    foregroundColour +
    Color value used for positive values of the control. +
    +
    signedForegroundColour +
    Color value used for negative values of the control. +
    +
    emptyBackgroundColour +
    What background color to use when the control is considered + "empty." (allow_none must be set to trigger this behavior.) +
    +
    validBackgroundColour +
    What background color to use when the control value is + considered valid. +
    +
    invalidBackgroundColour +
    Color value used for illegal values or values out-of-bounds of the + control when the bounds are set but the control is not limited. +
    +
    autoSize +
    Boolean indicating whether or not the control should set its own + width based on the integer and fraction widths. True by default. + Note: Setting this to False will produce seemingly odd + behavior unless the control is large enough to hold the maximum + specified value given the widths and the sign positions; if not, + the control will appear to "jump around" as the contents scroll. + (ie. autoSize is highly recommended.) +
+
+
+
masked.EVT_NUM(win, id, func) +
Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when +the value changes. Notice that this event will always be sent when the +control's contents changes - whether this is due to user input or +comes from the program itself (for example, if SetValue() is called.) +
+
+
SetValue(int|long|float|string) +
Sets the value of the control to the value specified, if +possible. The resulting actual value of the control may be +altered to conform to the format of the control, changed +to conform with the bounds set on the control if limited, +or colored if not limited but the value is out-of-bounds. +A ValueError exception will be raised if an invalid value +is specified. +
+
GetValue() +
Retrieves the numeric value from the control. The value +retrieved will be either be returned as a long if the +fractionWidth is 0, or a float otherwise. +
+
+
SetParameters(**kwargs) +
Allows simultaneous setting of various attributes +of the control after construction. Keyword arguments +allowed are the same parameters as supported in the constructor. +
+
+
SetIntegerWidth(value) +
Resets the width of the integer portion of the control. The +value must be >= 1, or an AttributeError exception will result. +This value should account for any grouping characters that might +be inserted (if grouping is enabled), but does not need to account +for the sign, as that is handled separately by the control. +
GetIntegerWidth() +
Returns the current width of the integer portion of the control, +not including any reserved sign position. +
+
+
SetFractionWidth(value) +
Resets the width of the fractional portion of the control. The +value must be >= 0, or an AttributeError exception will result. If +0, the current value of the control will be truncated to an integer +value. +
GetFractionWidth() +
Returns the current width of the fractional portion of the control. +
+
+
SetMin(min=None) +
Resets the minimum value of the control. If a value of None +is provided, then the control will have no explicit minimum value. +If the value specified is greater than the current maximum value, +then the function returns False and the minimum will not change from +its current setting. On success, the function returns True. +
+If successful and the current value is lower than the new lower +bound, if the control is limited, the value will be automatically +adjusted to the new minimum value; if not limited, the value in the +control will be colored as invalid. +
+If min > the max value allowed by the width of the control, +the function will return False, and the min will not be set. +
+
GetMin() +
Gets the current lower bound value for the control. +It will return None if no lower bound is currently specified. +
+
+
SetMax(max=None) +
Resets the maximum value of the control. If a value of None +is provided, then the control will have no explicit maximum value. +If the value specified is less than the current minimum value, then +the function returns False and the maximum will not change from its +current setting. On success, the function returns True. +
+If successful and the current value is greater than the new upper +bound, if the control is limited the value will be automatically +adjusted to this maximum value; if not limited, the value in the +control will be colored as invalid. +
+If max > the max value allowed by the width of the control, +the function will return False, and the max will not be set. +
+
GetMax() +
Gets the current upper bound value for the control. +It will return None if no upper bound is currently specified. +
+
+
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. +
GetBounds() +
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. +
+
+
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. +
+
+
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. +If the control's value currently exceeds the bounds, it will then +be limited accordingly. +If called with a value of False, this function will disable value +limiting, but coloring of out-of-bounds values will still take +place if bounds have been set for the control. +
GetLimited() +
IsLimited() +
Returns True if the control is currently limiting the +value to fall within the current bounds. +
+
+
SetAllowNone(bool) +
If called with a value of True, this function will cause the control +to allow the value to be empty, representing a value of None. +If called with a value of False, this function will prevent the value +from being None. If the value of the control is currently None, +ie. the control is empty, then the value will be changed to that +of the lower bound of the control, or 0 if no lower bound is set. +
GetAllowNone() +
IsNoneAllowed() +
Returns True if the control currently allows its +value to be None. +
+
+
SetAllowNegative(bool) +
If called with a value of True, this function will cause the +control to allow the value to be negative (and reserve space for +displaying the sign. If called with a value of False, and the +value of the control is currently negative, the value of the +control will be converted to the absolute value, and then +limited appropriately based on the existing bounds of the control +(if any). +
GetAllowNegative() +
IsNegativeAllowed() +
Returns True if the control currently permits values +to be negative. +
+
+
SetGroupDigits(bool) +
If called with a value of True, this will make the control +automatically add and manage grouping characters to the presented +value in integer portion of the control. +
GetGroupDigits() +
IsGroupingAllowed() +
Returns True if the control is currently set to group digits. +
+
+
SetGroupChar() +
Sets the grouping character for the integer portion of the +control. (The default grouping character this is ','. +
GetGroupChar() +
Returns the current grouping character for the control. +
+
+
SetSelectOnEntry() +
If called with a value of True, this will make the control +automatically select the contents of each field as it is entered +within the control. (The default is True.) +
GetSelectOnEntry() +
Returns True if the control currently auto selects +the field values on entry. +
+
+
SetAutoSize(bool) +
Resets the autoSize attribute of the control. +
GetAutoSize() +
Returns the current state of the autoSize attribute for the control. +
+
+
+ +""" + +import copy +import string +import types + +import wx + +from sys import maxint +MAXINT = maxint # (constants should be in upper case) +MININT = -maxint-1 + +from wx.tools.dbg import Logger +from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl +dbg = Logger() +##dbg(enable=0) + +#---------------------------------------------------------------------------- + +wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wx.NewEventType() +EVT_NUM = wx.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED, 1) + +#---------------------------------------------------------------------------- + +class NumberUpdatedEvent(wx.PyCommandEvent): + def __init__(self, id, value = 0, object=None): + wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id) + + self.__value = value + self.SetEventObject(object) + + def GetValue(self): + """Retrieve the value of the control at the time + this event was generated.""" + return self.__value + + +#---------------------------------------------------------------------------- +class NumCtrlAccessorsMixin: + # Define masked.NumCtrl's list of attributes having their own + # Get/Set functions, ignoring those that make no sense for + # an numeric control. + exposed_basectrl_params = ( + 'decimalChar', + 'shiftDecimalChar', + 'groupChar', + 'useParensForNegatives', + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'autoSize', + 'signedForegroundColour', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'emptyInvalid', + 'validFunc', + 'validRequired', + ) + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + + +#---------------------------------------------------------------------------- + +class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): + + + valid_ctrl_params = { + 'integerWidth': 10, # by default allow all 32-bit integers + 'fractionWidth': 0, # by default, use integers + 'decimalChar': '.', # by default, use '.' for decimal point + 'allowNegative': True, # by default, allow negative numbers + 'useParensForNegatives': False, # by default, use '-' to indicate negatives + 'groupDigits': True, # by default, don't insert grouping + 'groupChar': ',', # by default, use ',' for grouping + 'min': None, # by default, no bounds set + 'max': None, + 'limited': False, # by default, no limiting even if bounds set + 'allowNone': False, # by default, don't allow empty value + 'selectOnEntry': True, # by default, select the value of each field on entry + 'foregroundColour': "Black", + 'signedForegroundColour': "Red", + 'emptyBackgroundColour': "White", + 'validBackgroundColour': "White", + 'invalidBackgroundColour': "Yellow", + 'useFixedWidthFont': True, # by default, use a fixed-width font + 'autoSize': True, # by default, set the width of the control based on the mask + } + + + def __init__ ( + self, parent, id=-1, value = 0, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator, + name = "masked.num", + **kwargs ): + +## dbg('masked.NumCtrl::__init__', indent=1) + + # Set defaults for control: +## dbg('setting defaults:') + for key, param_value in NumCtrl.valid_ctrl_params.items(): + # This is done this way to make setattr behave consistently with + # "private attribute" name mangling + setattr(self, '_' + key, copy.copy(param_value)) + + # Assign defaults for all attributes: + init_args = copy.deepcopy(NumCtrl.valid_ctrl_params) +## dbg('kwargs:', kwargs) + for key, param_value in kwargs.items(): + key = key.replace('Color', 'Colour') + if key not in NumCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + else: + init_args[key] = param_value +## dbg('init_args:', indent=1) + for key, param_value in init_args.items(): +## dbg('%s:' % key, param_value) + pass +## dbg(indent=0) + + # Process initial fields for the control, as part of construction: + if type(init_args['integerWidth']) != types.IntType: + raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth'])) + elif init_args['integerWidth'] < 1: + raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth'])) + + fields = {} + + if init_args.has_key('fractionWidth'): + if type(init_args['fractionWidth']) != types.IntType: + raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth)) + elif init_args['fractionWidth'] < 0: + raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth'])) + self._fractionWidth = init_args['fractionWidth'] + + if self._fractionWidth: + fracmask = '.' + '#{%d}' % self._fractionWidth +## dbg('fracmask:', fracmask) + fields[1] = Field(defaultValue='0'*self._fractionWidth) + else: + fracmask = '' + + self._integerWidth = init_args['integerWidth'] + if init_args['groupDigits']: + self._groupSpace = (self._integerWidth - 1) / 3 + else: + self._groupSpace = 0 + intmask = '#{%d}' % (self._integerWidth + self._groupSpace) + if self._fractionWidth: + emptyInvalid = False + else: + emptyInvalid = True + fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) +## dbg('intmask:', intmask) + + # don't bother to reprocess these arguments: + del init_args['integerWidth'] + del init_args['fractionWidth'] + + self._autoSize = init_args['autoSize'] + if self._autoSize: + formatcodes = 'FR<' + else: + formatcodes = 'R<' + + + mask = intmask+fracmask + + # initial value of state vars + self._oldvalue = 0 + self._integerEnd = 0 + self._typedSign = False + + # Construct the base control: + BaseMaskedTextCtrl.__init__( + self, parent, id, '', + pos, size, style, validator, name, + mask = mask, + formatcodes = formatcodes, + fields = fields, + validFunc=self.IsInBounds, + setupEventHandling = False) + + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress + self.Bind(wx.EVT_TEXT, self.OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + # Establish any additional parameters, with appropriate error checking + self.SetParameters(**init_args) + + # Set the value requested (if possible) +## wxCallAfter(self.SetValue, value) + self.SetValue(value) + + # Ensure proper coloring: + self.Refresh() +## dbg('finished NumCtrl::__init__', indent=0) + + + def SetParameters(self, **kwargs): + """ + This routine is used to initialize and reconfigure the control: + """ +## dbg('NumCtrl::SetParameters', indent=1) + maskededit_kwargs = {} + reset_fraction_width = False + + + if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth) + or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth) + or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits) + or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ): + + fields = {} + + if kwargs.has_key('fractionWidth'): + if type(kwargs['fractionWidth']) != types.IntType: + raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth'])) + elif kwargs['fractionWidth'] < 0: + raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth'])) + else: + if self._fractionWidth != kwargs['fractionWidth']: + self._fractionWidth = kwargs['fractionWidth'] + + if self._fractionWidth: + fracmask = '.' + '#{%d}' % self._fractionWidth + fields[1] = Field(defaultValue='0'*self._fractionWidth) + emptyInvalid = False + else: + emptyInvalid = True + fracmask = '' +## dbg('fracmask:', fracmask) + + if kwargs.has_key('integerWidth'): + if type(kwargs['integerWidth']) != types.IntType: +## dbg(indent=0) + raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth'])) + elif kwargs['integerWidth'] < 0: +## dbg(indent=0) + raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth'])) + else: + self._integerWidth = kwargs['integerWidth'] + + if kwargs.has_key('groupDigits'): + self._groupDigits = kwargs['groupDigits'] + + if self._groupDigits: + self._groupSpace = (self._integerWidth - 1) / 3 + else: + self._groupSpace = 0 + + intmask = '#{%d}' % (self._integerWidth + self._groupSpace) +## dbg('intmask:', intmask) + fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) + maskededit_kwargs['fields'] = fields + + # don't bother to reprocess these arguments: + if kwargs.has_key('integerWidth'): + del kwargs['integerWidth'] + if kwargs.has_key('fractionWidth'): + del kwargs['fractionWidth'] + + maskededit_kwargs['mask'] = intmask+fracmask + + if kwargs.has_key('groupChar'): + old_groupchar = self._groupChar # save so we can reformat properly +## dbg("old_groupchar: '%s'" % old_groupchar) + maskededit_kwargs['groupChar'] = kwargs['groupChar'] + if kwargs.has_key('decimalChar'): + old_decimalchar = self._decimalChar +## dbg("old_decimalchar: '%s'" % old_decimalchar) + maskededit_kwargs['decimalChar'] = kwargs['decimalChar'] + + # for all other parameters, assign keyword args as appropriate: + for key, param_value in kwargs.items(): + key = key.replace('Color', 'Colour') + if key not in NumCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + elif key not in MaskedEditMixin.valid_ctrl_params.keys(): + setattr(self, '_' + key, param_value) + elif key in ('mask', 'autoformat'): # disallow explicit setting of mask + raise AttributeError('invalid keyword argument "%s"' % key) + else: + maskededit_kwargs[key] = param_value +## dbg('kwargs:', kwargs) + + # reprocess existing format codes to ensure proper resulting format: + formatcodes = self.GetCtrlParameter('formatcodes') + if kwargs.has_key('allowNegative'): + if kwargs['allowNegative'] and '-' not in formatcodes: + formatcodes += '-' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['allowNegative'] and '-' in formatcodes: + formatcodes = formatcodes.replace('-','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('groupDigits'): + if kwargs['groupDigits'] and ',' not in formatcodes: + formatcodes += ',' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['groupDigits'] and ',' in formatcodes: + formatcodes = formatcodes.replace(',','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('selectOnEntry'): + self._selectOnEntry = kwargs['selectOnEntry'] +## dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes) + if kwargs['selectOnEntry'] and 'S' not in formatcodes: + formatcodes += 'S' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['selectOnEntry'] and 'S' in formatcodes: + formatcodes = formatcodes.replace('S','') + maskededit_kwargs['formatcodes'] = formatcodes + + if kwargs.has_key('autoSize'): + self._autoSize = kwargs['autoSize'] + if kwargs['autoSize'] and 'F' not in formatcodes: + formatcodes += 'F' + maskededit_kwargs['formatcodes'] = formatcodes + elif not kwargs['autoSize'] and 'F' in formatcodes: + formatcodes = formatcodes.replace('F', '') + maskededit_kwargs['formatcodes'] = formatcodes + + + if 'r' in formatcodes and self._fractionWidth: + # top-level mask should only be right insert if no fractional + # part will be shown; ie. if reconfiguring control, remove + # previous "global" setting. + formatcodes = formatcodes.replace('r', '') + maskededit_kwargs['formatcodes'] = formatcodes + + + if kwargs.has_key('limited'): + if kwargs['limited'] and not self._limited: + maskededit_kwargs['validRequired'] = True + elif not kwargs['limited'] and self._limited: + maskededit_kwargs['validRequired'] = False + self._limited = kwargs['limited'] + +## dbg('maskededit_kwargs:', maskededit_kwargs) + if maskededit_kwargs.keys(): + self.SetCtrlParameters(**maskededit_kwargs) + + # Record end of integer and place cursor there: + integerEnd = self._fields[0]._extent[1] + self.SetInsertionPoint(0) + self.SetInsertionPoint(integerEnd) + self.SetSelection(integerEnd, integerEnd) + + # Go ensure all the format codes necessary are present: + orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes') + if 'r' not in intformat: + intformat += 'r' + if '>' not in intformat: + intformat += '>' + if intformat != orig_intformat: + if self._fractionWidth: + self.SetFieldParameters(0, formatcodes=intformat) + else: + self.SetCtrlParameters(formatcodes=intformat) + + # Set min and max as appropriate: + if kwargs.has_key('min'): + min = kwargs['min'] + if( self._max is None + or min is None + or (self._max is not None and self._max >= min) ): +## dbg('examining min') + if min is not None: + try: + textmin = self._toGUI(min, apply_limits = False) + except ValueError: +## dbg('min will not fit into control; ignoring', indent=0) + raise +## dbg('accepted min') + self._min = min + else: +## dbg('ignoring min') + pass + + + if kwargs.has_key('max'): + max = kwargs['max'] + if( self._min is None + or max is None + or (self._min is not None and self._min <= max) ): +## dbg('examining max') + if max is not None: + try: + textmax = self._toGUI(max, apply_limits = False) + except ValueError: +## dbg('max will not fit into control; ignoring', indent=0) + raise +## dbg('accepted max') + self._max = max + else: +## dbg('ignoring max') + pass + + if kwargs.has_key('allowNegative'): + self._allowNegative = kwargs['allowNegative'] + + # Ensure current value of control obeys any new restrictions imposed: + text = self._GetValue() +## dbg('text value: "%s"' % text) + if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1: + text = text.replace(old_groupchar, self._groupChar) + if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1: + text = text.replace(old_decimalchar, self._decimalChar) + if text != self._GetValue(): + wx.TextCtrl.SetValue(self, text) + + value = self.GetValue() + +## dbg('self._allowNegative?', self._allowNegative) + if not self._allowNegative and self._isNeg: + value = abs(value) +## dbg('abs(value):', value) + self._isNeg = False + + elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '': + if self._min > 0: + value = self._min + else: + value = 0 + + sel_start, sel_to = self.GetSelection() + if self.IsLimited() and self._min is not None and value < self._min: +## dbg('Set to min value:', self._min) + self._SetValue(self._toGUI(self._min)) + + elif self.IsLimited() and self._max is not None and value > self._max: +## dbg('Setting to max value:', self._max) + self._SetValue(self._toGUI(self._max)) + else: + # reformat current value as appropriate to possibly new conditions +## dbg('Reformatting value:', value) + sel_start, sel_to = self.GetSelection() + self._SetValue(self._toGUI(value)) + self.Refresh() # recolor as appropriate +## dbg('finished NumCtrl::SetParameters', indent=0) + + + + def _GetNumValue(self, value): + """ + This function attempts to "clean up" a text value, providing a regularized + convertable string, via atol() or atof(), for any well-formed numeric text value. + """ + return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip() + + + def GetFraction(self, candidate=None): + """ + Returns the fractional portion of the value as a float. If there is no + fractional portion, the value returned will be 0.0. + """ + if not self._fractionWidth: + return 0.0 + else: + fracstart, fracend = self._fields[1]._extent + if candidate is None: + value = self._toGUI(BaseMaskedTextCtrl.GetValue(self)) + else: + value = self._toGUI(candidate) + fracstring = value[fracstart:fracend].strip() + if not value: + return 0.0 + else: + return string.atof(fracstring) + + def _OnChangeSign(self, event): +## dbg('NumCtrl::_OnChangeSign', indent=1) + self._typedSign = True + MaskedEditMixin._OnChangeSign(self, event) +## dbg(indent=0) + + + def _disallowValue(self): +## dbg('NumCtrl::_disallowValue') + # limited and -1 is out of bounds + if self._typedSign: + self._isNeg = False + if not wx.Validator_IsSilent(): + wx.Bell() + sel_start, sel_to = self._GetSelection() +## dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position + wx.CallAfter(self.SetSelection, sel_start, sel_to) + + def _SetValue(self, value): + """ + This routine supersedes the base masked control _SetValue(). It is + needed to ensure that the value of the control is always representable/convertable + to a numeric return value (via GetValue().) This routine also handles + automatic adjustment and grouping of the value without explicit intervention + by the user. + """ + +## dbg('NumCtrl::_SetValue("%s")' % value, indent=1) + + if( (self._fractionWidth and value.find(self._decimalChar) == -1) or + (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) : + value = self._toGUI(value) + + numvalue = self._GetNumValue(value) +## dbg('cleansed value: "%s"' % numvalue) + replacement = None + + if numvalue == "": + if self._allowNone: +## dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value) + BaseMaskedTextCtrl._SetValue(self, value) + self.Refresh() + return + elif self._min > 0 and self.IsLimited(): + replacement = self._min + else: + replacement = 0 +## dbg('empty value; setting replacement:', replacement) + + if replacement is None: + # Go get the integer portion about to be set and verify its validity + intstart, intend = self._fields[0]._extent +## dbg('intstart, intend:', intstart, intend) +## dbg('raw integer:"%s"' % value[intstart:intend]) + int = self._GetNumValue(value[intstart:intend]) + numval = self._fromGUI(value) + +## dbg('integer: "%s"' % int) + try: + fracval = self.GetFraction(value) + except ValueError, e: +## dbg('Exception:', e, 'must be out of bounds; disallow value') + self._disallowValue() +## dbg(indent=0) + return + + if fracval == 0.0: +## dbg('self._isNeg?', self._isNeg) + if int == '-' and self._oldvalue < 0 and not self._typedSign: +## dbg('just a negative sign; old value < 0; setting replacement of 0') + replacement = 0 + self._isNeg = False + elif int[:2] == '-0' and self._fractionWidth == 0: + if self._oldvalue < 0: +## dbg('-0; setting replacement of 0') + replacement = 0 + self._isNeg = False + elif not self._limited or (self._min < -1 and self._max >= -1): +## dbg('-0; setting replacement of -1') + replacement = -1 + self._isNeg = True + else: + # limited and -1 is out of bounds + self._disallowValue() +## dbg(indent=0) + return + + elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0: + if not self._limited or (self._min < -1 and self._max >= -1): +## dbg('just a negative sign; setting replacement of -1') + replacement = -1 + else: + # limited and -1 is out of bounds + self._disallowValue() +## dbg(indent=0) + return + + elif( self._typedSign + and int.find('-') != -1 + and self._limited + and not self._min <= numval <= self._max): + # changed sign resulting in value that's now out-of-bounds; + # disallow + self._disallowValue() +## dbg(indent=0) + return + + if replacement is None: + if int and int != '-': + try: + string.atol(int) + except ValueError: + # integer requested is not legal. This can happen if the user + # is attempting to insert a digit in the middle of the control + # resulting in something like " 3 45". Disallow such actions: +## dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) + if not wx.Validator_IsSilent(): + wx.Bell() + sel_start, sel_to = self._GetSelection() +## dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position + wx.CallAfter(self.SetSelection, sel_start, sel_to) +## dbg(indent=0) + return + + if int[0] == '0' and len(int) > 1: +## dbg('numvalue: "%s"' % numvalue.replace(' ', '')) + if self._fractionWidth: + value = self._toGUI(string.atof(numvalue)) + else: + value = self._toGUI(string.atol(numvalue)) +## dbg('modified value: "%s"' % value) + + self._typedSign = False # reset state var + + if replacement is not None: + # Value presented wasn't a legal number, but control should do something + # reasonable instead: +## dbg('setting replacement value:', replacement) + self._SetValue(self._toGUI(replacement)) + sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement))) # find where it put the 1, so we can select it + sel_to = sel_start + len(str(abs(replacement))) +## dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) + wx.CallAfter(self.SetInsertionPoint, sel_start) + wx.CallAfter(self.SetSelection, sel_start, sel_to) +## dbg(indent=0) + return + + # Otherwise, apply appropriate formatting to value: + + # Because we're intercepting the value and adjusting it + # before a sign change is detected, we need to do this here: + if '-' in value or '(' in value: + self._isNeg = True + else: + self._isNeg = False + +## dbg('value:"%s"' % value, 'self._useParens:', self._useParens) + if self._fractionWidth: + adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) + else: + adjvalue = self._adjustInt(self._GetNumValue(value)) +## dbg('adjusted value: "%s"' % adjvalue) + + + sel_start, sel_to = self._GetSelection() # record current insertion point +## dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) + BaseMaskedTextCtrl._SetValue(self, adjvalue) + # After all actions so far scheduled, check that resulting cursor + # position is appropriate, and move if not: + wx.CallAfter(self._CheckInsertionPoint) + +## dbg('finished NumCtrl::_SetValue', indent=0) + + def _CheckInsertionPoint(self): + # If current insertion point is before the end of the integer and + # its before the 1st digit, place it just after the sign position: +## dbg('NumCtrl::CheckInsertionPoint', indent=1) + sel_start, sel_to = self._GetSelection() + text = self._GetValue() + if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): + text, signpos, right_signpos = self._getSignedValue() +## dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) + self.SetInsertionPoint(signpos+1) + self.SetSelection(signpos+1, signpos+1) +## dbg(indent=0) + + + def _OnErase( self, event ): + """ + This overrides the base control _OnErase, so that erasing around + grouping characters auto selects the digit before or after the + grouping character, so that the erasure does the right thing. + """ +## dbg('NumCtrl::_OnErase', indent=1) + + #if grouping digits, make sure deletes next to group char always + # delete next digit to appropriate side: + if self._groupDigits: + key = event.GetKeyCode() + value = BaseMaskedTextCtrl.GetValue(self) + sel_start, sel_to = self._GetSelection() + + if key == wx.WXK_BACK: + # if 1st selected char is group char, select to previous digit + if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: + self.SetInsertionPoint(sel_start-1) + self.SetSelection(sel_start-1, sel_to) + + # elif previous char is group char, select to previous digit + elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: + self.SetInsertionPoint(sel_start-2) + self.SetSelection(sel_start-2, sel_to) + + elif key == wx.WXK_DELETE: + if( sel_to < len(self._mask) - 2 + (1 *self._useParens) + and sel_start == sel_to + and value[sel_to] == self._groupChar ): + self.SetInsertionPoint(sel_start) + self.SetSelection(sel_start, sel_to+2) + + elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) + and value[sel_start:sel_to] == self._groupChar ): + self.SetInsertionPoint(sel_start) + self.SetSelection(sel_start, sel_to+1) + + BaseMaskedTextCtrl._OnErase(self, event) +## dbg(indent=0) + + + def OnTextChange( self, event ): + """ + Handles an event indicating that the text control's value + has changed, and issue EVT_NUM event. + NOTE: using wxTextCtrl.SetValue() to change the control's + contents from within a EVT_CHAR handler can cause double + text events. So we check for actual changes to the text + before passing the events on. + """ +## dbg('NumCtrl::OnTextChange', indent=1) + if not BaseMaskedTextCtrl._OnTextChange(self, event): +## dbg(indent=0) + return + + # else... legal value + + value = self.GetValue() + if value != self._oldvalue: + try: + self.GetEventHandler().ProcessEvent( + NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) + except ValueError: +## dbg(indent=0) + return + # let normal processing of the text continue + event.Skip() + self._oldvalue = value # record for next event +## dbg(indent=0) + + def _GetValue(self): + """ + Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the + control with this function. + """ + return wx.TextCtrl.GetValue(self) + + + def GetValue(self): + """ + Returns the current numeric value of the control. + """ + return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) + + def SetValue(self, value): + """ + Sets the value of the control to the value specified. + The resulting actual value of the control may be altered to + conform with the bounds set on the control if limited, + or colored if not limited but the value is out-of-bounds. + A ValueError exception will be raised if an invalid value + is specified. + """ + BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) + + + def SetIntegerWidth(self, value): + self.SetParameters(integerWidth=value) + def GetIntegerWidth(self): + return self._integerWidth + + def SetFractionWidth(self, value): + self.SetParameters(fractionWidth=value) + def GetFractionWidth(self): + return self._fractionWidth + + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns False and the minimum will not change from + its current setting. On success, the function returns True. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + + If min > the max value allowed by the width of the control, + the function will return False, and the min will not be set. + """ +## dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1) + if( self._max is None + or min is None + or (self._max is not None and self._max >= min) ): + try: + self.SetParameters(min=min) + bRet = True + except ValueError: + bRet = False + else: + bRet = False +## dbg(indent=0) + return bRet + + def GetMin(self): + """ + Gets the lower bound value of the control. It will return + None if not specified. + """ + return self._min + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + + If max > the max value allowed by the width of the control, + the function will return False, and the max will not be set. + """ + if( self._min is None + or max is None + or (self._min is not None and self._min <= max) ): + try: + self.SetParameters(max=max) + bRet = True + except ValueError: + bRet = False + else: + bRet = False + + return bRet + + + def GetMax(self): + """ + Gets the maximum value of the control. It will return the current + maximum integer, or None if not specified. + """ + return self._max + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + NOTE: leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self._min, self._max) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of False, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ + self.SetParameters(limited = limited) + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within the current bounds. + """ + return self._limited + + def GetLimited(self): + """ (For regularization of property accessors) """ + return self.IsLimited + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ +## dbg('IsInBounds(%s)' % repr(value), indent=1) + if value is None: + value = self.GetValue() + else: + try: + value = self._GetNumValue(self._toGUI(value)) + except ValueError, e: +## dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) + return False + if value.strip() == '': + value = None + elif self._fractionWidth: + value = float(value) + else: + value = long(value) + + min = self.GetMin() + max = self.GetMax() + if min is None: min = value + if max is None: max = value + + # if bounds set, and value is None, return False + if value == None and (min is not None or max is not None): +## dbg('finished IsInBounds', indent=0) + return 0 + else: +## dbg('finished IsInBounds', indent=0) + return min <= value <= max + + + def SetAllowNone(self, allow_none): + """ + Change the behavior of the validation code, allowing control + to have a value of None or not, as appropriate. If the value + of the control is currently None, and allow_none is False, the + value of the control will be set to the minimum value of the + control, or 0 if no lower bound is set. + """ + self._allowNone = allow_none + if not allow_none and self.GetValue() is None: + min = self.GetMin() + if min is not None: self.SetValue(min) + else: self.SetValue(0) + + + def IsNoneAllowed(self): + return self._allowNone + def GetAllowNone(self): + """ (For regularization of property accessors) """ + return self.IsNoneAllowed() + + def SetAllowNegative(self, value): + self.SetParameters(allowNegative=value) + def IsNegativeAllowed(self): + return self._allowNegative + def GetAllowNegative(self): + """ (For regularization of property accessors) """ + return self.IsNegativeAllowed() + + def SetGroupDigits(self, value): + self.SetParameters(groupDigits=value) + def IsGroupingAllowed(self): + return self._groupDigits + def GetGroupDigits(self): + """ (For regularization of property accessors) """ + return self.IsGroupingAllowed() + + def SetGroupChar(self, value): + self.SetParameters(groupChar=value) + def GetGroupChar(self): + return self._groupChar + + def SetDecimalChar(self, value): + self.SetParameters(decimalChar=value) + def GetDecimalChar(self): + return self._decimalChar + + def SetSelectOnEntry(self, value): + self.SetParameters(selectOnEntry=value) + def GetSelectOnEntry(self): + return self._selectOnEntry + + def SetAutoSize(self, value): + self.SetParameters(autoSize=value) + def GetAutoSize(self): + return self._autoSize + + + # (Other parameter accessors are inherited from base class) + + + def _toGUI( self, value, apply_limits = True ): + """ + Conversion function used to set the value of the control; does + type and bounds checking and raises ValueError if argument is + not a valid value. + """ +## dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1) + if value is None and self.IsNoneAllowed(): +## dbg(indent=0) + return self._template + + elif type(value) in (types.StringType, types.UnicodeType): + value = self._GetNumValue(value) +## dbg('cleansed num value: "%s"' % value) + if value == "": + if self.IsNoneAllowed(): +## dbg(indent=0) + return self._template + else: +## dbg('exception raised:', e, indent=0) + raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) + # else... + try: + if self._fractionWidth or value.find('.') != -1: + value = float(value) + else: + value = long(value) + except Exception, e: +## dbg('exception raised:', e, indent=0) + raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) + + elif type(value) not in (types.IntType, types.LongType, types.FloatType): +## dbg(indent=0) + raise ValueError ( + 'NumCtrl requires numeric value, passed %s'% repr(value) ) + + if not self._allowNegative and value < 0: + raise ValueError ( + 'control configured to disallow negative values, passed %s'% repr(value) ) + + if self.IsLimited() and apply_limits: + min = self.GetMin() + max = self.GetMax() + if not min is None and value < min: +## dbg(indent=0) + raise ValueError ( + 'value %d is below minimum value of control'% value ) + if not max is None and value > max: +## dbg(indent=0) + raise ValueError ( + 'value %d exceeds value of control'% value ) + + adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) +## dbg('len(%s):' % self._mask, len(self._mask)) +## dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) +## dbg('adjustwidth:', adjustwidth) + if self._fractionWidth == 0: + s = str(long(value)).rjust(self._integerWidth) + else: + format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) + s = format % float(value) +## dbg('s:"%s"' % s, 'len(s):', len(s)) + if len(s) > (adjustwidth - self._groupSpace): +## dbg(indent=0) + raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) + elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): +## dbg(indent=0) + raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) + + s = s.rjust(adjustwidth).replace('.', self._decimalChar) + if self._signOk and self._useParens: + if s.find('-') != -1: + s = s.replace('-', '(') + ')' + else: + s += ' ' +## dbg('returned: "%s"' % s, indent=0) + return s + + + def _fromGUI( self, value ): + """ + Conversion function used in getting the value of the control. + """ +## dbg(suspend=0) +## dbg('NumCtrl::_fromGUI(%s)' % value, indent=1) + # One or more of the underlying text control implementations + # issue an intermediate EVT_TEXT when replacing the control's + # value, where the intermediate value is an empty string. + # So, to ensure consistency and to prevent spurious ValueErrors, + # we make the following test, and react accordingly: + # + if value.strip() == '': + if not self.IsNoneAllowed(): +## dbg('empty value; not allowed,returning 0', indent = 0) + if self._fractionWidth: + return 0.0 + else: + return 0 + else: +## dbg('empty value; returning None', indent = 0) + return None + else: + value = self._GetNumValue(value) +## dbg('Num value: "%s"' % value) + if self._fractionWidth: + try: +## dbg(indent=0) + return float( value ) + except ValueError: +## dbg("couldn't convert to float; returning None") + return None + else: + raise + else: + try: +## dbg(indent=0) + return int( value ) + except ValueError: + try: +## dbg(indent=0) + return long( value ) + except ValueError: +## dbg("couldn't convert to long; returning None") + return None + + else: + raise + else: +## dbg('exception occurred; returning None') + return None + + + def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): + """ + Preprocessor for base control paste; if value needs to be right-justified + to fit in control, do so prior to paste: + """ +## dbg('NumCtrl::_Paste (value = "%s")' % value) + if value is None: + paste_text = self._getClipboardContents() + else: + paste_text = value + + # treat paste as "replace number", if appropriate: + sel_start, sel_to = self._GetSelection() + if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent: + paste_text = self._toGUI(paste_text) + self._SetSelection(0, len(self._mask)) + + return MaskedEditMixin._Paste(self, + paste_text, + raise_on_invalid=raise_on_invalid, + just_return_value=just_return_value) + + + +#=========================================================================== + +if __name__ == '__main__': + + import traceback + + class myDialog(wx.Dialog): + def __init__(self, parent, id, title, + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.DEFAULT_DIALOG_STYLE ): + wx.Dialog.__init__(self, parent, id, title, pos, size, style) + + self.int_ctrl = NumCtrl(self, wx.NewId(), size=(55,20)) + self.OK = wx.Button( self, wx.ID_OK, "OK") + self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel") + + vs = wx.BoxSizer( wx.VERTICAL ) + vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs = wx.BoxSizer( wx.HORIZONTAL ) + hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( vs ) + vs.Fit( self ) + vs.SetSizeHints( self ) + self.Bind(EVT_NUM, self.OnChange, self.int_ctrl) + + def OnChange(self, event): + print 'value now', event.GetValue() + + class TestApp(wx.App): + def OnInit(self): + try: + self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100) ) + self.panel = wx.Panel(self.frame, -1) + button = wx.Button(self.panel, -1, "Push Me", (20, 20)) + self.Bind(wx.EVT_BUTTON, self.OnClick, button) + except: + traceback.print_exc() + return False + return True + + def OnClick(self, event): + dlg = myDialog(self.panel, -1, "test NumCtrl") + dlg.int_ctrl.SetValue(501) + dlg.int_ctrl.SetInsertionPoint(1) + dlg.int_ctrl.SetSelection(1,2) + rc = dlg.ShowModal() + print 'final value', dlg.int_ctrl.GetValue() + del dlg + self.frame.Destroy() + + def Show(self): + self.frame.Show(True) + + try: + app = TestApp(0) + app.Show() + app.MainLoop() + except: + traceback.print_exc() + +i=0 +## To-Do's: +## =============================## +## 1. Add support for printf-style format specification. +## 2. Add option for repositioning on 'illegal' insertion point. +## +## Version 1.1 +## 1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. +## 2. Added autoSize parameter, to allow manual sizing of the control. +## 3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of +## nonsensical parameter methods from the control, so it will work +## properly with Boa. +## 4. Fixed allowNone bug found by user sameerc1@grandecom.net + diff --git a/wxPython/wx/lib/masked/textctrl.py b/wxPython/wx/lib/masked/textctrl.py new file mode 100644 index 0000000000..d73fbe4a4e --- /dev/null +++ b/wxPython/wx/lib/masked/textctrl.py @@ -0,0 +1,325 @@ +#---------------------------------------------------------------------------- +# Name: masked.textctrl.py +# Authors: Jeff Childers, Will Sadkin +# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com +# Created: 02/11/2003 +# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 +# Portions: (c) 2002 by Will Sadkin, 2002-2003 +# RCS-ID: $Id$ +# License: wxWidgets license +#---------------------------------------------------------------------------- +# +# This file contains the most typically used generic masked control, +# masked.TextCtrl. It also defines the BaseMaskedTextCtrl, which can +# be used to derive other "semantics-specific" classes, like masked.NumCtrl, +# masked.TimeCtrl, and masked.IpAddrCtrl. +# +#---------------------------------------------------------------------------- +import wx +from wx.lib.masked import * + +# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would +# be a good place to implement the 2.3 logger class +from wx.tools.dbg import Logger +dbg = Logger() +##dbg(enable=0) + +# ## TRICKY BIT: to avoid a ton of boiler-plate, and to +# ## automate the getter/setter generation for each valid +# ## control parameter so we never forget to add the +# ## functions when adding parameters, this loop +# ## programmatically adds them to the class: +# ## (This makes it easier for Designers like Boa to +# ## deal with masked controls.) +# +# ## To further complicate matters, this is done with an +# ## extra level of inheritance, so that "general" classes like +# ## MaskedTextCtrl can have all possible attributes, +# ## while derived classes, like TimeCtrl and MaskedNumCtrl +# ## can prevent exposure of those optional attributes of their base +# ## class that do not make sense for their derivation. Therefore, +# ## we define +# ## BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) +# ## and +# ## MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). +# ## +# ## This allows us to then derive: +# ## MaskedNumCtrl( BaseMaskedTextCtrl ) +# ## +# ## and not have to expose all the same accessor functions for the +# ## derived control when they don't all make sense for it. +# ## + +class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): + """ + This is the primary derivation from MaskedEditMixin. It provides + a general masked text control that can be configured with different + masks. It's actually a "base masked textCtrl", so that the + MaskedTextCtrl class can be derived from it, and add those + accessor functions to it that are appropriate to the general class, + whilst other classes can derive from BaseMaskedTextCtrl, and + only define those accessor functions that are appropriate for + those derivations. + """ + + def __init__( self, parent, id=-1, value = '', + pos = wx.DefaultPosition, + size = wx.DefaultSize, + style = wx.TE_PROCESS_TAB, + validator=wx.DefaultValidator, ## placeholder provided for data-transfer logic + name = 'maskedTextCtrl', + setupEventHandling = True, ## setup event handling by default + **kwargs): + + wx.TextCtrl.__init__(self, parent, id, value='', + pos=pos, size = size, + style=style, validator=validator, + name=name) + + self.controlInitialized = True + MaskedEditMixin.__init__( self, name, **kwargs ) + + self._SetInitialValue(value) + + if setupEventHandling: + ## Setup event handlers + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick + self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress + self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep + ## track of previous value for undo + + + def __repr__(self): + return "" % self.GetValue() + + + def _GetSelection(self): + """ + Allow mixin to get the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetSelection() + + def _SetSelection(self, sel_start, sel_to): + """ + Allow mixin to set the text selection of this control. + REQUIRED by any class derived from MaskedEditMixin. + """ +#### dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) + return self.SetSelection( sel_start, sel_to ) + + def SetSelection(self, sel_start, sel_to): + """ + This is just for debugging... + """ +## dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) + wx.TextCtrl.SetSelection(self, sel_start, sel_to) + + + def _GetInsertionPoint(self): + return self.GetInsertionPoint() + + def _SetInsertionPoint(self, pos): +#### dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals()) + self.SetInsertionPoint(pos) + + def SetInsertionPoint(self, pos): + """ + This is just for debugging... + """ +## dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals()) + wx.TextCtrl.SetInsertionPoint(self, pos) + + + def _GetValue(self): + """ + Allow mixin to get the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return self.GetValue() + + def _SetValue(self, value): + """ + Allow mixin to set the raw value of the control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1) + # Record current selection and insertion point, for undo + self._prevSelection = self._GetSelection() + self._prevInsertionPoint = self._GetInsertionPoint() + wx.TextCtrl.SetValue(self, value) +## dbg(indent=0) + + def SetValue(self, value): + """ + This function redefines the externally accessible .SetValue to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ +## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1) + + if not self._mask: + wx.TextCtrl.SetValue(self, value) # revert to base control behavior + return + + # empty previous contents, replacing entire value: + self._SetInsertionPoint(0) + self._SetSelection(0, self._masklength) + if self._signOk and self._useParens: + signpos = value.find('-') + if signpos != -1: + value = value[:signpos] + '(' + value[signpos+1:].strip() + ')' + elif value.find(')') == -1 and len(value) < self._masklength: + value += ' ' # add place holder for reserved space for right paren + + if( len(value) < self._masklength # value shorter than control + and (self._isFloat or self._isInt) # and it's a numeric control + and self._ctrl_constraints._alignRight ): # and it's a right-aligned control + +## dbg('len(value)', len(value), ' < self._masklength', self._masklength) + # try to intelligently "pad out" the value to the right size: + value = self._template[0:self._masklength - len(value)] + value + if self._isFloat and value.find('.') == -1: + value = value[1:] +## dbg('padded value = "%s"' % value) + + # make SetValue behave the same as if you had typed the value in: + try: + value = self._Paste(value, raise_on_invalid=True, just_return_value=True) + if self._isFloat: + self._isNeg = False # (clear current assumptions) + value = self._adjustFloat(value) + elif self._isInt: + self._isNeg = False # (clear current assumptions) + value = self._adjustInt(value) + elif self._isDate and not self.IsValid(value) and self._4digityear: + value = self._adjustDate(value, fixcentury=True) + except ValueError: + # If date, year might be 2 digits vs. 4; try adjusting it: + if self._isDate and self._4digityear: + dateparts = value.split(' ') + dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) + value = string.join(dateparts, ' ') +## dbg('adjusted value: "%s"' % value) + value = self._Paste(value, raise_on_invalid=True, just_return_value=True) + else: +## dbg('exception thrown', indent=0) + raise + + self._SetValue(value) # note: to preserve similar capability, .SetValue() + # does not change IsModified() +#### dbg('queuing insertion after .SetValue', self._masklength) + wx.CallAfter(self._SetInsertionPoint, self._masklength) + wx.CallAfter(self._SetSelection, self._masklength, self._masklength) +## dbg(indent=0) + + + def Clear(self): + """ Blanks the current control value by replacing it with the default value.""" +## dbg("MaskedTextCtrl::Clear - value reset to default value (template)") + if self._mask: + self.ClearValue() + else: + wx.TextCtrl.Clear(self) # else revert to base control behavior + + + def _Refresh(self): + """ + Allow mixin to refresh the base control with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ +## dbg('MaskedTextCtrl::_Refresh', indent=1) + wx.TextCtrl.Refresh(self) +## dbg(indent=0) + + + def Refresh(self): + """ + This function redefines the externally accessible .Refresh() to + validate the contents of the masked control as it refreshes. + NOTE: this must be done in the class derived from the base wx control. + """ +## dbg('MaskedTextCtrl::Refresh', indent=1) + self._CheckValid() + self._Refresh() +## dbg(indent=0) + + + def _IsEditable(self): + """ + Allow mixin to determine if the base control is editable with this function. + REQUIRED by any class derived from MaskedEditMixin. + """ + return wx.TextCtrl.IsEditable(self) + + + def Cut(self): + """ + This function redefines the externally accessible .Cut to be + a smart "erase" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Cut() # call the mixin's Cut method + else: + wx.TextCtrl.Cut(self) # else revert to base control behavior + + + def Paste(self): + """ + This function redefines the externally accessible .Paste to be + a smart "paste" of the text in question, so as not to corrupt the + masked control. NOTE: this must be done in the class derived + from the base wx control. + """ + if self._mask: + self._Paste() # call the mixin's Paste method + else: + wx.TextCtrl.Paste(self, value) # else revert to base control behavior + + + def Undo(self): + """ + This function defines the undo operation for the control. (The default + undo is 1-deep.) + """ + if self._mask: + self._Undo() + else: + wx.TextCtrl.Undo(self) # else revert to base control behavior + + + def IsModified(self): + """ + This function overrides the raw wxTextCtrl method, because the + masked edit mixin uses SetValue to change the value, which doesn't + modify the state of this attribute. So, we keep track on each + keystroke to see if the value changes, and if so, it's been + modified. + """ + return wx.TextCtrl.IsModified(self) or self.modified + + + def _CalcSize(self, size=None): + """ + Calculate automatic size if allowed; use base mixin function. + """ + return self._calcSize(size) + + +class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): + """ + This extra level of inheritance allows us to add the generic set of + masked edit parameters only to this class while allowing other + classes to derive from the "base" masked text control, and provide + a smaller set of valid accessor functions. + """ + pass + + diff --git a/wxPython/wx/lib/masked/timectrl.py b/wxPython/wx/lib/masked/timectrl.py new file mode 100644 index 0000000000..36fbbee97a --- /dev/null +++ b/wxPython/wx/lib/masked/timectrl.py @@ -0,0 +1,1317 @@ +#---------------------------------------------------------------------------- +# Name: timectrl.py +# Author: Will Sadkin +# Created: 09/19/2002 +# Copyright: (c) 2002 by Will Sadkin, 2002 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# NOTE: +# This was written way it is because of the lack of masked edit controls +# in wxWindows/wxPython. I would also have preferred to derive this +# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl +# component of that control is inaccessible through the interface exposed in +# wxPython. +# +# TimeCtrl does not use validators, because it does careful manipulation +# of the cursor in the text window on each keystroke, and validation is +# cursor-position specific, so the control intercepts the key codes before the +# validator would fire. +# +# TimeCtrl now also supports .SetValue() with either strings or wxDateTime +# values, as well as range limits, with the option of either enforcing them +# or simply coloring the text of the control if the limits are exceeded. +# +# Note: this class now makes heavy use of wxDateTime for parsing and +# regularization, but it always does so with ephemeral instances of +# wxDateTime, as the C++/Python validity of these instances seems to not +# persist. Because "today" can be a day for which an hour can "not exist" +# or be counted twice (1 day each per year, for DST adjustments), the date +# portion of all wxDateTimes used/returned have their date portion set to +# Jan 1, 1970 (the "epoch.") +#---------------------------------------------------------------------------- +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for V2.5 compatability +# o wx.SpinCtl has some issues that cause the control to +# lock up. Noted in other places using it too, it's not this module +# that's at fault. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMaskedTextCtrl -> masked.TextCtrl +# o wxTimeCtrl -> masked.TimeCtrl +# + +""" +

+TimeCtrl 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 TimeCtrl, and the up/down arrows act +like a spin control. TimeCtrl also allows for an actual spin button to be attached +to the control, so that it acts like the up/down arrow keys. +

+The ! or c key sets the value of the control to the current time. +

+Here's the API for TimeCtrl: +

+    TimeCtrl(
+         parent, id = -1,
+         value = '12:00:00 AM',
+         pos = wx.DefaultPosition,
+         size = wx.DefaultSize,
+         style = wxTE_PROCESS_TAB,
+         validator = wx.DefaultValidator,
+         name = "time",
+         format = 'HHMMSS',
+         fmt24hr = False,
+         displaySeconds = True,
+         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 wx.DefaultSize is specified. +
    style +
    By default, TimeCtrl will process TAB events, by allowing tab to the + different cells within the control. +
    validator +
    By default, TimeCtrl just uses the default (empty) validator, as all + of its validation for entry control is handled internally. However, a validator + can be supplied to provide data transfer capability to the control. +
    +
    format +
    This parameter can be used instead of the fmt24hr and displaySeconds + parameters, respectively; it provides a shorthand way to specify the time + format you want. Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and + '24HHMM'. If the format is specified, the other two arguments will be ignored. +
    +
    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. (This value is ignored if the format + parameter is specified.) +
    +
    displaySeconds +
    If True, control will include a seconds field; if False, it will + just show hours and minutes. (This value is ignored if the format + parameter is specified.) +
    +
    spinButton +
    If specified, this button's events will be bound to the behavior of the + TimeCtrl, working like up/down cursor key events. (See BindSpinButton.) +
    +
    min +
    Defines the lower bound for "valid" selections in the control. + By default, TimeCtrl doesn't have bounds. You must set both upper and lower + bounds to make the control pay attention to them, (as only one bound makes no sense + with times.) "Valid" times will fall between the min and max "pie wedge" of the + clock. +
    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(SpinBtton) +
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 copy +import string +import types + +import wx + +from wx.tools.dbg import Logger +from wx.lib.masked import Field, BaseMaskedTextCtrl + +dbg = Logger() +##dbg(enable=0) + +try: + from mx import DateTime + accept_mx = True +except ImportError: + accept_mx = False + +# This class of event fires whenever the value of the time changes in the control: +wxEVT_TIMEVAL_UPDATED = wx.NewEventType() +EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1) + +class TimeUpdatedEvent(wx.PyCommandEvent): + def __init__(self, id, value ='12:00:00 AM'): + wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id) + self.value = value + def GetValue(self): + """Retrieve the value of the time control at the time this event was generated""" + return self.value + +class TimeCtrlAccessorsMixin: + # Define TimeCtrl's list of attributes having their own + # Get/Set functions, ignoring those that make no sense for + # an numeric control. + exposed_basectrl_params = ( + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'validFunc', + 'validRequired', + ) + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + +class TimeCtrl(BaseMaskedTextCtrl): + + valid_ctrl_params = { + 'format' : 'HHMMSS', # default format code + 'displaySeconds' : True, # by default, shows seconds + 'min': None, # by default, no bounds set + 'max': None, + 'limited': False, # by default, no limiting even if bounds set + 'useFixedWidthFont': True, # by default, use a fixed-width font + 'oob_color': "Yellow" # by default, the default masked.TextCtrl "invalid" color + } + + def __init__ ( + self, parent, id=-1, value = '12:00:00 AM', + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr=False, + spinButton = None, + style = wx.TE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = "time", + **kwargs ): + + # set defaults for control: +## dbg('setting defaults:') + for key, param_value in TimeCtrl.valid_ctrl_params.items(): + # This is done this way to make setattr behave consistently with + # "private attribute" name mangling + setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) + + # create locals from current defaults, so we can override if + # specified in kwargs, and handle uniformly: + min = self.__min + max = self.__max + limited = self.__limited + self.__posCurrent = 0 + # handle deprecated keword argument name: + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if not kwargs.has_key('displaySeconds'): + kwargs['displaySeconds'] = True + + # (handle positional arg (from original release) differently from rest of kwargs:) + self.__fmt24hr = False + if not kwargs.has_key('format'): + if fmt24hr: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = '24HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = '24HHMM' + else: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = 'HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = 'HHMM' + + if not kwargs.has_key('useFixedWidthFont'): + # allow control over font selection: + kwargs['useFixedWidthFont'] = self.__useFixedWidthFont + + maskededit_kwargs = self.SetParameters(**kwargs) + + # allow for explicit size specification: + if size != wx.DefaultSize: + # override (and remove) "autofit" autoformat code in standard time formats: + maskededit_kwargs['formatcodes'] = 'T!' + + # This allows range validation if set + maskededit_kwargs['validFunc'] = self.IsInBounds + + # This allows range limits to affect insertion into control or not + # dynamically without affecting individual field constraint validation + maskededit_kwargs['retainFieldValidation'] = True + + # Now we can initialize the base control: + BaseMaskedTextCtrl.__init__( + self, parent, id=id, + pos=pos, size=size, + style = style, + validator = validator, + name = name, + setupEventHandling = False, + **maskededit_kwargs) + + + # This makes ':' act like tab (after we fix each ':' key event to remove "shift") + self._SetKeyHandler(':', self._OnChangeField) + + + # This makes the up/down keys act like spin button controls: + self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) + self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) + + + # This allows ! and c/C to set the control to the current time: + self._SetKeyHandler('!', self.__OnSetToNow) + self._SetKeyHandler('c', self.__OnSetToNow) + self._SetKeyHandler('C', self.__OnSetToNow) + + + # Set up event handling ourselves, so we can insert special + # processing on the ":' key to remove the "shift" attribute + # *before* the default handlers have been installed, so + # that : takes you forward, not back, and so we can issue + # EVT_TIMEUPDATE events on changes: + + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event, + ## then call BaseMaskedTextCtrl._OnChar with + ## the possibly modified event. + self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events + + + # Validate initial value and set if appropriate + try: + self.SetBounds(min, max) + self.SetLimited(limited) + self.SetValue(value) + except: + self.SetValue('12:00:00 AM') + + if spinButton: + self.BindSpinButton(spinButton) # bind spin button up/down events to this control + + + def SetParameters(self, **kwargs): +## dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) + maskededit_kwargs = {} + reset_format = False + + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): + del kwargs['displaySeconds'] # always apply format if specified + + # assign keyword args as appropriate: + for key, param_value in kwargs.items(): + if key not in TimeCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + + if key == 'format': + # handle both local or generic 'maskededit' autoformat codes: + if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = False + elif param_value == 'HHMM' or param_value == 'TIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = False + elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = True + elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = True + else: + raise AttributeError('"%s" is not a valid format' % param_value) + reset_format = True + + elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'): + self.__displaySeconds = param_value + reset_format = True + + elif key == "min": min = param_value + elif key == "max": max = param_value + elif key == "limited": limited = param_value + + elif key == "useFixedWidthFont": + maskededit_kwargs[key] = param_value + + elif key == "oob_color": + maskededit_kwargs['invalidBackgroundColor'] = param_value + + if reset_format: + if self.__fmt24hr: + if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' + else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' + + # Set hour field to zero-pad, right-insert, require explicit field change, + # select entire field on entry, and require a resultant valid entry + # to allow character entry: + hourfield = Field(formatcodes='0r" % self.GetValue() + + + def SetValue(self, value): + """ + Validating SetValue function for time values: + This function will do dynamic type checking on the value argument, + and convert wxDateTime, mxDateTime, or 12/24 format time string + into the appropriate format string for the control. + """ +## dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) + try: + strtime = self._toGUI(self.__validateValue(value)) + except: +## dbg('validation failed', indent=0) + raise + +## dbg('strtime:', strtime) + self._SetValue(strtime) +## dbg(indent=0) + + def GetValue(self, + as_wxDateTime = False, + as_mxDateTime = False, + as_wxTimeSpan = False, + as_mxDateTimeDelta = False): + + + if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: + value = self.GetWxDateTime() + if as_wxDateTime: + pass + elif as_mxDateTime: + value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_wxTimeSpan: + value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_mxDateTimeDelta: + value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) + else: + value = BaseMaskedTextCtrl.GetValue(self) + return value + + + def SetWxDateTime(self, wxdt): + """ + Because SetValue can take a wxDateTime, this is now just an alias. + """ + self.SetValue(wxdt) + + + def GetWxDateTime(self, value=None): + """ + This function is the conversion engine for TimeCtrl; it takes + one of the following types: + time string + wxDateTime + wxTimeSpan + mxDateTime + mxDateTimeDelta + and converts it to a wxDateTime that always has Jan 1, 1970 as its date + portion, so that range comparisons around values can work using + wxDateTime's built-in comparison function. If a value is not + provided to convert, the string value of the control will be used. + If the value is not one of the accepted types, a ValueError will be + raised. + """ + global accept_mx +## dbg(suspend=1) +## dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) + if value is None: +## dbg('getting control value') + value = self.GetValue() +## dbg('value = "%s"' % value) + + if type(value) == types.UnicodeType: + value = str(value) # convert to regular string + + valid = True # assume true + if type(value) == types.StringType: + + # Construct constant wxDateTime, then try to parse the string: + wxdt = wx.DateTimeFromDMY(1, 0, 1970) +## dbg('attempting conversion') + value = value.strip() # (parser doesn't like leading spaces) + checkTime = wxdt.ParseTime(value) + valid = checkTime == len(value) # entire string parsed? +## dbg('checkTime == len(value)?', valid) + + if not valid: +## dbg(indent=0, suspend=0) + raise ValueError('cannot convert string "%s" to valid time' % value) + + else: + if isinstance(value, wx.DateTime): + hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() + elif isinstance(value, wx.TimeSpan): + totalseconds = value.GetSeconds() + hour = totalseconds / 3600 + minute = totalseconds / 60 - (hour * 60) + second = totalseconds - ((hour * 3600) + (minute * 60)) + + elif accept_mx and isinstance(value, DateTime.DateTimeType): + hour, minute, second = value.hour, value.minute, value.second + elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): + hour, minute, second = value.hour, value.minute, value.second + else: + # Not a valid function argument + if accept_mx: + error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) + else: + error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) +## dbg(indent=0, suspend=0) + raise ValueError(error) + + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + wxdt.SetHour(hour) + wxdt.SetMinute(minute) + wxdt.SetSecond(second) + +## dbg('wxdt:', wxdt, indent=0, suspend=0) + return wxdt + + + def SetMxDateTime(self, mxdt): + """ + Because SetValue can take an mxDateTime, (if DateTime is importable), + this is now just an alias. + """ + self.SetValue(value) + + + def GetMxDateTime(self, value=None): + if value is None: + t = self.GetValue(as_mxDateTime=True) + else: + # Convert string 1st to wxDateTime, then use components, since + # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: + wxdt = self.GetWxDateTime(value) + hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() + t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) + return t + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns 0 and the minimum will not change from + its current setting. On success, the function returns 1. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + """ +## dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) + if min is not None: + try: + min = self.GetWxDateTime(min) + self.__min = self._toGUI(min) + except: +## dbg('exception occurred', indent=0) + return False + else: + self.__min = min + + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True +## dbg('ret:', ret, indent=0) + return ret + + + def GetMin(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ +## dbg(suspend=1) +## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__min is None: +## dbg('(min == None)') + ret = self.__min + elif as_string: + ret = self.__min +## dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__min) + except: +## dbg(suspend=0) +## dbg('exception occurred', indent=0) + raise +## dbg('ret:', repr(ret)) +## dbg(indent=0, suspend=0) + return ret + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + """ +## dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) + if max is not None: + try: + max = self.GetWxDateTime(max) + self.__max = self._toGUI(max) + except: +## dbg('exception occurred', indent=0) + return False + else: + self.__max = max +## dbg('max:', repr(self.__max)) + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True +## dbg('ret:', ret, indent=0) + return ret + + + def GetMax(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ +## dbg(suspend=1) +## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__max is None: +## dbg('(max == None)') + ret = self.__max + elif as_string: + ret = self.__max +## dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__max) + except: +## dbg(suspend=0) +## dbg('exception occurred', indent=0) + raise +## dbg('ret:', repr(ret)) +## dbg(indent=0, suspend=0) + return ret + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + NOTE: leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self, as_string = False): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self.GetMin(as_string), self.GetMax(as_string)) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of 0, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ +## dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) + self.__limited = limited + + if not limited: + self.SetMaskParameters(validRequired = False) + self._CheckValid() +## dbg(indent=0) + return + +## dbg('requiring valid value') + self.SetMaskParameters(validRequired = True) + + min = self.GetMin() + max = self.GetMax() + if min is None or max is None: +## dbg('both bounds not set; no further action taken') + return # can't limit without 2 bounds + + elif not self.IsInBounds(): + # set value to the nearest bound: + try: + value = self.GetWxDateTime() + except: +## dbg('exception occurred', indent=0) + raise + + if min <= max: # valid range doesn't span midnight +## dbg('min <= max') + # which makes the "nearest bound" computation trickier... + + # determine how long the "invalid" pie wedge is, and cut + # this interval in half for comparison purposes: + + # Note: relies on min and max and value date portions + # always being the same. + interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max + + half_interval = wx.TimeSpan( + 0, # hours + 0, # minutes + interval.GetSeconds() / 2, # seconds + 0) # msec + + if value < min: # min is on next day, so use value on + # "next day" for "nearest" interval calculation: + cmp_value = value + wx.TimeSpan(24, 0, 0, 0) + else: # "before midnight; ok + cmp_value = value + + if (cmp_value - max) > half_interval: +## dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: +## dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + else: +## dbg('max < min') + # therefore max < value < min guaranteed to be true, + # so "nearest bound" calculation is much easier: + if (value - max) >= (min - value): + # current value closer to min; pick that edge of pie wedge +## dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: +## dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + +## dbg(indent=0) + + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within any current bounds. Note: can + be set even if there are no current bounds. + """ + return self.__limited + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. As the clock + is a "circle", both minimum and maximum bounds must be set for + a value to ever be considered "out of bounds". This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ + if value is not None: + try: + value = self.GetWxDateTime(value) # try to regularize passed value + except ValueError: +## dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) + raise + +## dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) + if self.__min is None or self.__max is None: +## dbg(indent=0) + return True + + elif value is None: + try: + value = self.GetWxDateTime() + except: +## dbg('exception occurred', indent=0) + raise + +## dbg('value:', value.FormatTime()) + + # Get wxDateTime representations of bounds: + min = self.GetMin() + max = self.GetMax() + + midnight = wx.DateTimeFromDMY(1, 0, 1970) + if min <= max: # they don't span midnight + ret = min <= value <= max + + else: + # have to break into 2 tests; to be in bounds + # either "min" <= value (<= midnight of *next day*) + # or midnight <= value <= "max" + ret = min <= value or (midnight <= value <= max) +## dbg('in bounds?', ret, indent=0) + return ret + + + def IsValid( self, value ): + """ + Can be used to determine if a given value would be a legal and + in-bounds value for the control. + """ + try: + self.__validateValue(value) + return True + except ValueError: + return False + + def SetFormat(self, format): + self.SetParameters(format=format) + + def GetFormat(self): + if self.__displaySeconds: + if self.__fmt24hr: return '24HHMMSS' + else: return 'HHMMSS' + else: + if self.__fmt24hr: return '24HHMM' + else: return 'HHMM' + +#------------------------------------------------------------------------------------------------------------- +# these are private functions and overrides: + + + def __OnTextChange(self, event=None): +## dbg('TimeCtrl::OnTextChange', indent=1) + + # Allow Maskedtext base control to color as appropriate, + # and Skip the EVT_TEXT event (if appropriate.) + ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() + ## call is generating two (2) EVT_TEXT events. (!) + ## The the only mechanism I can find to mask this problem is to + ## keep track of last value seen, and declare a valid EVT_TEXT + ## event iff the value has actually changed. The masked edit + ## OnTextChange routine does this, and returns True on a valid event, + ## False otherwise. + if not BaseMaskedTextCtrl._OnTextChange(self, event): + return + +## dbg('firing TimeUpdatedEvent...') + evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) + evt.SetEventObject(self) + self.GetEventHandler().ProcessEvent(evt) +## dbg(indent=0) + + + def SetInsertionPoint(self, pos): + """ + Records the specified position and associated cell before calling base class' function. + This is necessary to handle the optional spin button, because the insertion + point is lost when the focus shifts to the spin button. + """ +## dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) + BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire) + self.__posCurrent = self.GetInsertionPoint() +## dbg(indent=0) + + + def SetSelection(self, sel_start, sel_to): +## dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) + + # Adjust selection range to legal extent if not already + if sel_start < 0: + sel_start = 0 + + if self.__posCurrent != sel_start: # force selection and insertion point to match + self.SetInsertionPoint(sel_start) + cell_start, cell_end = self._FindField(sel_start)._extent + if not cell_start <= sel_to <= cell_end: + sel_to = cell_end + + self.__bSelection = sel_start != sel_to + BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) +## dbg(indent=0) + + + def __OnSpin(self, key): + """ + This is the function that gets called in response to up/down arrow or + bound spin button events. + """ + self.__IncrementValue(key, self.__posCurrent) # changes the value + + # Ensure adjusted control regains focus and has adjusted portion + # selected: + self.SetFocus() + start, end = self._FindField(self.__posCurrent)._extent + self.SetInsertionPoint(start) + self.SetSelection(start, end) +## dbg('current position:', self.__posCurrent) + + + def __OnSpinUp(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_UP; + causes control to behave as if up arrow was pressed. + """ +## dbg('TimeCtrl::OnSpinUp', indent=1) + self.__OnSpin(wx.WXK_UP) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def __OnSpinDown(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_DOWN; + causes control to behave as if down arrow was pressed. + """ +## dbg('TimeCtrl::OnSpinDown', indent=1) + self.__OnSpin(wx.WXK_DOWN) + keep_processing = False +## dbg(indent=0) + return keep_processing + + + def __OnChar(self, event): + """ + Handler to explicitly look for ':' keyevents, and if found, + clear the m_shiftDown field, so it will behave as forward tab. + It then calls the base control's _OnChar routine with the modified + event instance. + """ +## dbg('TimeCtrl::OnChar', indent=1) + keycode = event.GetKeyCode() +## dbg('keycode:', keycode) + if keycode == ord(':'): +## dbg('colon seen! removing shift attribute') + event.m_shiftDown = False + BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress +## dbg(indent=0) + + + def __OnSetToNow(self, event): + """ + This is the key handler for '!' and 'c'; this allows the user to + quickly set the value of the control to the current time. + """ + self.SetValue(wx.DateTime_Now().FormatTime()) + keep_processing = False + return keep_processing + + + def __LimitSelection(self, event): + """ + Event handler for motion events; this handler + changes limits the selection to the new cell boundaries. + """ +## dbg('TimeCtrl::LimitSelection', indent=1) + pos = self.GetInsertionPoint() + self.__posCurrent = pos + sel_start, sel_to = self.GetSelection() + selection = sel_start != sel_to + if selection: + # only allow selection to end of current cell: + start, end = self._FindField(sel_start)._extent + if sel_to < pos: sel_to = start + elif sel_to > pos: sel_to = end + +## dbg('new pos =', self.__posCurrent, 'select to ', sel_to) + self.SetInsertionPoint(self.__posCurrent) + self.SetSelection(self.__posCurrent, sel_to) + if event: event.Skip() +## dbg(indent=0) + + + def __IncrementValue(self, key, pos): +## dbg('TimeCtrl::IncrementValue', key, pos, indent=1) + text = self.GetValue() + field = self._FindField(pos) +## dbg('field: ', field._index) + start, end = field._extent + slice = text[start:end] + if key == wx.WXK_UP: increment = 1 + else: increment = -1 + + if slice in ('A', 'P'): + if slice == 'A': newslice = 'P' + elif slice == 'P': newslice = 'A' + newvalue = text[:start] + newslice + text[end:] + + elif field._index == 0: + # adjusting this field is trickier, as its value can affect the + # am/pm setting. So, we use wxDateTime to generate a new value for us: + # (Use a fixed date not subject to DST variations:) + converter = wx.DateTimeFromDMY(1, 0, 1970) +## dbg('text: "%s"' % text) + converter.ParseTime(text.strip()) + currenthour = converter.GetHour() +## dbg('current hour:', currenthour) + newhour = (currenthour + increment) % 24 +## dbg('newhour:', newhour) + converter.SetHour(newhour) +## dbg('converter.GetHour():', converter.GetHour()) + newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue() + + else: # minute or second field; handled the same way: + newslice = "%02d" % ((int(slice) + increment) % 60) + newvalue = text[:start] + newslice + text[end:] + + try: + self.SetValue(newvalue) + + except ValueError: # must not be in bounds: + if not wx.Validator_IsSilent(): + wx.Bell() +## dbg(indent=0) + + + def _toGUI( self, wxdt ): + """ + This function takes a wxdt as an unambiguous representation of a time, and + converts it to a string appropriate for the format of the control. + """ + if self.__fmt24hr: + if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') + else: strval = wxdt.Format('%H:%M') + else: + if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') + else: strval = wxdt.Format('%I:%M %p') + + return strval + + + def __validateValue( self, value ): + """ + This function converts the value to a wxDateTime if not already one, + does bounds checking and raises ValueError if argument is + not a valid value for the control as currently specified. + It is used by both the SetValue() and the IsValid() methods. + """ +## dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) + if not value: +## dbg(indent=0) + raise ValueError('%s not a valid time value' % repr(value)) + + valid = True # assume true + try: + value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so + except: +## dbg('exception occurred', indent=0) + raise + + if self.IsLimited() and not self.IsInBounds(value): +## dbg(indent=0) + raise ValueError ( + 'value %s is not within the bounds of the control' % str(value) ) +## dbg(indent=0) + return value + +#---------------------------------------------------------------------------- +# Test jig for TimeCtrl: + +if __name__ == '__main__': + import traceback + + class TestPanel(wx.Panel): + def __init__(self, parent, id, + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr = 0, test_mx = 0, + style = wx.TAB_TRAVERSAL ): + + wx.Panel.__init__(self, parent, id, pos, size, style) + + self.test_mx = test_mx + + self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) + sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) + self.tc.BindSpinButton(sb) + + sizer = wx.BoxSizer( wx.HORIZONTAL ) + sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) + sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( sizer ) + sizer.Fit( self ) + sizer.SetSizeHints( self ) + + self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) + + def OnTimeChange(self, event): +## dbg('OnTimeChange: value = ', event.GetValue()) + wxdt = self.tc.GetWxDateTime() +## dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) + if self.test_mx: + mxdt = self.tc.GetMxDateTime() +## dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) + + + class MyApp(wx.App): + def OnInit(self): + import sys + fmt24hr = '24' in sys.argv + test_mx = 'mx' in sys.argv + try: + frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) + panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) + frame.Show(True) + except: + traceback.print_exc() + return False + return True + + try: + app = MyApp(0) + app.MainLoop() + except: + traceback.print_exc() +i=0 +## Version 1.2 +## 1. Changed parameter name display_seconds to displaySeconds, to follow +## other masked edit conventions. +## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. +## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of +## nonsensical parameter methods from the control, so it will work +## properly with Boa. diff --git a/wxPython/wx/lib/maskedctrl.py b/wxPython/wx/lib/maskedctrl.py deleted file mode 100644 index d5537775ae..0000000000 --- a/wxPython/wx/lib/maskedctrl.py +++ /dev/null @@ -1,109 +0,0 @@ -#---------------------------------------------------------------------------- -# Name: wxPython.lib.maskedctrl.py -# Author: Will Sadkin -# Created: 09/24/2003 -# Copyright: (c) 2003 by Will Sadkin -# RCS-ID: $Id$ -# License: wxWindows license -#---------------------------------------------------------------------------- -# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Updated for wx namespace (minor) -# -# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Removed wx prefix -# - -""" -

-MaskedCtrl is actually a factory function for several types of -masked edit controls: -

-

    -
  • MaskedTextCtrl - standard masked edit text box
  • -
  • MaskedComboBox - adds combobox capabilities
  • -
  • IpAddrCtrl - adds logical input semantics for IP address entry
  • -
  • TimeCtrl - special subclass handling lots of time formats as values
  • -
  • MaskedNumCtrl - special subclass handling numeric values
  • -
-

-MaskedCtrl works by looking for a special controlType -parameter in the variable arguments of the control, to determine -what kind of instance to return. -controlType can be one of: -


-    controlTypes.MASKEDTEXT
-    controlTypes.MASKEDCOMBO
-    controlTypes.IPADDR
-    controlTypes.TIME
-    controlTypes.NUMBER
-
-These constants are also available individually, ie, you can -use either of the following: -

-    from wxPython.wx.lib.maskedctrl import MaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER
-    from wxPython.wx.lib.maskedctrl import MaskedCtrl, controlTypes
-
-If not specified as a keyword argument, the default controlType is -controlTypes.MASKEDTEXT. -

-Each of the above classes has its own unique arguments, but MaskedCtrl -provides a single "unified" interface for masked controls. MaskedTextCtrl, -MaskedComboBox and IpAddrCtrl are all documented below; the others have -their own demo pages and interface descriptions. - -""" - -from wx.lib.maskededit import MaskedTextCtrl, MaskedComboBox, IpAddrCtrl -from wx.lib.maskednumctrl import MaskedNumCtrl -from wx.lib.timectrl import TimeCtrl - - -# "type" enumeration for class instance factory function -MASKEDTEXT = 0 -MASKEDCOMBO = 1 -IPADDR = 2 -TIME = 3 -NUMBER = 4 - -# for ease of import -class controlTypes: - MASKEDTEXT = MASKEDTEXT - MASKEDCOMBO = MASKEDCOMBO - IPADDR = IPADDR - TIME = TIME - NUMBER = NUMBER - - -def MaskedCtrl( *args, **kwargs): - """ - Actually a factory function providing a unifying - interface for generating masked controls. - """ - if not kwargs.has_key('controlType'): - controlType = MASKEDTEXT - else: - controlType = kwargs['controlType'] - del kwargs['controlType'] - - if controlType == MASKEDTEXT: - return MaskedTextCtrl(*args, **kwargs) - - elif controlType == MASKEDCOMBO: - return MaskedComboBox(*args, **kwargs) - - elif controlType == IPADDR: - return IpAddrCtrl(*args, **kwargs) - - elif controlType == TIME: - return TimeCtrl(*args, **kwargs) - - elif controlType == NUMBER: - return MaskedNumCtrl(*args, **kwargs) - - else: - raise AttributeError( - "invalid controlType specified: %s" % repr(controlType)) - - diff --git a/wxPython/wx/lib/maskededit.py b/wxPython/wx/lib/maskededit.py deleted file mode 100644 index bdc463543e..0000000000 --- a/wxPython/wx/lib/maskededit.py +++ /dev/null @@ -1,7608 +0,0 @@ -#---------------------------------------------------------------------------- -# Name: maskededit.py -# Authors: Jeff Childers, Will Sadkin -# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com -# Created: 02/11/2003 -# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 -# Portions: (c) 2002 by Will Sadkin, 2002-2003 -# RCS-ID: $Id$ -# License: wxWidgets license -#---------------------------------------------------------------------------- -# NOTE: -# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by -# Jason Hihn, and borrows liberally from Will Sadkin's original masked edit -# control for time entry, TimeCtrl (which is now rewritten using this -# control!). -# -# MaskedEdit controls do not normally use validators, because they do -# careful manipulation of the cursor in the text window on each keystroke, -# and validation is cursor-position specific, so the control intercepts the -# key codes before the validator would fire. However, validators can be -# provided to do data transfer to the controls. -# -#---------------------------------------------------------------------------- -# -# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Updated for wx namespace. No guarantees. This is one huge file. -# -# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Missed wx.DateTime stuff earlier. -# -# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o wxMaskedEditMixin -> MaskedEditMixin -# o wxMaskedTextCtrl -> MaskedTextCtrl -# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent -# o wxMaskedComboBox -> MaskedComboBox -# o wxIpAddrCtrl -> IpAddrCtrl -# o wxTimeCtrl -> TimeCtrl -# - -"""\ -Masked Edit Overview: -===================== -MaskedTextCtrl - is a sublassed text control that can carefully control the user's input - based on a mask string you provide. - - General usage example: - control = MaskedTextCtrl( win, -1, '', mask = '(###) ###-####') - - The example above will create a text control that allows only numbers to be - entered and then only in the positions indicated in the mask by the # sign. - -MaskedComboBox - 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. - -wxMaskedCtrl - is actually a factory function for several types of masked edit controls: - - MaskedTextCtrl - standard masked edit text box - MaskedComboBox - adds combobox capabilities - IpAddrCtrl - adds special semantics for IP address entry - TimeCtrl - special subclass handling lots of types as values - wxMaskedNumCtrl - special subclass handling numeric values - - It works by looking for a controlType parameter in the keyword - arguments of the control, to determine what kind of instance to return. - If not specified as a keyword argument, the default control type returned - will be MaskedTextCtrl. - - Each of the above classes has its own set of arguments, but wxMaskedCtrl - provides a single "unified" interface for masked controls. Those for - MaskedTextCtrl, MaskedComboBox and IpAddrCtrl are all documented - below; the others have their own demo pages and interface descriptions. - (See end of following discussion for how to configure the wxMaskedCtrl() - to select the above control types.) - - -INITILIZATION PARAMETERS -======================== -mask= -Allowed mask characters and function: - Character Function - # Allow numeric only (0-9) - N Allow letters and numbers (0-9) - A Allow uppercase letters only - a Allow lowercase letters only - C Allow any letter, upper or lower - X Allow string.letters, string.punctuation, string.digits - & Allow string.punctuation only - - - These controls define these sets of characters using string.letters, - string.uppercase, etc. These sets are affected by the system locale - setting, so in order to have the masked controls accept characters - that are specific to your users' language, your application should - set the locale. - For example, to allow international characters to be used in the - above masks, you can place the following in your code as part of - your application's initialization code: - - import locale - locale.setlocale(locale.LC_ALL, '') - - - Using these mask characters, a variety of template masks can be built. See - the demo for some other common examples include date+time, social security - number, etc. If any of these characters are needed as template rather - than mask characters, they can be escaped with \, ie. \N means "literal N". - (use \\ for literal backslash, as in: r'CCC\\NNN'.) - - - Note: - Masks containing only # characters and one optional decimal point - character are handled specially, as "numeric" controls. Such - controls have special handling for typing the '-' key, handling - the "decimal point" character as truncating the integer portion, - optionally allowing grouping characters and so forth. - There are several parameters and format codes that only make sense - when combined with such masks, eg. groupChar, decimalChar, and so - forth (see below). These allow you to construct reasonable - numeric entry controls. - - 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 are specified as a string of single character formatting - codes that modify behavior of the control: - _ Allow spaces - ! Force upper - ^ Force lower - R Right-align field(s) - r Right-insert in field(s) (implies R) - < Stay in field until explicit navigation out of it - - > Allow insert/delete within partially filled fields (as - opposed to the default "overwrite" mode for fixed-width - masked edit controls.) This allows single-field controls - or each field within a multi-field control to optionally - behave more like standard text controls. - (See EMAIL or phone number autoformat examples.) - - Note: This also governs whether backspace/delete operations - shift contents of field to right of cursor, or just blank the - erased section. - - Also, when combined with 'r', this indicates that the field - or control allows right insert anywhere within the current - non-empty value in the field. (Otherwise right-insert behavior - is only performed to when the entire right-insertable field is - selected or the cursor is at the right edge of the field. - - - , Allow grouping character in integer fields of numeric controls - and auto-group/regroup digits (if the result fits) when leaving - such a field. (If specified, .SetValue() will attempt to - auto-group as well.) - ',' is also the default grouping character. To change the - grouping character and/or decimal character, use the groupChar - and decimalChar parameters, respectively. - Note: typing the "decimal point" character in such fields will - clip the value to that left of the cursor for integer - fields of controls with "integer" or "floating point" masks. - If the ',' format code is specified, this will also cause the - resulting digits to be regrouped properly, using the current - grouping character. - - Prepend and reserve leading space for sign to mask and allow - signed values (negative #s shown in red by default.) Can be - used with argument useParensForNegatives (see below.) - 0 integer fields get leading zeros - D Date[/time] field - T Time field - F Auto-Fit: the control calulates its size from - the length of the template mask - V validate entered chars against validRegex before allowing them - to be entered vs. being allowed by basic mask and then having - the resulting value just colored as invalid. - (See USSTATE autoformat demo for how this can be used.) - S select entire field when navigating to new field - -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. - -groupChar= -decimalChar= - These parameters govern what character is used to group numbers - and is used to indicate the decimal point for numeric format controls. - The default groupChar is ',', the default decimalChar is '.' - By changing these, you can customize the presentation of numbers - for your location. - eg: formatcodes = ',', groupChar="'" allows 12'345.34 - formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34 - -shiftDecimalChar= - The default "shiftDecimalChar" (used for "backwards-tabbing" until - shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for - other keyboards, you may want to customize this, eg '?' for shift ',' on - AZERTY keyboards, ':' or ';' for other European keyboards, etc. - -useParensForNegatives=False - This option can be used with signed numeric format controls to - indicate signs via () rather than '-'. - -autoSelect=False - This option can be used to have a field or the control try to - auto-complete on each keystroke if choices have been specified. - -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 - choices= A list of strings that are allowed choices for the control. - choiceRequired= value must be member of choices list - compareNoCase= Perform case-insensitive matching when validating against list - Note: for MaskedComboBox, this defaults to True. - 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 .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow': - - emptyBackgroundColour= Control Background color when identified as empty. Default=White - invalidBackgroundColour= Control Background color when identified as Not valid. Default=Yellow - validBackgroundColour= Control Background color when identified as Valid. Default=white - - - The following parameters control the default foreground color coloring behavior of the - control. Pass a color as string e.g. 'Yellow': - foregroundColour= Control foreground color when value is not negative. Default=Black - signedForegroundColour= Control foreground color when value is negative. Default=Red - - -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 - groupChar # if set for a field, it will override the control's default - defaultValue # sets field-specific default value; overrides any default from control - compareNoCase # overrides control's settings - emptyInvalid # determines whether field is required to be filled at all times - validRequired # if set, requires field to contain valid value - - If any of the above parameters are subsequently specified for the control as a - whole, that new value will be propagated to each field, unless the - retainFieldValidation control-level parameter is set. - - formatcodes # Augments control's settings - excludeChars # ' ' ' - includeChars # ' ' ' - validRegex # ' ' ' - validRange # ' ' ' - choices # ' ' ' - choiceRequired # ' ' ' - validFunc # ' ' ' - - - -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 replacement" of passed value into the control, as does - the .Paste() method. As with other text entry controls, the - .SetValue() text replacement begins at left-edge of the control, - with missing mask characters inserted as appropriate. - .SetValue will also adjust integer, float or date mask entry values, - adding commas, auto-completing years, etc. as appropriate. - For "right-aligned" numeric controls, it will also now automatically - right-adjust any value whose length is less than the width of the - control before attempting to set the value. - If a value does not follow the format of the control's mask, or will - not fit into the control, a ValueError exception will be raised. - Eg: - mask = '(###) ###-####' - .SetValue('1234567890') => '(123) 456-7890' - .SetValue('(123)4567890') => '(123) 456-7890' - .SetValue('(123)456-7890') => '(123) 456-7890' - .SetValue('123/4567-890') => illegal paste; ValueError - - mask = '#{6}.#{2}', formatcodes = '_,-', - .SetValue('111') => ' 111 . ' - .SetValue(' %9.2f' % -111.12345 ) => ' -111.12' - .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00' - .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError - - mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control - .SetValue('111') => padded value misalignment ValueError: " 111" will not fit - .SetValue('%.2f' % 111 ) => ' 111.00' - .SetValue('%.2f' % -111.12345 ) => ' -111.12' - - - .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. - - .SetCtrlParameters(**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 = MaskedTextCtrl( self, -1 ) - ctl.SetCtrlParameters( mask='###-####', - defaultValue='555-1212', - formatcodes='F') - - .GetCtrlParameter(parametername) - This function allows you to retrieve the current value of a parameter - from the control. - - Note: Each of the control parameters can also be set using its - own Set and Get function. These functions follow a regular form: - All of the parameter names start with lower case; for their - corresponding Set/Get function, the parameter name is capitalized. - Eg: ctl.SetMask('###-####') - ctl.SetDefaultValue('555-1212') - ctl.GetChoiceRequired() - ctl.GetFormatcodes() - - Note: After any change in parameters, the choices for the - control are reevaluated to ensure that they are still legal. If you - have large choice lists, it is therefore more efficient to set parameters - before setting the choices available. - - .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 point. Without a decimal (e.g. '######', the control will treat it as an integer -value. With a decimal (e.g. '###.##'), the control will act as a floating point control -(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the -integer control truncates the value. However, for a true numeric control, -MaskedNumCtrl provides all this, and true numeric input/output support as well. - - -Check your controls by calling each control's .IsValid() function and the -.IsEmpty() function to determine which controls have been a) filled in and -b) filled in properly. - - -Regular expression validations can be used flexibly and creatively. -Take a look at the demo; the zip-code validation succeeds as long as the -first five numerals are entered. the last four are optional, but if -any are entered, there must be 4 to be valid. - -wxMaskedCtrl Configuration -========================== -wxMaskedCtrl works by looking for a special controlType -parameter in the variable arguments of the control, to determine -what kind of instance to return. -controlType can be one of: - - controlTypes.MASKEDTEXT - controlTypes.MASKEDCOMBO - controlTypes.IPADDR - controlTypes.TIME - controlTypes.NUMBER - -These constants are also available individually, ie, you can -use either of the following: - - from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, controlTypes - from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER - -If not specified as a keyword argument, the default controlType is -controlTypes.TEXT. -""" - -""" -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -DEVELOPER COMMENTS: - -Naming Conventions ------------------- - All methods of the Mixin that are not meant to be exposed to the external - interface are prefaced with '_'. Those functions that are primarily - intended to be internal subroutines subsequently start with a lower-case - letter; those that are primarily intended to be used and/or overridden - by derived subclasses start with a capital letter. - - The following methods must be used and/or defined when deriving a control - from wxMaskedEditMixin. NOTE: if deriving from a *masked edit* control - (eg. class IpAddrCtrl(MaskedTextCtrl) ), then this is NOT necessary, - as it's already been done for you in the base class. - - ._SetInitialValue() - This function must be called after the associated base - control has been initialized in the subclass __init__ - function. It sets the initial value of the control, - either to the value specified if non-empty, the - default value if specified, or the "template" for - the empty control as necessary. It will also set/reset - the font if necessary and apply formatting to the - control at this time. - - ._GetSelection() - REQUIRED - Each class derived from wxMaskedEditMixin must define - the function for getting the start and end of the - current text selection. The reason for this is - that not all controls have the same function name for - doing this; eg. wxTextCtrl uses .GetSelection(), - whereas we had to write a .GetMark() function for - wxComboBox, because .GetSelection() for the control - gets the currently selected list item from the combo - box, and the control doesn't (yet) natively provide - a means of determining the text selection. - ._SetSelection() - REQUIRED - Similarly to _GetSelection, each class derived from - wxMaskedEditMixin must define the function for setting - the start and end of the current text selection. - (eg. .SetSelection() for MaskedTextCtrl, and .SetMark() for - MaskedComboBox. - - ._GetInsertionPoint() - ._SetInsertionPoint() - REQUIRED - For consistency, and because the mixin shouldn't rely - on fixed names for any manipulations it does of any of - the base controls, we require each class derived from - wxMaskedEditMixin to define these functions as well. - - ._GetValue() - ._SetValue() REQUIRED - Each class derived from wxMaskedEditMixin must define - the functions used to get and set the raw value of the - control. - This is necessary so that recursion doesn't take place - when setting the value, and so that the mixin can - call the appropriate function after doing all its - validation and manipulation without knowing what kind - of base control it was mixed in with. To handle undo - functionality, the ._SetValue() must record the current - selection prior to setting the value. - - .Cut() - .Paste() - .Undo() - .SetValue() REQUIRED - Each class derived from wxMaskedEditMixin must redefine - these functions to call the _Cut(), _Paste(), _Undo() - and _SetValue() methods, respectively for the control, - so as to prevent programmatic corruption of the control's - value. This must be done in each derivation, as the - mixin cannot itself override a member of a sibling class. - - ._Refresh() REQUIRED - Each class derived from wxMaskedEditMixin must define - the function used to refresh the base control. - - .Refresh() REQUIRED - Each class derived from wxMaskedEditMixin must redefine - this function so that it checks the validity of the - control (via self._CheckValid) and then refreshes - control using the base class method. - - ._IsEditable() REQUIRED - Each class derived from wxMaskedEditMixin must define - the function used to determine if the base control is - editable or not. (For MaskedComboBox, this has to - be done with code, rather than specifying the proper - function in the base control, as there isn't one...) - ._CalcSize() REQUIRED - Each class derived from wxMaskedEditMixin must define - the function used to determine how wide the control - should be given the mask. (The mixin function - ._calcSize() provides a baseline estimate.) - - -Event Handling --------------- - Event handlers are "chained", and wxMaskedEditMixin usually - swallows most of the events it sees, thereby preventing any other - handlers from firing in the chain. It is therefore required that - each class derivation using the mixin to have an option to hook up - the event handlers itself or forego this operation and let a - subclass of the masked control do so. For this reason, each - subclass should probably include the following code: - - if setupEventHandling: - ## Setup event handlers - EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection - EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator - EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick - EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu - EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. - EVT_CHAR( self, self._OnChar ) ## handle each keypress - EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately & keep - ## track of previous value for undo - - where setupEventHandling is an argument to its constructor. - - These 5 handlers must be "wired up" for the wxMaskedEdit - control to provide default behavior. (The setupEventHandling - is an argument to MaskedTextCtrl and MaskedComboBox, so - that controls derived from *them* may replace one of these - handlers if they so choose.) - - If your derived control wants to preprocess events before - taking action, it should then set up the event handling itself, - so it can be first in the event handler chain. - - - The following routines are available to facilitate changing - the default behavior of wxMaskedEdit controls: - - ._SetKeycodeHandler(keycode, func) - ._SetKeyHandler(char, func) - Use to replace default handling for any given keycode. - func should take the key event as argument and return - False if no further action is required to handle the - key. Eg: - self._SetKeycodeHandler(WXK_UP, self.IncrementValue) - self._SetKeyHandler('-', self._OnChangeSign) - - "Navigation" keys are assumed to change the cursor position, and - therefore don't cause automatic motion of the cursor as insertable - characters do. - - ._AddNavKeycode(keycode, handler=None) - ._AddNavKey(char, handler=None) - Allows controls to specify other keys (and optional handlers) - to be treated as navigational characters. (eg. '.' in IpAddrCtrl) - - ._GetNavKeycodes() Returns the current list of navigational keycodes. - - ._SetNavKeycodes(key_func_tuples) - Allows replacement of the current list of keycode - processed as navigation keys, and bind associated - optional keyhandlers. argument is a list of key/handler - tuples. Passing a value of None for the handler in a - given tuple indicates that default processing for the key - is desired. - - ._FindField(pos) Returns the Field object associated with this position - in the control. - - ._FindFieldExtent(pos, getslice=False, value=None) - Returns edit_start, edit_end of the field corresponding - to the specified position within the control, and - optionally also returns the current contents of that field. - If value is specified, it will retrieve the slice the corresponding - slice from that value, rather than the current value of the - control. - - ._AdjustField(pos) - This is, the function that gets called for a given position - whenever the cursor is adjusted to leave a given field. - By default, it adjusts the year in date fields if mask is a date, - It can be overridden by a derived class to - adjust the value of the control at that time. - (eg. IpAddrCtrl reformats the address in this way.) - - ._Change() Called by internal EVT_TEXT handler. Return False to force - skip of the normal class change event. - ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force - skip of the normal class keypress event. - ._LostFocus() Called by internal EVT_KILL_FOCUS handler - - ._OnKeyDown(event) - This is the default EVT_KEY_DOWN routine; it just checks for - "navigation keys", and if event.ControlDown(), it fires the - mixin's _OnChar() routine, as such events are not always seen - by the "cooked" EVT_CHAR routine. - - ._OnChar(event) This is the main EVT_CHAR handler for the - wxMaskedEditMixin. - - The following routines are used to handle standard actions - for control keys: - _OnArrow(event) used for arrow navigation events - _OnCtrl_A(event) 'select all' - _OnCtrl_C(event) 'copy' (uses base control function, as copy is non-destructive) - _OnCtrl_S(event) 'save' (does nothing) - _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste - _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection - _OnCtrl_Z(event) 'undo' - resets value to previous value (if any) - - _OnChangeField(event) primarily used for tab events, but can be - used for other keys (eg. '.' in IpAddrCtrl) - - _OnErase(event) used for backspace and delete - _OnHome(event) - _OnEnd(event) - - The following routine provides a hook back to any class derivations, so that - they can react to parameter changes before any value is set/reset as a result of - those changes. (eg. MaskedComboBox needs to detect when the choices list is - modified, either implicitly or explicitly, so it can reset the base control - to have the appropriate choice list *before* the initial value is reset to match.) - - _OnCtrlParametersChanged() - -Accessor Functions ------------------- - For convenience, each class derived from MaskedEditMixin should - define an accessors mixin, so that it exposes only those parameters - that make sense for the derivation. This is done with an intermediate - level of inheritance, ie: - - class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): - - class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): - class MaskedNumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): - class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): - class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): - - etc. - - Each accessors mixin defines Get/Set functions for the base class parameters - that are appropriate for that derivation. - This allows the base classes to be "more generic," exposing the widest - set of options, while not requiring derived classes to be so general. -""" - -import copy -import difflib -import re -import string -import types - -import wx - -# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would -# be a good place to implement the 2.3 logger class -from wx.tools.dbg import Logger - -dbg = Logger() -##dbg(enable=0) - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -## Constants for identifying control keys and classes of keys: - -WXK_CTRL_A = (ord('A')+1) - ord('A') ## These keys are not already defined in wx -WXK_CTRL_C = (ord('C')+1) - ord('A') -WXK_CTRL_S = (ord('S')+1) - ord('A') -WXK_CTRL_V = (ord('V')+1) - ord('A') -WXK_CTRL_X = (ord('X')+1) - ord('A') -WXK_CTRL_Z = (ord('Z')+1) - ord('A') - -nav = ( - wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, - wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT - ) - -control = ( - wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, - WXK_CTRL_X, WXK_CTRL_Z - ) - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -## Constants for masking. This is where mask characters -## are defined. -## maskchars used to identify valid mask characters from all others -## #- allow numeric 0-9 only -## A- allow uppercase only. Combine with forceupper to force lowercase to upper -## a- allow lowercase only. Combine with forcelower to force upper to lowercase -## X- allow any character (string.letters, string.punctuation, string.digits) -## Note: locale settings affect what "uppercase", lowercase, etc comprise. -## -maskchars = ("#","A","a","X","C","N", '&') - -months = '(01|02|03|04|05|06|07|08|09|10|11|12)' -charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)' -charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, - 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} - -days = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)' -hours = '(0\d| \d|1[012])' -milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)' -minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ -16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ -36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ -56|57|58|59)""" -seconds = minutes -am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde' - -states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',') - -state_names = ['Alabama','Alaska','Arizona','Arkansas', - 'California','Colorado','Connecticut', - 'Delaware','District of Columbia', - 'Florida','Georgia','Hawaii', - 'Idaho','Illinois','Indiana','Iowa', - 'Kansas','Kentucky','Louisiana', - 'Maine','Maryland','Massachusetts','Michigan', - 'Minnesota','Mississippi','Missouri','Montana', - 'Nebraska','Nevada','New Hampshire','New Jersey', - 'New Mexico','New York','North Carolina','North Dakokta', - 'Ohio','Oklahoma','Oregon', - 'Pennsylvania','Puerto Rico','Rhode Island', - 'South Carolina','South Dakota', - 'Tennessee','Texas','Utah', - 'Vermont','Virginia', - 'Washington','West Virginia', - 'Wisconsin','Wyoming'] - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -## The following dictionary defines the current set of autoformats: - -masktags = { - "USPHONEFULLEXT": { - 'mask': "(###) ###-#### x:###", - 'formatcodes': 'F^->', - 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", - 'description': "Phone Number w/opt. ext" - }, - "USPHONETIGHTEXT": { - 'mask': "###-###-#### x:###", - 'formatcodes': 'F^->', - 'validRegex': "^\d{3}-\d{3}-\d{4}", - 'description': "Phone Number\n (w/hyphens and opt. ext)" - }, - "USPHONEFULL": { - 'mask': "(###) ###-####", - 'formatcodes': 'F^->', - 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", - 'description': "Phone Number only" - }, - "USPHONETIGHT": { - 'mask': "###-###-####", - 'formatcodes': 'F^->', - 'validRegex': "^\d{3}-\d{3}-\d{4}", - 'description': "Phone Number\n(w/hyphens)" - }, - "USSTATE": { - 'mask': "AA", - 'formatcodes': 'F!V', - 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), - 'choices': states, - 'choiceRequired': True, - 'description': "US State Code" - }, - "USSTATENAME": { - 'mask': "ACCCCCCCCCCCCCCCCCCC", - 'formatcodes': 'F_', - 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'), - 'choices': state_names, - 'choiceRequired': True, - 'description': "US State Name" - }, - - "USDATETIMEMMDDYYYY/HHMMSS": { - 'mask': "##/##/#### ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "US Date + Time" - }, - "USDATETIMEMMDDYYYY-HHMMSS": { - 'mask': "##-##-#### ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "US Date + Time\n(w/hypens)" - }, - "USDATE24HRTIMEMMDDYYYY/HHMMSS": { - 'mask': "##/##/#### ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, - 'description': "US Date + 24Hr (Military) Time" - }, - "USDATE24HRTIMEMMDDYYYY-HHMMSS": { - 'mask': "##-##-#### ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, - 'description': "US Date + 24Hr Time\n(w/hypens)" - }, - "USDATETIMEMMDDYYYY/HHMM": { - 'mask': "##/##/#### ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', - 'description': "US Date + Time\n(without seconds)" - }, - "USDATE24HRTIMEMMDDYYYY/HHMM": { - 'mask': "##/##/#### ##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes, - 'description': "US Date + 24Hr Time\n(without seconds)" - }, - "USDATETIMEMMDDYYYY-HHMM": { - 'mask': "##-##-#### ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', - 'description': "US Date + Time\n(w/hypens and w/o secs)" - }, - "USDATE24HRTIMEMMDDYYYY-HHMM": { - 'mask': "##-##-#### ##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes, - 'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" - }, - "USDATEMMDDYYYY/": { - 'mask': "##/##/####", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '/' + days + '/' + '\d{4}', - 'description': "US Date\n(MMDDYYYY)" - }, - "USDATEMMDDYY/": { - 'mask': "##/##/##", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '/' + days + '/\d\d', - 'description': "US Date\n(MMDDYY)" - }, - "USDATEMMDDYYYY-": { - 'mask': "##-##-####", - 'formatcodes': 'DF', - 'validRegex': '^' + months + '-' + days + '-' +'\d{4}', - 'description': "MM-DD-YYYY" - }, - - "EUDATEYYYYMMDD/": { - 'mask': "####/##/##", - 'formatcodes': 'DF', - 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days, - 'description': "YYYY/MM/DD" - }, - "EUDATEYYYYMMDD.": { - 'mask': "####.##.##", - 'formatcodes': 'DF', - 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days, - 'description': "YYYY.MM.DD" - }, - "EUDATEDDMMYYYY/": { - 'mask': "##/##/####", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '/' + months + '/' + '\d{4}', - 'description': "DD/MM/YYYY" - }, - "EUDATEDDMMYYYY.": { - 'mask': "##.##.####", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '.' + months + '.' + '\d{4}', - 'description': "DD.MM.YYYY" - }, - "EUDATEDDMMMYYYY.": { - 'mask': "##.CCC.####", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}', - 'description': "DD.Month.YYYY" - }, - "EUDATEDDMMMYYYY/": { - 'mask': "##/CCC/####", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}', - 'description': "DD/Month/YYYY" - }, - - "EUDATETIMEYYYYMMDD/HHMMSS": { - 'mask': "####/##/## ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "YYYY/MM/DD HH:MM:SS" - }, - "EUDATETIMEYYYYMMDD.HHMMSS": { - 'mask': "####.##.## ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "YYYY.MM.DD HH:MM:SS" - }, - "EUDATETIMEDDMMYYYY/HHMMSS": { - 'mask': "##/##/#### ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "DD/MM/YYYY HH:MM:SS" - }, - "EUDATETIMEDDMMYYYY.HHMMSS": { - 'mask': "##.##.#### ##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "DD.MM.YYYY HH:MM:SS" - }, - - "EUDATETIMEYYYYMMDD/HHMM": { - 'mask': "####/##/## ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M', - 'description': "YYYY/MM/DD HH:MM" - }, - "EUDATETIMEYYYYMMDD.HHMM": { - 'mask': "####.##.## ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M', - 'description': "YYYY.MM.DD HH:MM" - }, - "EUDATETIMEDDMMYYYY/HHMM": { - 'mask': "##/##/#### ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', - 'description': "DD/MM/YYYY HH:MM" - }, - "EUDATETIMEDDMMYYYY.HHMM": { - 'mask': "##.##.#### ##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'DF!', - 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', - 'description': "DD.MM.YYYY HH:MM" - }, - - "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { - 'mask': "####/##/## ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds, - 'description': "YYYY/MM/DD 24Hr Time" - }, - "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { - 'mask': "####.##.## ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds, - 'description': "YYYY.MM.DD 24Hr Time" - }, - "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { - 'mask': "##/##/#### ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, - 'description': "DD/MM/YYYY 24Hr Time" - }, - "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { - 'mask': "##.##.#### ##:##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, - 'description': "DD.MM.YYYY 24Hr Time" - }, - "EUDATE24HRTIMEYYYYMMDD/HHMM": { - 'mask': "####/##/## ##:##", - 'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes, - 'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" - }, - "EUDATE24HRTIMEYYYYMMDD.HHMM": { - 'mask': "####.##.## ##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes, - 'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" - }, - "EUDATE24HRTIMEDDMMYYYY/HHMM": { - 'mask': "##/##/#### ##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes, - 'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" - }, - "EUDATE24HRTIMEDDMMYYYY.HHMM": { - 'mask': "##.##.#### ##:##", - 'formatcodes': 'DF', - 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes, - 'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" - }, - - "TIMEHHMMSS": { - 'mask': "##:##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'TF!', - 'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', - 'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" - }, - "TIMEHHMM": { - 'mask': "##:## AM", - 'excludeChars': am_pm_exclude, - 'formatcodes': 'TF!', - 'validRegex': '^' + hours + ':' + minutes + ' (A|P)M', - 'description': "HH:MM (A|P)M\n(see TimeCtrl)" - }, - "24HRTIMEHHMMSS": { - 'mask': "##:##:##", - 'formatcodes': 'TF', - 'validRegex': '^' + milhours + ':' + minutes + ':' + seconds, - 'description': "24Hr HH:MM:SS\n(see TimeCtrl)" - }, - "24HRTIMEHHMM": { - 'mask': "##:##", - 'formatcodes': 'TF', - 'validRegex': '^' + milhours + ':' + minutes, - 'description': "24Hr HH:MM\n(see TimeCtrl)" - }, - "USSOCIALSEC": { - 'mask': "###-##-####", - 'formatcodes': 'F', - 'validRegex': "\d{3}-\d{2}-\d{4}", - 'description': "Social Sec#" - }, - "CREDITCARD": { - 'mask': "####-####-####-####", - 'formatcodes': 'F', - 'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", - 'description': "Credit Card" - }, - "EXPDATEMMYY": { - 'mask': "##/##", - 'formatcodes': "F", - 'validRegex': "^" + months + "/\d\d", - 'description': "Expiration MM/YY" - }, - "USZIP": { - 'mask': "#####", - 'formatcodes': 'F', - 'validRegex': "^\d{5}", - 'description': "US 5-digit zip code" - }, - "USZIPPLUS4": { - 'mask': "#####-####", - 'formatcodes': 'F', - 'validRegex': "\d{5}-(\s{4}|\d{4})", - 'description': "US zip+4 code" - }, - "PERCENT": { - 'mask': "0.##", - 'formatcodes': 'F', - 'validRegex': "^0.\d\d", - 'description': "Percentage" - }, - "AGE": { - 'mask': "###", - 'formatcodes': "F", - 'validRegex': "^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]", - 'description': "Age" - }, - "EMAIL": { - 'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - 'excludeChars': " \\/*&%$#!+='\"", - 'formatcodes': "F>", - 'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$", - 'description': "Email address" - }, - "IPADDR": { - 'mask': "###.###.###.###", - 'formatcodes': 'F_Sr', - 'validRegex': "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}", - 'description': "IP Address\n(see IpAddrCtrl)" - } - } - -# build demo-friendly dictionary of descriptions of autoformats -autoformats = [] -for key, value in masktags.items(): - autoformats.append((key, value['description'])) -autoformats.sort() - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -class Field: - valid_params = { - 'index': None, ## which field of mask; set by parent control. - 'mask': "", ## mask chars for this field - 'extent': (), ## (edit start, edit_end) of field; set by parent control. - 'formatcodes': "", ## codes indicating formatting options for the control - 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given - 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits - 'decimalChar': '.', ## used with numeric fields; indicates what char separates integer from fraction - 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard - 'useParensForNegatives': False, ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. - 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar - 'excludeChars': "", ## optional string of chars to exclude even if main mask type does - 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't - 'validRegex': "", ## optional regular expression to use to validate the control - 'validRange': (), ## Optional hi-low range for numerics - 'choices': [], ## Optional list for character expressions - 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list - 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search - 'autoSelect': False, ## Set to True to try auto-completion on each keystroke: - 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl - 'validRequired': False, ## Set to True to disallow input that results in an invalid value - 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID - 'description': "", ## primarily for autoformats, but could be useful elsewhere - } - - # This list contains all parameters that when set at the control level should - # propagate down to each field: - propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', - 'compareNoCase', 'emptyInvalid', 'validRequired') - - def __init__(self, **kwargs): - """ - This is the "constructor" for setting up parameters for fields. - a field_index of -1 is used to indicate "the entire control." - """ -#### dbg('Field::Field', indent=1) - # Validate legitimate set of parameters: - for key in kwargs.keys(): - if key not in Field.valid_params.keys(): -#### dbg(indent=0) - raise TypeError('invalid parameter "%s"' % (key)) - - # Set defaults for each parameter for this instance, and fully - # populate initial parameter list for configuration: - for key, value in Field.valid_params.items(): - setattr(self, '_' + key, copy.copy(value)) - if not kwargs.has_key(key): - kwargs[key] = copy.copy(value) - - self._autoCompleteIndex = -1 - self._SetParameters(**kwargs) - self._ValidateParameters(**kwargs) - -#### dbg(indent=0) - - - def _SetParameters(self, **kwargs): - """ - This function can be used to set individual or multiple parameters for - a masked edit field parameter after construction. - """ -## dbg(suspend=1) -## dbg('maskededit.Field::_SetParameters', indent=1) - # Validate keyword arguments: - for key in kwargs.keys(): - if key not in Field.valid_params.keys(): -## dbg(indent=0, suspend=0) - raise AttributeError('invalid keyword argument "%s"' % key) - - if self._index is not None: dbg('field index:', self._index) -## dbg('parameters:', indent=1) - for key, value in kwargs.items(): -## dbg('%s:' % key, value) - pass -## dbg(indent=0) - - - old_fillChar = self._fillChar # store so we can change choice lists accordingly if it changes - - # First, Assign all parameters specified: - for key in Field.valid_params.keys(): - if kwargs.has_key(key): - setattr(self, '_' + key, kwargs[key] ) - - if kwargs.has_key('formatcodes'): # (set/changed) - self._forceupper = '!' in self._formatcodes - self._forcelower = '^' in self._formatcodes - self._groupdigits = ',' in self._formatcodes - self._okSpaces = '_' in self._formatcodes - self._padZero = '0' in self._formatcodes - self._autofit = 'F' in self._formatcodes - self._insertRight = 'r' in self._formatcodes - self._allowInsert = '>' in self._formatcodes - self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes - self._moveOnFieldFull = not '<' in self._formatcodes - self._selectOnFieldEntry = 'S' in self._formatcodes - - if kwargs.has_key('groupChar'): - self._groupChar = kwargs['groupChar'] - if kwargs.has_key('decimalChar'): - self._decimalChar = kwargs['decimalChar'] - if kwargs.has_key('shiftDecimalChar'): - self._shiftDecimalChar = kwargs['shiftDecimalChar'] - - if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'): - self._regexMask = 'V' in self._formatcodes and self._validRegex - - if kwargs.has_key('fillChar'): - self._old_fillChar = old_fillChar -#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) - - if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed) - self._isInt = isInteger(self._mask) -## dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) - -## dbg(indent=0, suspend=0) - - - def _ValidateParameters(self, **kwargs): - """ - This function can be used to validate individual or multiple parameters for - a masked edit field parameter after construction. - """ -## dbg(suspend=1) -## dbg('maskededit.Field::_ValidateParameters', indent=1) - if self._index is not None: dbg('field index:', self._index) -#### dbg('parameters:', indent=1) -## for key, value in kwargs.items(): -#### dbg('%s:' % key, value) -#### dbg(indent=0) -#### dbg("self._old_fillChar: '%s'" % self._old_fillChar) - - # Verify proper numeric format params: - if self._groupdigits and self._groupChar == self._decimalChar: -## dbg(indent=0, suspend=0) - raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar)) - - - # Now go do validation, semantic and inter-dependency parameter processing: - if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed) - - self._compareChoices = [choice.strip() for choice in self._choices] - - if self._compareNoCase and self._choices: - self._compareChoices = [item.lower() for item in self._compareChoices] - - if kwargs.has_key('choices'): - self._autoCompleteIndex = -1 - - - if kwargs.has_key('validRegex'): # (set/changed) - if self._validRegex: - try: - if self._compareNoCase: - self._filter = re.compile(self._validRegex, re.IGNORECASE) - else: - self._filter = re.compile(self._validRegex) - except: -## dbg(indent=0, suspend=0) - raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex)) - else: - self._filter = None - - if kwargs.has_key('validRange'): # (set/changed) - self._hasRange = False - self._rangeHigh = 0 - self._rangeLow = 0 - if self._validRange: - if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]: -## dbg(indent=0, suspend=0) - raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' - % (str(self._index), repr(self._validRange)) ) - - self._hasRange = True - self._rangeLow = self._validRange[0] - self._rangeHigh = self._validRange[1] - - if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)): # (set/changed) - self._hasList = False - if self._choices and type(self._choices) not in (types.TupleType, types.ListType): -## dbg(indent=0, suspend=0) - raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) - elif len( self._choices) > 0: - for choice in self._choices: - if type(choice) not in (types.StringType, types.UnicodeType): -## dbg(indent=0, suspend=0) - raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) - - length = len(self._mask) -## dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) - if len(self._choices) and length: - if len(self._choices[0]) > length: - # changed mask without respecifying choices; readjust the width as appropriate: - self._choices = [choice.strip() for choice in self._choices] - if self._alignRight: - self._choices = [choice.rjust( length ) for choice in self._choices] - else: - self._choices = [choice.ljust( length ) for choice in self._choices] -## dbg('aligned choices:', self._choices) - - if hasattr(self, '_template'): - # Verify each choice specified is valid: - for choice in self._choices: - if self.IsEmpty(choice) and not self._validRequired: - # allow empty values even if invalid, (just colored differently) - continue - if not self.IsValid(choice): -## dbg(indent=0, suspend=0) - raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) - self._hasList = True - -#### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) -#### dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) - if kwargs.has_key('fillChar') and len(self._choices) > 0: - if kwargs['fillChar'] != ' ': - self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices] - else: - self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices] -## dbg('updated choices:', self._choices) - - - if kwargs.has_key('autoSelect') and kwargs['autoSelect']: - if not self._hasList: -## dbg('no list to auto complete; ignoring "autoSelect=True"') - self._autoSelect = False - - # reset field validity assumption: - self._valid = True -## dbg(indent=0, suspend=0) - - - def _GetParameter(self, paramname): - """ - Routine for retrieving the value of any given parameter - """ - if Field.valid_params.has_key(paramname): - return getattr(self, '_' + paramname) - else: - TypeError('Field._GetParameter: invalid parameter "%s"' % key) - - - def IsEmpty(self, slice): - """ - Indicates whether the specified slice is considered empty for the - field. - """ -## dbg('Field::IsEmpty("%s")' % slice, indent=1) - if not hasattr(self, '_template'): -## dbg(indent=0) - raise AttributeError('_template') - -## dbg('self._template: "%s"' % self._template) -## dbg('self._defaultValue: "%s"' % str(self._defaultValue)) - if slice == self._template and not self._defaultValue: -## dbg(indent=0) - return True - - elif slice == self._template: - empty = True - for pos in range(len(self._template)): -#### dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) - if slice[pos] not in (' ', self._fillChar): - empty = False - break -## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) - return empty - else: -## dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) - return False - - - def IsValid(self, slice): - """ - Indicates whether the specified slice is considered a valid value for the - field. - """ -## dbg(suspend=1) -## dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) - valid = True # assume true to start - - if self.IsEmpty(slice): -## dbg(indent=0, suspend=0) - if self._emptyInvalid: - return False - else: - return True - - elif self._hasList and self._choiceRequired: -## dbg("(member of list required)") - # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): - if self._fillChar != ' ': - slice = slice.replace(self._fillChar, ' ') -## dbg('updated slice:"%s"' % slice) - compareStr = slice.strip() - - if self._compareNoCase: - compareStr = compareStr.lower() - valid = compareStr in self._compareChoices - - elif self._hasRange and not self.IsEmpty(slice): -## dbg('validating against range') - try: - # allow float as well as int ranges (int comparisons for free.) - valid = self._rangeLow <= float(slice) <= self._rangeHigh - except: - valid = False - - elif self._validRegex and self._filter: -## dbg('validating against regex') - valid = (re.match( self._filter, slice) is not None) - - if valid and self._validFunc: -## dbg('validating against supplied function') - valid = self._validFunc(slice) -## dbg('valid?', valid, indent=0, suspend=0) - return valid - - - def _AdjustField(self, slice): - """ 'Fixes' an integer field. Right or left-justifies, as required.""" -## dbg('Field::_AdjustField("%s")' % slice, indent=1) - length = len(self._mask) -#### dbg('length(self._mask):', length) -#### dbg('self._useParensForNegatives?', self._useParensForNegatives) - if self._isInt: - if self._useParensForNegatives: - signpos = slice.find('(') - right_signpos = slice.find(')') - intStr = slice.replace('(', '').replace(')', '') # drop sign, if any - else: - signpos = slice.find('-') - intStr = slice.replace( '-', '' ) # drop sign, if any - right_signpos = -1 - - intStr = intStr.replace(' ', '') # drop extra spaces - intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars - intStr = string.replace(intStr,"-","") # drop sign, if any - intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots -#### dbg('intStr:"%s"' % intStr) - start, end = self._extent - field_len = end - start - if not self._padZero and len(intStr) != field_len and intStr.strip(): - intStr = str(long(intStr)) -#### dbg('raw int str: "%s"' % intStr) -#### dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) - if self._groupdigits: - new = '' - cnt = 1 - for i in range(len(intStr)-1, -1, -1): - new = intStr[i] + new - if (cnt) % 3 == 0: - new = self._groupChar + new - cnt += 1 - if new and new[0] == self._groupChar: - new = new[1:] - if len(new) <= length: - # expanded string will still fit and leave room for sign: - intStr = new - # else... leave it without the commas... - -## dbg('padzero?', self._padZero) -## dbg('len(intStr):', len(intStr), 'field length:', length) - if self._padZero and len(intStr) < length: - intStr = '0' * (length - len(intStr)) + intStr - if signpos != -1: # we had a sign before; restore it - if self._useParensForNegatives: - intStr = '(' + intStr[1:] - if right_signpos != -1: - intStr += ')' - else: - intStr = '-' + intStr[1:] - elif signpos != -1 and slice[0:signpos].strip() == '': # - was before digits - if self._useParensForNegatives: - intStr = '(' + intStr - if right_signpos != -1: - intStr += ')' - else: - intStr = '-' + intStr - elif right_signpos != -1: - # must have had ')' but '(' was before field; re-add ')' - intStr += ')' - slice = intStr - - slice = slice.strip() # drop extra spaces - - if self._alignRight: ## Only if right-alignment is enabled - slice = slice.rjust( length ) - else: - slice = slice.ljust( length ) - if self._fillChar != ' ': - slice = slice.replace(' ', self._fillChar) -## dbg('adjusted slice: "%s"' % slice, indent=0) - return slice - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -class MaskedEditMixin: - """ - This class allows us to abstract the masked edit functionality that could - be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.) - """ - valid_ctrl_params = { - 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control - 'autoformat': "", ## optional auto-format code to set format from masktags dictionary - 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask - 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year - 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete - 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control - 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently, - ## from field validation constraints - 'emptyBackgroundColour': "White", - 'validBackgroundColour': "White", - 'invalidBackgroundColour': "Yellow", - 'foregroundColour': "Black", - 'signedForegroundColour': "Red", - 'demo': False} - - - def __init__(self, name = 'wxMaskedEdit', **kwargs): - """ - This is the "constructor" for setting up the mixin variable parameters for the composite class. - """ - - self.name = name - - # set up flag for doing optional things to base control if possible - if not hasattr(self, 'controlInitialized'): - self.controlInitialized = False - - # Set internal state var for keeping track of whether or not a character - # action results in a modification of the control, since .SetValue() - # doesn't modify the base control's internal state: - self.modified = False - self._previous_mask = None - - # Validate legitimate set of parameters: - for key in kwargs.keys(): - if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): - raise TypeError('%s: invalid parameter "%s"' % (name, key)) - - ## Set up dictionary that can be used by subclasses to override or add to default - ## behavior for individual characters. Derived subclasses needing to change - ## default behavior for keys can either redefine the default functions for the - ## common keys or add functions for specific keys to this list. Each function - ## added should take the key event as argument, and return False if the key - ## requires no further processing. - ## - ## Initially populated with navigation and function control keys: - self._keyhandlers = { - # default navigation keys and handlers: - wx.WXK_BACK: self._OnErase, - wx.WXK_LEFT: self._OnArrow, - wx.WXK_RIGHT: self._OnArrow, - wx.WXK_UP: self._OnAutoCompleteField, - wx.WXK_DOWN: self._OnAutoCompleteField, - wx.WXK_TAB: self._OnChangeField, - wx.WXK_HOME: self._OnHome, - wx.WXK_END: self._OnEnd, - wx.WXK_RETURN: self._OnReturn, - wx.WXK_PRIOR: self._OnAutoCompleteField, - wx.WXK_NEXT: self._OnAutoCompleteField, - - # default function control keys and handlers: - wx.WXK_DELETE: self._OnErase, - WXK_CTRL_A: self._OnCtrl_A, - WXK_CTRL_C: self._OnCtrl_C, - WXK_CTRL_S: self._OnCtrl_S, - WXK_CTRL_V: self._OnCtrl_V, - WXK_CTRL_X: self._OnCtrl_X, - WXK_CTRL_Z: self._OnCtrl_Z, - } - - ## bind standard navigational and control keycodes to this instance, - ## so that they can be augmented and/or changed in derived classes: - self._nav = list(nav) - self._control = list(control) - - ## Dynamically evaluate and store string constants for mask chars - ## so that locale settings can be made after this module is imported - ## and the controls created after that is done can allow the - ## appropriate characters: - self.maskchardict = { - '#': string.digits, - 'A': string.uppercase, - 'a': string.lowercase, - 'X': string.letters + string.punctuation + string.digits, - 'C': string.letters, - 'N': string.letters + string.digits, - '&': string.punctuation - } - - ## self._ignoreChange is used by MaskedComboBox, because - ## of the hack necessary to determine the selection; it causes - ## EVT_TEXT messages from the combobox to be ignored if set. - self._ignoreChange = False - - # These are used to keep track of previous value, for undo functionality: - self._curValue = None - self._prevValue = None - - self._valid = True - - # Set defaults for each parameter for this instance, and fully - # populate initial parameter list for configuration: - for key, value in MaskedEditMixin.valid_ctrl_params.items(): - setattr(self, '_' + key, copy.copy(value)) - if not kwargs.has_key(key): -#### dbg('%s: "%s"' % (key, repr(value))) - kwargs[key] = copy.copy(value) - - # Create a "field" that holds global parameters for control constraints - self._ctrl_constraints = self._fields[-1] = Field(index=-1) - self.SetCtrlParameters(**kwargs) - - - - def SetCtrlParameters(self, **kwargs): - """ - This public function can be used to set individual or multiple masked edit - parameters after construction. - """ -## dbg(suspend=1) -## dbg('MaskedEditMixin::SetCtrlParameters', indent=1) -#### dbg('kwargs:', indent=1) -## for key, value in kwargs.items(): -#### dbg(key, '=', value) -#### dbg(indent=0) - - # Validate keyword arguments: - constraint_kwargs = {} - ctrl_kwargs = {} - for key, value in kwargs.items(): - key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling - if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): -## dbg(indent=0, suspend=0) - raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name)) - elif key in Field.valid_params.keys(): - constraint_kwargs[key] = value - else: - ctrl_kwargs[key] = value - - mask = None - reset_args = {} - - if ctrl_kwargs.has_key('autoformat'): - autoformat = ctrl_kwargs['autoformat'] - else: - autoformat = None - - # handle "parochial name" backward compatibility: - if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys(): - autoformat = autoformat.replace('MILTIME', '24HRTIME') - - if autoformat != self._autoformat and autoformat in masktags.keys(): -## dbg('autoformat:', autoformat) - self._autoformat = autoformat - mask = masktags[self._autoformat]['mask'] - # gather rest of any autoformat parameters: - for param, value in masktags[self._autoformat].items(): - if param == 'mask': continue # (must be present; already accounted for) - constraint_kwargs[param] = value - - elif autoformat and not autoformat in masktags.keys(): - raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat)) - else: -## dbg('autoformat not selected') - if kwargs.has_key('mask'): - mask = kwargs['mask'] -## dbg('mask:', mask) - - ## Assign style flags - if mask is None: -## dbg('preserving previous mask') - mask = self._previous_mask # preserve previous mask - else: -## dbg('mask (re)set') - reset_args['reset_mask'] = mask - constraint_kwargs['mask'] = mask - - # wipe out previous fields; preserve new control-level constraints - self._fields = {-1: self._ctrl_constraints} - - - if ctrl_kwargs.has_key('fields'): - # do field parameter type validation, and conversion to internal dictionary - # as appropriate: - fields = ctrl_kwargs['fields'] - if type(fields) in (types.ListType, types.TupleType): - for i in range(len(fields)): - field = fields[i] - if not isinstance(field, Field): -## dbg(indent=0, suspend=0) - raise AttributeError('invalid type for field parameter: %s' % repr(field)) - self._fields[i] = field - - elif type(fields) == types.DictionaryType: - for index, field in fields.items(): - if not isinstance(field, Field): -## dbg(indent=0, suspend=0) - raise AttributeError('invalid type for field parameter: %s' % repr(field)) - self._fields[index] = field - else: -## dbg(indent=0, suspend=0) - raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields)) - - # Assign constraint parameters for entire control: -#### dbg('control constraints:', indent=1) -## for key, value in constraint_kwargs.items(): -#### dbg('%s:' % key, value) -#### dbg(indent=0) - - # determine if changing parameters that should affect the entire control: - for key in MaskedEditMixin.valid_ctrl_params.keys(): - if key in ( 'mask', 'fields' ): continue # (processed separately) - if ctrl_kwargs.has_key(key): - setattr(self, '_' + key, ctrl_kwargs[key]) - - # Validate color parameters, converting strings to named colors and validating - # result if appropriate: - for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', - 'foregroundColour', 'signedForegroundColour'): - if ctrl_kwargs.has_key(key): - if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType): - c = wx.NamedColour(ctrl_kwargs[key]) - if c.Get() == (-1, -1, -1): - raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) - else: - # replace attribute with wxColour object: - setattr(self, '_' + key, c) - # attach a python dynamic attribute to wxColour for debug printouts - c._name = ctrl_kwargs[key] - - elif type(ctrl_kwargs[key]) != type(wx.BLACK): - raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) - - -## dbg('self._retainFieldValidation:', self._retainFieldValidation) - if not self._retainFieldValidation: - # Build dictionary of any changing parameters which should be propagated to the - # component fields: - for arg in Field.propagating_params: -#### dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) -#### dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) - reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg) -#### dbg('reset_args[%s]?' % arg, reset_args[arg]) - - # Set the control-level constraints: - self._ctrl_constraints._SetParameters(**constraint_kwargs) - - # This routine does the bulk of the interdependent parameter processing, determining - # the field extents of the mask if changed, resetting parameters as appropriate, - # determining the overall template value for the control, etc. - self._configure(mask, **reset_args) - - # now that we've propagated the field constraints and mask portions to the - # various fields, validate the constraints - self._ctrl_constraints._ValidateParameters(**constraint_kwargs) - - # Validate that all choices for given fields are at least of the - # necessary length, and that they all would be valid pastes if pasted - # into their respective fields: -#### dbg('validating choices') - self._validateChoices() - - - self._autofit = self._ctrl_constraints._autofit - self._isNeg = False - - self._isDate = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask) - self._isTime = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask) - if self._isDate: - # Set _dateExtent, used in date validation to locate date in string; - # always set as though year will be 4 digits, even if mask only has - # 2 digits, so we can always properly process the intended year for - # date validation (leap years, etc.) - if self._mask.find('CCC') != -1: self._dateExtent = 11 - else: self._dateExtent = 10 - - self._4digityear = len(self._mask) > 8 and self._mask[9] == '#' - - if self._isDate and self._autoformat: - # Auto-decide datestyle: - if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY' - elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD' - elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD' - elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY' - elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY' - - # Give derived controls a chance to react to parameter changes before - # potentially changing current value of the control. - self._OnCtrlParametersChanged() - - if self.controlInitialized: - # Then the base control is available for configuration; - # take action on base control based on new settings, as appropriate. - if kwargs.has_key('useFixedWidthFont'): - # Set control font - fixed width by default - self._setFont() - - if reset_args.has_key('reset_mask'): -## dbg('reset mask') - curvalue = self._GetValue() - if curvalue.strip(): - try: -## dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) - self._SetInitialValue(self._GetValue()) - except Exception, e: -## dbg('exception caught:', e) -## dbg("current value doesn't work; attempting to reset to template") - self._SetInitialValue() - else: -## dbg('attempting to _SetInitialValue() with template') - self._SetInitialValue() - - elif kwargs.has_key('useParensForNegatives'): - newvalue = self._getSignedValue()[0] - - if newvalue is not None: - # Adjust for new mask: - if len(newvalue) < len(self._mask): - newvalue += ' ' - elif len(newvalue) > len(self._mask): - if newvalue[-1] in (' ', ')'): - newvalue = newvalue[:-1] - -## dbg('reconfiguring value for parens:"%s"' % newvalue) - self._SetValue(newvalue) - - if self._prevValue != newvalue: - self._prevValue = newvalue # disallow undo of sign type - - if self._autofit: -## dbg('setting client size to:', self._CalcSize()) - self.SetClientSize(self._CalcSize()) - - # Set value/type-specific formatting - self._applyFormatting() -## dbg(indent=0, suspend=0) - - def SetMaskParameters(self, **kwargs): - """ old name for this function """ - return self.SetCtrlParameters(**kwargs) - - - def GetCtrlParameter(self, paramname): - """ - Routine for retrieving the value of any given parameter - """ - if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')): - return getattr(self, '_' + paramname.replace('Color', 'Colour')) - elif Field.valid_params.has_key(paramname): - return self._ctrl_constraints._GetParameter(paramname) - else: - TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname)) - - def GetMaskParameter(self, paramname): - """ old name for this function """ - return self.GetCtrlParameter(paramname) - - -## This idea worked, but Boa was unable to use this solution... -## def _attachMethod(self, func): -## import new -## setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) -## -## -## def _DefinePropertyFunctions(exposed_params): -## for param in exposed_params: -## propname = param[0].upper() + param[1:] -## -## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) -## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) -## self._attachMethod(locals()['Set%s' % propname]) -## self._attachMethod(locals()['Get%s' % propname]) -## -## if param.find('Colour') != -1: -## # add non-british spellings, for backward-compatibility -## propname.replace('Colour', 'Color') -## -## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) -## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) -## self._attachMethod(locals()['Set%s' % propname]) -## self._attachMethod(locals()['Get%s' % propname]) -## - - - def SetFieldParameters(self, field_index, **kwargs): - """ - Routine provided to modify the parameters of a given field. - Because changes to fields can affect the overall control, - direct access to the fields is prevented, and the control - is always "reconfigured" after setting a field parameter. - """ - if field_index not in self._field_indices: - raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) - # set parameters as requested: - self._fields[field_index]._SetParameters(**kwargs) - - # Possibly reprogram control template due to resulting changes, and ensure - # control-level params are still propagated to fields: - self._configure(self._previous_mask) - self._fields[field_index]._ValidateParameters(**kwargs) - - if self.controlInitialized: - if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'): - self._SetInitialValue() - - if self._autofit: - self.SetClientSize(self._CalcSize()) - - # Set value/type-specific formatting - self._applyFormatting() - - - def GetFieldParameter(self, field_index, paramname): - """ - Routine provided for getting a parameter of an individual field. - """ - if field_index not in self._field_indices: - raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) - elif Field.valid_params.has_key(paramname): - return self._fields[field_index]._GetParameter(paramname) - else: - TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname)) - - - def _SetKeycodeHandler(self, keycode, func): - """ - This function adds and/or replaces key event handling functions - used by the control. 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._decimalChar = self._ctrl_constraints._decimalChar - self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar - - self._isFloat = isFloatingPoint(s) and not self._ctrl_constraints._validRegex - self._isInt = isInteger(s) and not self._ctrl_constraints._validRegex - self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt) - self._useParens = self._ctrl_constraints._useParensForNegatives - self._isNeg = False -#### dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) -#### dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s), -## 'ctrl regex:', self._ctrl_constraints._validRegex) - - if self._signOk and s[0] != ' ': - s = ' ' + s - if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ': - self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue - self._signpos = 0 - - if self._useParens: - s += ' ' - self._ctrl_constraints._defaultValue += ' ' - - # Now, go build up a dictionary of booleans, indexed by position, - # indicating whether or not a given position is masked or not - ismasked = {} - i = 0 - while i < len(s): - if s[i] == '\\': # if escaped character: - ismasked[i] = False # mark position as not a mask char - if i+1 < len(s): # if another char follows... - s = s[:i] + s[i+1:] # elide the '\' - if i+2 < len(s) and s[i+1] == '\\': - # if next char also a '\', char is a literal '\' - s = s[:i] + s[i+1:] # elide the 2nd '\' as well - else: # else if special char, mark position accordingly - ismasked[i] = s[i] in maskchars -#### dbg('ismasked[%d]:' % i, ismasked[i], s) - i += 1 # increment to next char -#### dbg('ismasked:', ismasked) -## dbg('new mask: "%s"' % s, indent=0) - - return s, ismasked - - - def _calcFieldExtents(self): - """ - Subroutine responsible for establishing/configuring field instances with - indices and editable extents appropriate to the specified mask, and building - the lookup table mapping each position to the corresponding field. - """ - self._lookupField = {} - if self._mask: - - ## Create dictionary of positions,characters in mask - self.maskdict = {} - for charnum in range( len( self._mask)): - self.maskdict[charnum] = self._mask[charnum:charnum+1] - - # For the current mask, create an ordered list of field extents - # and a dictionary of positions that map to field indices: - - if self._signOk: start = 1 - else: start = 0 - - if self._isFloat: - # Skip field "discovery", and just construct a 2-field control with appropriate - # constraints for a floating-point entry. - - # .setdefault always constructs 2nd argument even if not needed, so we do this - # the old-fashioned way... - if not self._fields.has_key(0): - self._fields[0] = Field() - if not self._fields.has_key(1): - self._fields[1] = Field() - - self._decimalpos = string.find( self._mask, '.') -## dbg('decimal pos =', self._decimalpos) - - formatcodes = self._fields[0]._GetParameter('formatcodes') - if 'R' not in formatcodes: formatcodes += 'R' - self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos), - mask=self._mask[start:self._decimalpos], formatcodes=formatcodes) - end = len(self._mask) - if self._signOk and self._useParens: - end -= 1 - self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end), - mask=self._mask[self._decimalpos+1:end]) - - for i in range(self._decimalpos+1): - self._lookupField[i] = 0 - - for i in range(self._decimalpos+1, len(self._mask)+1): - self._lookupField[i] = 1 - - elif self._isInt: - # Skip field "discovery", and just construct a 1-field control with appropriate - # constraints for a integer entry. - if not self._fields.has_key(0): - self._fields[0] = Field(index=0) - end = len(self._mask) - if self._signOk and self._useParens: - end -= 1 - self._fields[0]._SetParameters(index=0, extent=(start, end), - mask=self._mask[start:end]) - for i in range(len(self._mask)+1): - self._lookupField[i] = 0 - else: - # generic control; parse mask to figure out where the fields are: - field_index = 0 - pos = 0 - i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point: - if i < len(self._mask): # no editable chars! - for j in range(pos, i+1): - self._lookupField[j] = field_index - pos = i # figure out field for 1st editable space: - - while i <= len(self._mask): -#### dbg('searching: outer field loop: i = ', i) - if self._isMaskChar(i): -#### dbg('1st char is mask char; recording edit_start=', i) - edit_start = i - # Skip to end of editable part of current field: - while i < len(self._mask) and self._isMaskChar(i): - self._lookupField[i] = field_index - i += 1 -#### dbg('edit_end =', i) - edit_end = i - self._lookupField[i] = field_index -#### dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) - if not self._fields.has_key(field_index): - kwargs = Field.valid_params.copy() - kwargs['index'] = field_index - kwargs['extent'] = (edit_start, edit_end) - kwargs['mask'] = self._mask[edit_start:edit_end] - self._fields[field_index] = Field(**kwargs) - else: - self._fields[field_index]._SetParameters( - index=field_index, - extent=(edit_start, edit_end), - mask=self._mask[edit_start:edit_end]) - pos = i - i = self._findNextEntry(pos, adjustInsert=False) # go to next field: - if i > pos: - for j in range(pos, i+1): - self._lookupField[j] = field_index - if i >= len(self._mask): - break # if past end, we're done - else: - field_index += 1 -#### dbg('next field:', field_index) - - indices = self._fields.keys() - indices.sort() - self._field_indices = indices[1:] -#### dbg('lookupField map:', indent=1) -## for i in range(len(self._mask)): -#### dbg('pos %d:' % i, self._lookupField[i]) -#### dbg(indent=0) - - # Verify that all field indices specified are valid for mask: - for index in self._fields.keys(): - if index not in [-1] + self._lookupField.values(): - raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask)) - - - def _calcTemplate(self, reset_fillchar, reset_default): - """ - Subroutine for processing current fillchars and default values for - whole control and individual fields, constructing the resulting - overall template, and adjusting the current value as necessary. - """ - default_set = False - if self._ctrl_constraints._defaultValue: - default_set = True - else: - for field in self._fields.values(): - if field._defaultValue and not reset_default: - default_set = True -## dbg('default set?', default_set) - - # Determine overall new template for control, and keep track of previous - # values, so that current control value can be modified as appropriate: - if self.controlInitialized: curvalue = list(self._GetValue()) - else: curvalue = None - - if hasattr(self, '_fillChar'): old_fillchars = self._fillChar - else: old_fillchars = None - - if hasattr(self, '_template'): old_template = self._template - else: old_template = None - - self._template = "" - - self._fillChar = {} - reset_value = False - - for field in self._fields.values(): - field._template = "" - - for pos in range(len(self._mask)): -#### dbg('pos:', pos) - field = self._FindField(pos) -#### dbg('field:', field._index) - start, end = field._extent - - if pos == 0 and self._signOk: - self._template = ' ' # always make 1st 1st position blank, regardless of fillchar - elif self._isFloat and pos == self._decimalpos: - self._template += self._decimalChar - elif self._isMaskChar(pos): - if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar: - fillChar = field._fillChar - else: - fillChar = self._ctrl_constraints._fillChar - self._fillChar[pos] = fillChar - - # Replace any current old fillchar with new one in current value; - # if action required, set reset_value flag so we can take that action - # after we're all done - if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue: - if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar: - reset_value = True - curvalue[pos] = fillChar - - if not field._defaultValue and not self._ctrl_constraints._defaultValue: -#### dbg('no default value') - self._template += fillChar - field._template += fillChar - - elif field._defaultValue and not reset_default: -#### dbg('len(field._defaultValue):', len(field._defaultValue)) -#### dbg('pos-start:', pos-start) - if len(field._defaultValue) > pos-start: -#### dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) - self._template += field._defaultValue[pos-start] - field._template += field._defaultValue[pos-start] - else: -#### dbg('field default not long enough; using fillChar') - self._template += fillChar - field._template += fillChar - else: - if len(self._ctrl_constraints._defaultValue) > pos: -#### dbg('using control default') - self._template += self._ctrl_constraints._defaultValue[pos] - field._template += self._ctrl_constraints._defaultValue[pos] - else: -#### dbg('ctrl default not long enough; using fillChar') - self._template += fillChar - field._template += fillChar -#### dbg('field[%d]._template now "%s"' % (field._index, field._template)) -#### dbg('self._template now "%s"' % self._template) - else: - self._template += self._mask[pos] - - self._fields[-1]._template = self._template # (for consistency) - - if curvalue: # had an old value, put new one back together - newvalue = string.join(curvalue, "") - else: - newvalue = None - - if default_set: - self._defaultValue = self._template -## dbg('self._defaultValue:', self._defaultValue) - if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): -#### dbg(indent=0) - raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name)) - - # if no fillchar change, but old value == old template, replace it: - if newvalue == old_template: - newvalue = self._template - reset_value = True - else: - self._defaultValue = None - - if reset_value: -## dbg('resetting value to: "%s"' % newvalue) - pos = self._GetInsertionPoint() - sel_start, sel_to = self._GetSelection() - self._SetValue(newvalue) - self._SetInsertionPoint(pos) - self._SetSelection(sel_start, sel_to) - - - def _propagateConstraints(self, **reset_args): - """ - Subroutine for propagating changes to control-level constraints and - formatting to the individual fields as appropriate. - """ - parent_codes = self._ctrl_constraints._formatcodes - parent_includes = self._ctrl_constraints._includeChars - parent_excludes = self._ctrl_constraints._excludeChars - for i in self._field_indices: - field = self._fields[i] - inherit_args = {} - if len(self._field_indices) == 1: - inherit_args['formatcodes'] = parent_codes - inherit_args['includeChars'] = parent_includes - inherit_args['excludeChars'] = parent_excludes - else: - field_codes = current_codes = field._GetParameter('formatcodes') - for c in parent_codes: - if c not in field_codes: field_codes += c - if field_codes != current_codes: - inherit_args['formatcodes'] = field_codes - - include_chars = current_includes = field._GetParameter('includeChars') - for c in parent_includes: - if not c in include_chars: include_chars += c - if include_chars != current_includes: - inherit_args['includeChars'] = include_chars - - exclude_chars = current_excludes = field._GetParameter('excludeChars') - for c in parent_excludes: - if not c in exclude_chars: exclude_chars += c - if exclude_chars != current_excludes: - inherit_args['excludeChars'] = exclude_chars - - if reset_args.has_key('defaultValue') and reset_args['defaultValue']: - inherit_args['defaultValue'] = "" # (reset for field) - - for param in Field.propagating_params: -#### dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) -#### dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) - if reset_args.has_key(param): - inherit_args[param] = self.GetCtrlParameter(param) -#### dbg('inherit_args[%s]' % param, inherit_args[param]) - - if inherit_args: - field._SetParameters(**inherit_args) - field._ValidateParameters(**inherit_args) - - - def _validateChoices(self): - """ - Subroutine that validates that all choices for given fields are at - least of the necessary length, and that they all would be valid pastes - if pasted into their respective fields. - """ - for field in self._fields.values(): - if field._choices: - index = field._index - if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices: -## dbg('skipping (duplicate) choice validation of field 0') - continue -#### dbg('checking for choices for field', field._index) - start, end = field._extent - field_length = end - start -#### dbg('start, end, length:', start, end, field_length) - for choice in field._choices: -#### dbg('testing "%s"' % choice) - valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) - if not valid_paste: -#### dbg(indent=0) - raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name)) - elif replace_to > end: -#### dbg(indent=0) - raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name)) -#### dbg(choice, 'valid in field', index) - - - def _configure(self, mask, **reset_args): - """ - This function sets flags for automatic styling options. It is - called whenever a control or field-level parameter is set/changed. - - This routine does the bulk of the interdependent parameter processing, determining - the field extents of the mask if changed, resetting parameters as appropriate, - determining the overall template value for the control, etc. - - reset_args is supplied if called from control's .SetCtrlParameters() - routine, and indicates which if any parameters which can be - overridden by individual fields have been reset by request for the - whole control. - - """ -## dbg(suspend=1) -## dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) - - # Preprocess specified mask to expand {n} syntax, handle escaped - # mask characters, etc and build the resulting positionally keyed - # dictionary for which positions are mask vs. template characters: - self._mask, self.ismasked = self._processMask(mask) - self._masklength = len(self._mask) -#### dbg('processed mask:', self._mask) - - # Preserve original mask specified, for subsequent reprocessing - # if parameters change. -## dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) - self._previous_mask = mask # save unexpanded mask for next time - # Set expanded mask and extent of field -1 to width of entire control: - self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength)) - - # Go parse mask to determine where each field is, construct field - # instances as necessary, configure them with those extents, and - # build lookup table mapping each position for control to its corresponding - # field. -#### dbg('calculating field extents') - - self._calcFieldExtents() - - - # Go process defaultValues and fillchars to construct the overall - # template, and adjust the current value as necessary: - reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar'] - reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue'] - -#### dbg('calculating template') - self._calcTemplate(reset_fillchar, reset_default) - - # Propagate control-level formatting and character constraints to each - # field if they don't already have them; if only one field, propagate - # control-level validation constraints to field as well: -#### dbg('propagating constraints') - self._propagateConstraints(**reset_args) - - - if self._isFloat and self._fields[0]._groupChar == self._decimalChar: - raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % - (self._fields[0]._groupChar, self._decimalChar) ) - -#### dbg('fields:', indent=1) -## for i in [-1] + self._field_indices: -#### dbg('field %d:' % i, self._fields[i].__dict__) -#### dbg(indent=0) - - # Set up special parameters for numeric control, if appropriate: - if self._signOk: - self._signpos = 0 # assume it starts here, but it will move around on floats - signkeys = ['-', '+', ' '] - if self._useParens: - signkeys += ['(', ')'] - for key in signkeys: - keycode = ord(key) - if not self._keyhandlers.has_key(keycode): - self._SetKeyHandler(key, self._OnChangeSign) - - - - if self._isFloat or self._isInt: - if self.controlInitialized: - value = self._GetValue() -#### dbg('value: "%s"' % value, 'len(value):', len(value), -## 'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) - if len(value) < len(self._ctrl_constraints._mask): - newvalue = value - if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1: - newvalue += ' ' - if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1: - newvalue = ' ' + newvalue - if len(newvalue) < len(self._ctrl_constraints._mask): - if self._ctrl_constraints._alignRight: - newvalue = newvalue.rjust(len(self._ctrl_constraints._mask)) - else: - newvalue = newvalue.ljust(len(self._ctrl_constraints._mask)) -## dbg('old value: "%s"' % value) -## dbg('new value: "%s"' % newvalue) - try: - self._SetValue(newvalue) - except Exception, e: -## dbg('exception raised:', e, 'resetting to initial value') - self._SetInitialValue() - - elif len(value) > len(self._ctrl_constraints._mask): - newvalue = value - if not self._useParens and newvalue[-1] == ' ': - newvalue = newvalue[:-1] - if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask): - newvalue = newvalue[1:] - if not self._signOk: - newvalue, signpos, right_signpos = self._getSignedValue(newvalue) - -## dbg('old value: "%s"' % value) -## dbg('new value: "%s"' % newvalue) - try: - self._SetValue(newvalue) - except Exception, e: -## dbg('exception raised:', e, 'resetting to initial value') - self._SetInitialValue() - elif not self._signOk and ('(' in value or '-' in value): - newvalue, signpos, right_signpos = self._getSignedValue(value) -## dbg('old value: "%s"' % value) -## dbg('new value: "%s"' % newvalue) - try: - self._SetValue(newvalue) - except e: -## dbg('exception raised:', e, 'resetting to initial value') - self._SetInitialValue() - - # Replace up/down arrow default handling: - # make down act like tab, up act like shift-tab: - -#### dbg('Registering numeric navigation and control handlers (if not already set)') - if not self._keyhandlers.has_key(wx.WXK_DOWN): - self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) - if not self._keyhandlers.has_key(wx.WXK_UP): - self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) - - # On ., truncate contents right of cursor to decimal point (if any) - # leaves cusor after decimal point if floating point, otherwise at 0. - if not self._keyhandlers.has_key(ord(self._decimalChar)): - self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint) - if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)): - self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards) - - # Allow selective insert of groupchar in numbers: - if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)): - self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar) - -## dbg(indent=0, suspend=0) - - - def _SetInitialValue(self, value=""): - """ - fills the control with the generated or supplied default value. - It will also set/reset the font if necessary and apply - formatting to the control at this time. - """ -## dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) - if not value: - self._prevValue = self._curValue = self._template - # don't apply external validation rules in this case, as template may - # not coincide with "legal" value... - try: - self._SetValue(self._curValue) # note the use of "raw" ._SetValue()... - except Exception, e: -## dbg('exception thrown:', e, indent=0) - raise - else: - # Otherwise apply validation as appropriate to passed value: -#### dbg('value = "%s", length:' % value, len(value)) - self._prevValue = self._curValue = value - try: - self.SetValue(value) # use public (validating) .SetValue() - except Exception, e: -## dbg('exception thrown:', e, indent=0) - raise - - - # Set value/type-specific formatting - self._applyFormatting() -## dbg(indent=0) - - - def _calcSize(self, size=None): - """ Calculate automatic size if allowed; must be called after the base control is instantiated""" -#### dbg('MaskedEditMixin::_calcSize', indent=1) - cont = (size is None or size == wx.DefaultSize) - - if cont and self._autofit: - sizing_text = 'M' * self._masklength - if wx.Platform != "__WXMSW__": # give it a little extra space - sizing_text += 'M' - if wx.Platform == "__WXMAC__": # give it even a little more... - sizing_text += 'M' -#### dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) - w, h = self.GetTextExtent(sizing_text) - size = (w+4, self.GetClientSize().height) -#### dbg('size:', size, indent=0) - return size - - - def _setFont(self): - """ Set the control's font typeface -- pass the font name as str.""" -#### dbg('MaskedEditMixin::_setFont', indent=1) - if not self._useFixedWidthFont: - self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) - else: - font = self.GetFont() # get size, weight, etc from current font - - # Set to teletype font (guaranteed to be mappable to all wxWidgets - # platforms: - self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(), - font.GetWeight(), font.GetUnderlined()) -#### dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) - - self.SetFont(self._font) -#### dbg(indent=0) - - - def _OnTextChange(self, event): - """ - Handler for EVT_TEXT event. - self._Change() is provided for subclasses, and may return False to - skip this method logic. This function returns True if the event - detected was a legitimate event, or False if it was a "bogus" - EVT_TEXT event. (NOTE: There is currently an issue with calling - .SetValue from within the EVT_CHAR handler that causes duplicate - EVT_TEXT events for the same change.) - """ - newvalue = self._GetValue() -## dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) - bValid = False - if self._ignoreChange: # ie. if an "intermediate text change event" -## dbg(indent=0) - return bValid - - ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue - ## call is generating two (2) EVT_TEXT events. - ## This is the only mechanism I can find to mask this problem: - if newvalue == self._curValue: -## dbg('ignoring bogus text change event', indent=0) - pass - else: -## dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue)) - if self._Change(): - if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1: -## dbg('clearing self._isNeg') - self._isNeg = False - text, self._signpos, self._right_signpos = self._getSignedValue() - self._CheckValid() # Recolor control as appropriate -## dbg('calling event.Skip()') - event.Skip() - bValid = True - self._prevValue = self._curValue # save for undo - self._curValue = newvalue # Save last seen value for next iteration -## dbg(indent=0) - return bValid - - - def _OnKeyDown(self, event): - """ - This function allows the control to capture Ctrl-events like Ctrl-tab, - that are not normally seen by the "cooked" EVT_CHAR routine. - """ - # Get keypress value, adjusted by control options (e.g. convert to upper etc) - key = event.GetKeyCode() - if key in self._nav and event.ControlDown(): - # then this is the only place we will likely see these events; - # process them now: -## dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') - self._OnChar(event) - return - # else allow regular EVT_CHAR key processing - event.Skip() - - - def _OnChar(self, event): - """ - This is the engine of wxMaskedEdit controls. It examines each keystroke, - decides if it's allowed, where it should go or what action to take. - """ -## dbg('MaskedEditMixin::_OnChar', indent=1) - - # Get keypress value, adjusted by control options (e.g. convert to upper etc) - key = event.GetKeyCode() - orig_pos = self._GetInsertionPoint() - orig_value = self._GetValue() -## dbg('keycode = ', key) -## dbg('current pos = ', orig_pos) -## dbg('current selection = ', self._GetSelection()) - - if not self._Keypress(key): -## dbg(indent=0) - return - - # If no format string for this control, or the control is marked as "read-only", - # skip the rest of the special processing, and just "do the standard thing:" - if not self._mask or not self._IsEditable(): - event.Skip() -## dbg(indent=0) - return - - # Process navigation and control keys first, with - # position/selection unadulterated: - if key in self._nav + self._control: - if self._keyhandlers.has_key(key): - keep_processing = self._keyhandlers[key](event) - if self._GetValue() != orig_value: - self.modified = True - if not keep_processing: -## dbg(indent=0) - return - self._applyFormatting() -## dbg(indent=0) - return - - # Else... adjust the position as necessary for next input key, - # and determine resulting selection: - pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed - sel_start, sel_to = self._GetSelection() ## check for a range of selected text -## dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) - - keep_processing = True - # Capture user past end of format field - if pos > len(self.maskdict): -## dbg("field length exceeded:",pos) - keep_processing = False - - if keep_processing: - if self._isMaskChar(pos): ## Get string of allowed characters for validation - okchars = self._getAllowedChars(pos) - else: -## dbg('Not a valid position: pos = ', pos,"chars=",maskchars) - okchars = "" - - key = self._adjustKey(pos, key) # apply formatting constraints to key: - - if self._keyhandlers.has_key(key): - # there's an override for default behavior; use override function instead -## dbg('using supplied key handler:', self._keyhandlers[key]) - keep_processing = self._keyhandlers[key](event) - if self._GetValue() != orig_value: - self.modified = True - if not keep_processing: -## dbg(indent=0) - return - # else skip default processing, but do final formatting - if key < wx.WXK_SPACE or key > 255: -## dbg('key < WXK_SPACE or key > 255') - event.Skip() # non alphanumeric - keep_processing = False - else: - field = self._FindField(pos) -## dbg("key ='%s'" % chr(key)) - if chr(key) == ' ': -## dbg('okSpaces?', field._okSpaces) - pass - - - if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars: - keep_processing = False - - if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ): -## dbg("key allowed by mask") - # insert key into candidate new value, but don't change control yet: - oldstr = self._GetValue() - newstr, newpos, new_select_to, match_field, match_index = self._insertKey( - chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True) -## dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr) - if self._ctrl_constraints._validRequired and not self.IsValid(newstr): -## dbg('not valid; checking to see if adjusted string is:') - keep_processing = False - if self._isFloat and newstr != self._template: - newstr = self._adjustFloat(newstr) -## dbg('adjusted str:', newstr) - if self.IsValid(newstr): -## dbg("it is!") - keep_processing = True - wx.CallAfter(self._SetInsertionPoint, self._decimalpos) - if not keep_processing: -## dbg("key disallowed by validation") - if not wx.Validator_IsSilent() and orig_pos == pos: - wx.Bell() - - if keep_processing: - unadjusted = newstr - - # special case: adjust date value as necessary: - if self._isDate and newstr != self._template: - newstr = self._adjustDate(newstr) -## dbg('adjusted newstr:', newstr) - - if newstr != orig_value: - self.modified = True - - wx.CallAfter(self._SetValue, newstr) - - # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: - if not self.IsDefault() and self._isDate and self._4digityear: - year2dig = self._dateExtent - 2 - if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]: - newpos = pos+2 - - wx.CallAfter(self._SetInsertionPoint, newpos) - - if match_field is not None: -## dbg('matched field') - self._OnAutoSelect(match_field, match_index) - - if new_select_to != newpos: -## dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) - wx.CallAfter(self._SetSelection, newpos, new_select_to) - else: - newfield = self._FindField(newpos) - if newfield != field and newfield._selectOnFieldEntry: -## dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) - wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1]) - keep_processing = False - - elif keep_processing: -## dbg('char not allowed') - keep_processing = False - if (not wx.Validator_IsSilent()) and orig_pos == pos: - wx.Bell() - - self._applyFormatting() - - # Move to next insertion point - if keep_processing and key not in self._nav: - pos = self._GetInsertionPoint() - next_entry = self._findNextEntry( pos ) - if pos != next_entry: -## dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) - wx.CallAfter(self._SetInsertionPoint, next_entry ) - - if self._isTemplateChar(pos): - self._AdjustField(pos) -## dbg(indent=0) - - - def _FindFieldExtent(self, pos=None, getslice=False, value=None): - """ returns editable extent of field corresponding to - position pos, and, optionally, the contents of that field - in the control or the value specified. - Template chars are bound to the preceding field. - For masks beginning with template chars, these chars are ignored - when calculating the current field. - - Eg: with template (###) ###-####, - >>> self._FindFieldExtent(pos=0) - 1, 4 - >>> self._FindFieldExtent(pos=1) - 1, 4 - >>> self._FindFieldExtent(pos=5) - 1, 4 - >>> self._FindFieldExtent(pos=6) - 6, 9 - >>> self._FindFieldExtent(pos=10) - 10, 14 - etc. - """ -## dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) - - field = self._FindField(pos) - if not field: - if getslice: - return None, None, "" - else: - return None, None - edit_start, edit_end = field._extent - if getslice: - if value is None: value = self._GetValue() - slice = value[edit_start:edit_end] -## dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) -## dbg(indent=0) - return edit_start, edit_end, slice - else: -## dbg('edit_start:', edit_start, 'edit_end:', edit_end) -## dbg(indent=0) - return edit_start, edit_end - - - def _FindField(self, pos=None): - """ - Returns the field instance in which pos resides. - Template chars are bound to the preceding field. - For masks beginning with template chars, these chars are ignored - when calculating the current field. - - """ -#### dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) - if pos is None: pos = self._GetInsertionPoint() - elif pos < 0 or pos > self._masklength: - raise IndexError('position %s out of range of control' % str(pos)) - - if len(self._fields) == 0: -## dbg(indent=0) - return None - - # else... -#### dbg(indent=0) - return self._fields[self._lookupField[pos]] - - - def ClearValue(self): - """ Blanks the current control value by replacing it with the default value.""" -## dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") - self._SetValue( self._template ) - self._SetInsertionPoint(0) - self.Refresh() - - - def _baseCtrlEventHandler(self, event): - """ - This function is used whenever a key should be handled by the base control. - """ - event.Skip() - return False - - - def _OnUpNumeric(self, event): - """ - Makes up-arrow act like shift-tab should; ie. take you to start of - previous field. - """ -## dbg('MaskedEditMixin::_OnUpNumeric', indent=1) - event.m_shiftDown = 1 -## dbg('event.ShiftDown()?', event.ShiftDown()) - self._OnChangeField(event) -## dbg(indent=0) - - - def _OnArrow(self, event): - """ - Used in response to left/right navigation keys; makes these actions skip - over mask template chars. - """ -## dbg("MaskedEditMixin::_OnArrow", indent=1) - pos = self._GetInsertionPoint() - keycode = event.GetKeyCode() - sel_start, sel_to = self._GetSelection() - entry_end = self._goEnd(getPosOnly=True) - if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN): - if( ( not self._isTemplateChar(pos) and pos+1 > entry_end) - or ( self._isTemplateChar(pos) and pos >= entry_end) ): -## dbg("can't advance", indent=0) - return False - elif self._isTemplateChar(pos): - self._AdjustField(pos) - elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): -## dbg('adjusting field') - self._AdjustField(pos) - - # treat as shifted up/down arrows as tab/reverse tab: - if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN): - # remove "shifting" and treat as (forward) tab: - event.m_shiftDown = False - keep_processing = self._OnChangeField(event) - - elif self._FindField(pos)._selectOnFieldEntry: - if( keycode in (wx.WXK_UP, wx.WXK_LEFT) - and sel_start != 0 - and self._isTemplateChar(sel_start-1) - and sel_start != self._masklength - and not self._signOk and not self._useParens): - - # call _OnChangeField to handle "ctrl-shifted event" - # (which moves to previous field and selects it.) - event.m_shiftDown = True - event.m_ControlDown = True - keep_processing = self._OnChangeField(event) - elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT) - and sel_to != self._masklength - and self._isTemplateChar(sel_to)): - - # when changing field to the right, ensure don't accidentally go left instead - event.m_shiftDown = False - keep_processing = self._OnChangeField(event) - else: - # treat arrows as normal, allowing selection - # as appropriate: -## dbg('using base ctrl event processing') - event.Skip() - else: - if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT) - or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ): - if not wx.Validator_IsSilent(): - wx.Bell() - else: - # treat arrows as normal, allowing selection - # as appropriate: -## dbg('using base event processing') - event.Skip() - - keep_processing = False -## dbg(indent=0) - return keep_processing - - - def _OnCtrl_S(self, event): - """ Default Ctrl-S handler; prints value information if demo enabled. """ -## dbg("MaskedEditMixin::_OnCtrl_S") - if self._demo: - print 'MaskedEditMixin.GetValue() = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue()) - print "Valid? => " + str(self.IsValid()) - print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True)) - return False - - - def _OnCtrl_X(self, event=None): - """ Handles ctrl-x keypress in control and Cut operation on context menu. - Should return False to skip other processing. """ -## dbg("MaskedEditMixin::_OnCtrl_X", indent=1) - self.Cut() -## dbg(indent=0) - return False - - def _OnCtrl_C(self, event=None): - """ Handles ctrl-C keypress in control and Copy operation on context menu. - Uses base control handling. Should return False to skip other processing.""" - self.Copy() - return False - - def _OnCtrl_V(self, event=None): - """ Handles ctrl-V keypress in control and Paste operation on context menu. - Should return False to skip other processing. """ -## dbg("MaskedEditMixin::_OnCtrl_V", indent=1) - self.Paste() -## dbg(indent=0) - return False - - def _OnCtrl_Z(self, event=None): - """ Handles ctrl-Z keypress in control and Undo operation on context menu. - Should return False to skip other processing. """ -## dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) - self.Undo() -## dbg(indent=0) - return False - - def _OnCtrl_A(self,event=None): - """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ - end = self._goEnd(getPosOnly=True) - if not event or event.ShiftDown(): - wx.CallAfter(self._SetInsertionPoint, 0) - wx.CallAfter(self._SetSelection, 0, self._masklength) - else: - wx.CallAfter(self._SetInsertionPoint, 0) - wx.CallAfter(self._SetSelection, 0, end) - return False - - - def _OnErase(self, event=None): - """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" -## dbg("MaskedEditMixin::_OnErase", indent=1) - sel_start, sel_to = self._GetSelection() ## check for a range of selected text - - if event is None: # called as action routine from Cut() operation. - key = wx.WXK_DELETE - else: - key = event.GetKeyCode() - - field = self._FindField(sel_to) - start, end = field._extent - value = self._GetValue() - oldstart = sel_start - - # If trying to erase beyond "legal" bounds, disallow operation: - if( (sel_to == 0 and key == wx.WXK_BACK) - or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK) - or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight) - or (self._signOk and self._useParens - and sel_start == sel_to - and sel_to == self._masklength - 1 - and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ): - if not wx.Validator_IsSilent(): - wx.Bell() -## dbg(indent=0) - return False - - - if( field._insertRight # an insert-right field - and value[start:end] != self._template[start:end] # and field not empty - and sel_start >= start # and selection starts in field - and ((sel_to == sel_start # and no selection - and sel_to == end # and cursor at right edge - and key in (wx.WXK_BACK, wx.WXK_DELETE)) # and either delete or backspace key - or # or - (key == wx.WXK_BACK # backspacing - and (sel_to == end # and selection ends at right edge - or sel_to < end and field._allowInsert)) ) ): # or allow right insert at any point in field - -## dbg('delete left') - # if backspace but left of cursor is empty, adjust cursor right before deleting - while( key == wx.WXK_BACK - and sel_start == sel_to - and sel_start < end - and value[start:sel_start] == self._template[start:sel_start]): - sel_start += 1 - sel_to = sel_start - -## dbg('sel_start, start:', sel_start, start) - - if sel_start == sel_to: - keep = sel_start -1 - else: - keep = sel_start - newfield = value[start:keep] + value[sel_to:end] - - # handle sign char moving from outside field into the field: - move_sign_into_field = False - if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('): - signchar = value[0] - newfield = signchar + newfield - move_sign_into_field = True -## dbg('cut newfield: "%s"' % newfield) - - # handle what should fill in from the left: - left = "" - for i in range(start, end - len(newfield)): - if field._padZero: - left += '0' - elif( self._signOk and self._isNeg and i == 1 - and ((self._useParens and newfield.find('(') == -1) - or (not self._useParens and newfield.find('-') == -1)) ): - left += ' ' - else: - left += self._template[i] # this can produce strange results in combination with default values... - newfield = left + newfield -## dbg('filled newfield: "%s"' % newfield) - - newstr = value[:start] + newfield + value[end:] - - # (handle sign located in "mask position" in front of field prior to delete) - if move_sign_into_field: - newstr = ' ' + newstr[1:] - pos = sel_to - else: - # handle erasure of (left) sign, moving selection accordingly... - if self._signOk and sel_start == 0: - newstr = value = ' ' + value[1:] - sel_start += 1 - - if field._allowInsert and sel_start >= start: - # selection (if any) falls within current insert-capable field: - select_len = sel_to - sel_start - # determine where cursor should end up: - if key == wx.WXK_BACK: - if select_len == 0: - newpos = sel_start -1 - else: - newpos = sel_start - erase_to = sel_to - else: - newpos = sel_start - if sel_to == sel_start: - erase_to = sel_to + 1 - else: - erase_to = sel_to - - if self._isTemplateChar(newpos) and select_len == 0: - if self._signOk: - if value[newpos] in ('(', '-'): - newpos += 1 # don't move cusor - newstr = ' ' + value[newpos:] - elif value[newpos] == ')': - # erase right sign, but don't move cursor; (matching left sign handled later) - newstr = value[:newpos] + ' ' - else: - # no deletion; just move cursor - newstr = value - else: - # no deletion; just move cursor - newstr = value - else: - if erase_to > end: erase_to = end - erase_len = erase_to - newpos - - left = value[start:newpos] -## dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) - right = value[erase_to:end] + self._template[end-erase_len:end] - pos_adjust = 0 - if field._alignRight: - rstripped = right.rstrip() - if rstripped != right: - pos_adjust = len(right) - len(rstripped) - right = rstripped - - if not field._insertRight and value[-1] == ')' and end == self._masklength - 1: - # need to shift ) into the field: - right = right[:-1] + ')' - value = value[:-1] + ' ' - - newfield = left+right - if pos_adjust: - newfield = newfield.rjust(end-start) - newpos += pos_adjust -## dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) - newstr = value[:start] + newfield + value[end:] - - pos = newpos - - else: - if sel_start == sel_to: -## dbg("current sel_start, sel_to:", sel_start, sel_to) - if key == wx.WXK_BACK: - sel_start, sel_to = sel_to-1, sel_to-1 -## dbg("new sel_start, sel_to:", sel_start, sel_to) - - if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''): - # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: - newchar = '0' - else: - newchar = self._template[sel_to] ## get an original template character to "clear" the current char -## dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) - - if self._isTemplateChar(sel_to): - if sel_to == 0 and self._signOk and value[sel_to] == '-': # erasing "template" sign char - newstr = ' ' + value[1:] - sel_to += 1 - elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('): - # allow "change sign" by removing both parens: - newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' ' - else: - newstr = value - newpos = sel_to - else: - if field._insertRight and sel_start == sel_to: - # force non-insert-right behavior, by selecting char to be replaced: - sel_to += 1 - newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value) - - else: - # selection made - newstr = self._eraseSelection(value, sel_start, sel_to) - - pos = sel_start # put cursor back at beginning of selection - - if self._signOk and self._useParens: - # account for resultant unbalanced parentheses: - left_signpos = newstr.find('(') - right_signpos = newstr.find(')') - - if left_signpos == -1 and right_signpos != -1: - # erased left-sign marker; get rid of right sign marker: - newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:] - - elif left_signpos != -1 and right_signpos == -1: - # erased right-sign marker; get rid of left-sign marker: - newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:] - -## dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) -## dbg("newstr:'%s'" % newstr, 'pos:', pos) - - # if erasure results in an invalid field, disallow it: -## dbg('field._validRequired?', field._validRequired) -## dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) - if field._validRequired and not field.IsValid(newstr[start:end]): - if not wx.Validator_IsSilent(): - wx.Bell() -## dbg(indent=0) - return False - - # if erasure results in an invalid value, disallow it: - if self._ctrl_constraints._validRequired and not self.IsValid(newstr): - if not wx.Validator_IsSilent(): - wx.Bell() -## dbg(indent=0) - return False - -## dbg('setting value (later) to', newstr) - wx.CallAfter(self._SetValue, newstr) -## dbg('setting insertion point (later) to', pos) - wx.CallAfter(self._SetInsertionPoint, pos) -## dbg(indent=0) - if newstr != value: - self.modified = True - return False - - - def _OnEnd(self,event): - """ Handles End keypress in control. Should return False to skip other processing. """ -## dbg("MaskedEditMixin::_OnEnd", indent=1) - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - if not event.ControlDown(): - end = self._masklength # go to end of control - if self._signOk and self._useParens: - end = end - 1 # account for reserved char at end - else: - end_of_input = self._goEnd(getPosOnly=True) - sel_start, sel_to = self._GetSelection() - if sel_to < pos: sel_to = pos - field = self._FindField(sel_to) - field_end = self._FindField(end_of_input) - - # pick different end point if either: - # - cursor not in same field - # - or at or past last input already - # - or current selection = end of current field: -#### dbg('field != field_end?', field != field_end) -#### dbg('sel_to >= end_of_input?', sel_to >= end_of_input) - if field != field_end or sel_to >= end_of_input: - edit_start, edit_end = field._extent -#### dbg('edit_end:', edit_end) -#### dbg('sel_to:', sel_to) -#### dbg('sel_to == edit_end?', sel_to == edit_end) -#### dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) - - if sel_to == edit_end and field._index < self._field_indices[-1]: - edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end)) # go to end of next field: - end = edit_end -## dbg('end moved to', end) - - elif sel_to == edit_end and field._index == self._field_indices[-1]: - # already at edit end of last field; select to end of control: - end = self._masklength -## dbg('end moved to', end) - else: - end = edit_end # select to end of current field -## dbg('end moved to ', end) - else: - # select to current end of input - end = end_of_input - - -#### dbg('pos:', pos, 'end:', end) - - if event.ShiftDown(): - if not event.ControlDown(): -## dbg("shift-end; select to end of control") - pass - else: -## dbg("shift-ctrl-end; select to end of non-whitespace") - pass - wx.CallAfter(self._SetInsertionPoint, pos) - wx.CallAfter(self._SetSelection, pos, end) - else: - if not event.ControlDown(): -## dbg('go to end of control:') - pass - wx.CallAfter(self._SetInsertionPoint, end) - wx.CallAfter(self._SetSelection, end, end) - -## dbg(indent=0) - return False - - - def _OnReturn(self, event): - """ - Changes the event to look like a tab event, so we can then call - event.Skip() on it, and have the parent form "do the right thing." - """ -## dbg('MaskedEditMixin::OnReturn') - event.m_keyCode = wx.WXK_TAB - event.Skip() - - - def _OnHome(self,event): - """ Handles Home keypress in control. Should return False to skip other processing.""" -## dbg("MaskedEditMixin::_OnHome", indent=1) - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - sel_start, sel_to = self._GetSelection() - - # There are 5 cases here: - - # 1) shift: select from start of control to end of current - # selection. - if event.ShiftDown() and not event.ControlDown(): -## dbg("shift-home; select to start of control") - start = 0 - end = sel_start - - # 2) no shift, no control: move cursor to beginning of control. - elif not event.ControlDown(): -## dbg("home; move to start of control") - start = 0 - end = 0 - - # 3) No shift, control: move cursor back to beginning of field; if - # there already, go to beginning of previous field. - # 4) shift, control, start of selection not at beginning of control: - # move sel_start back to start of field; if already there, go to - # start of previous field. - elif( event.ControlDown() - and (not event.ShiftDown() - or (event.ShiftDown() and sel_start > 0) ) ): - if len(self._field_indices) > 1: - field = self._FindField(sel_start) - start, ignore = field._extent - if sel_start == start and field._index != self._field_indices[0]: # go to start of previous field: - start, ignore = self._FindFieldExtent(sel_start-1) - elif sel_start == start: - start = 0 # go to literal beginning if edit start - # not at that point - end_of_field = True - - else: - start = 0 - - if not event.ShiftDown(): -## dbg("ctrl-home; move to beginning of field") - end = start - else: -## dbg("shift-ctrl-home; select to beginning of field") - end = sel_to - - else: - # 5) shift, control, start of selection at beginning of control: - # unselect by moving sel_to backward to beginning of current field; - # if already there, move to start of previous field. - start = sel_start - if len(self._field_indices) > 1: - # find end of previous field: - field = self._FindField(sel_to) - if sel_to > start and field._index != self._field_indices[0]: - ignore, end = self._FindFieldExtent(field._extent[0]-1) - else: - end = start - end_of_field = True - else: - end = start - end_of_field = False -## dbg("shift-ctrl-home; unselect to beginning of field") - -## dbg('queuing new sel_start, sel_to:', (start, end)) - wx.CallAfter(self._SetInsertionPoint, start) - wx.CallAfter(self._SetSelection, start, end) -## dbg(indent=0) - return False - - - def _OnChangeField(self, event): - """ - Primarily handles TAB events, but can be used for any key that - designer wants to change fields within a masked edit control. - NOTE: at the moment, although coded to handle shift-TAB and - control-shift-TAB, these events are not sent to the controls - by the framework. - """ -## dbg('MaskedEditMixin::_OnChangeField', indent = 1) - # determine end of current field: - pos = self._GetInsertionPoint() -## dbg('current pos:', pos) - sel_start, sel_to = self._GetSelection() - - if self._masklength < 0: # no fields; process tab normally - self._AdjustField(pos) - if event.GetKeyCode() == wx.WXK_TAB: -## dbg('tab to next ctrl') - event.Skip() - #else: do nothing -## dbg(indent=0) - return False - - - if event.ShiftDown(): - - # "Go backward" - - # NOTE: doesn't yet work with SHIFT-tab under wx; the control - # never sees this event! (But I've coded for it should it ever work, - # and it *does* work for '.' in IpAddrCtrl.) - field = self._FindField(pos) - index = field._index - field_start = field._extent[0] - if pos < field_start: -## dbg('cursor before 1st field; cannot change to a previous field') - if not wx.Validator_IsSilent(): - wx.Bell() - return False - - if event.ControlDown(): -## dbg('queuing select to beginning of field:', field_start, pos) - wx.CallAfter(self._SetInsertionPoint, field_start) - wx.CallAfter(self._SetSelection, field_start, pos) -## dbg(indent=0) - return False - - elif index == 0: - # We're already in the 1st field; process shift-tab normally: - self._AdjustField(pos) - if event.GetKeyCode() == wx.WXK_TAB: -## dbg('tab to previous ctrl') - event.Skip() - else: -## dbg('position at beginning') - wx.CallAfter(self._SetInsertionPoint, field_start) -## dbg(indent=0) - return False - else: - # find beginning of previous field: - begin_prev = self._FindField(field_start-1)._extent[0] - self._AdjustField(pos) -## dbg('repositioning to', begin_prev) - wx.CallAfter(self._SetInsertionPoint, begin_prev) - if self._FindField(begin_prev)._selectOnFieldEntry: - edit_start, edit_end = self._FindFieldExtent(begin_prev) -## dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) - wx.CallAfter(self._SetInsertionPoint, edit_start) - wx.CallAfter(self._SetSelection, edit_start, edit_end) -## dbg(indent=0) - return False - - else: - # "Go forward" - field = self._FindField(sel_to) - field_start, field_end = field._extent - if event.ControlDown(): -## dbg('queuing select to end of field:', pos, field_end) - wx.CallAfter(self._SetInsertionPoint, pos) - wx.CallAfter(self._SetSelection, pos, field_end) -## dbg(indent=0) - return False - else: - if pos < field_start: -## dbg('cursor before 1st field; go to start of field') - wx.CallAfter(self._SetInsertionPoint, field_start) - if field._selectOnFieldEntry: - wx.CallAfter(self._SetSelection, field_start, field_end) - else: - wx.CallAfter(self._SetSelection, field_start, field_start) - return False - # else... -## dbg('end of current field:', field_end) -## dbg('go to next field') - if field_end == self._fields[self._field_indices[-1]]._extent[1]: - self._AdjustField(pos) - if event.GetKeyCode() == wx.WXK_TAB: -## dbg('tab to next ctrl') - event.Skip() - else: -## dbg('position at end') - wx.CallAfter(self._SetInsertionPoint, field_end) -## dbg(indent=0) - return False - else: - # we have to find the start of the next field - next_pos = self._findNextEntry(field_end) - if next_pos == field_end: -## dbg('already in last field') - self._AdjustField(pos) - if event.GetKeyCode() == wx.WXK_TAB: -## dbg('tab to next ctrl') - event.Skip() - #else: do nothing -## dbg(indent=0) - return False - else: - self._AdjustField( pos ) - - # move cursor to appropriate point in the next field and select as necessary: - field = self._FindField(next_pos) - edit_start, edit_end = field._extent - if field._selectOnFieldEntry: -## dbg('move to ', next_pos) - wx.CallAfter(self._SetInsertionPoint, next_pos) - edit_start, edit_end = self._FindFieldExtent(next_pos) -## dbg('queuing select', edit_start, edit_end) - wx.CallAfter(self._SetSelection, edit_start, edit_end) - else: - if field._insertRight: - next_pos = field._extent[1] -## dbg('move to ', next_pos) - wx.CallAfter(self._SetInsertionPoint, next_pos) -## dbg(indent=0) - return False - - - def _OnDecimalPoint(self, event): -## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) - - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - - if self._isFloat: ## handle float value, move to decimal place -## dbg('key == Decimal tab; decimal pos:', self._decimalpos) - value = self._GetValue() - if pos < self._decimalpos: - clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:] -## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) - newstr = self._adjustFloat(clipped_text) - else: - newstr = self._adjustFloat(value) - wx.CallAfter(self._SetValue, newstr) - fraction = self._fields[1] - start, end = fraction._extent - wx.CallAfter(self._SetInsertionPoint, start) - if fraction._selectOnFieldEntry: -## dbg('queuing selection after decimal point to:', (start, end)) - wx.CallAfter(self._SetSelection, start, end) - keep_processing = False - - if self._isInt: ## handle integer value, truncate from current position -## dbg('key == Integer decimal event') - value = self._GetValue() - clipped_text = value[0:pos] -## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) - newstr = self._adjustInt(clipped_text) -## dbg('newstr: "%s"' % newstr) - wx.CallAfter(self._SetValue, newstr) - newpos = len(newstr.rstrip()) - if newstr.find(')') != -1: - newpos -= 1 # (don't move past right paren) - wx.CallAfter(self._SetInsertionPoint, newpos) - keep_processing = False -## dbg(indent=0) - - - def _OnChangeSign(self, event): -## dbg('MaskedEditMixin::_OnChangeSign', indent=1) - key = event.GetKeyCode() - pos = self._adjustPos(self._GetInsertionPoint(), key) - value = self._eraseSelection() - integer = self._fields[0] - start, end = integer._extent - -#### dbg('adjusted pos:', pos) - if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos): - cursign = self._isNeg -## dbg('cursign:', cursign) - if chr(key) in ('-','(', ')'): - self._isNeg = (not self._isNeg) ## flip value - else: - self._isNeg = False -## dbg('isNeg?', self._isNeg) - - text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value) -## dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) - if text is None: - text = value - - if self._isNeg and self._signpos is not None and self._signpos != -1: - if self._useParens and self._right_signpos is not None: - text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:] - else: - text = text[:self._signpos] + '-' + text[self._signpos+1:] - else: -#### dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) - if self._useParens: - text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:] - else: - text = text[:self._signpos] + ' ' + text[self._signpos+1:] -## dbg('clearing self._isNeg') - self._isNeg = False - - wx.CallAfter(self._SetValue, text) - wx.CallAfter(self._applyFormatting) -## dbg('pos:', pos, 'signpos:', self._signpos) - if pos == self._signpos or integer.IsEmpty(text[start:end]): - wx.CallAfter(self._SetInsertionPoint, self._signpos+1) - else: - wx.CallAfter(self._SetInsertionPoint, pos) - - keep_processing = False - else: - keep_processing = True -## dbg(indent=0) - return keep_processing - - - def _OnGroupChar(self, event): - """ - This handler is only registered if the mask is a numeric mask. - It allows the insertion of ',' or '.' if appropriate. - """ -## dbg('MaskedEditMixin::_OnGroupChar', indent=1) - keep_processing = True - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - sel_start, sel_to = self._GetSelection() - groupchar = self._fields[0]._groupChar - if not self._isCharAllowed(groupchar, pos, checkRegex=True): - keep_processing = False - if not wx.Validator_IsSilent(): - wx.Bell() - - if keep_processing: - newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() ) -## dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) - if self._ctrl_constraints._validRequired and not self.IsValid(newstr): - keep_processing = False - if not wx.Validator_IsSilent(): - wx.Bell() - - if keep_processing: - wx.CallAfter(self._SetValue, newstr) - wx.CallAfter(self._SetInsertionPoint, newpos) - keep_processing = False -## dbg(indent=0) - return keep_processing - - - def _findNextEntry(self,pos, adjustInsert=True): - """ Find the insertion point for the next valid entry character position.""" - if self._isTemplateChar(pos): # if changing fields, pay attn to flag - adjustInsert = adjustInsert - else: # else within a field; flag not relevant - adjustInsert = False - - while self._isTemplateChar(pos) and pos < self._masklength: - pos += 1 - - # if changing fields, and we've been told to adjust insert point, - # look at new field; if empty and right-insert field, - # adjust to right edge: - if adjustInsert and pos < self._masklength: - field = self._FindField(pos) - start, end = field._extent - slice = self._GetValue()[start:end] - if field._insertRight and field.IsEmpty(slice): - pos = end - return pos - - - def _findNextTemplateChar(self, pos): - """ Find the position of the next non-editable character in the mask.""" - while not self._isTemplateChar(pos) and pos < self._masklength: - pos += 1 - return pos - - - def _OnAutoCompleteField(self, event): -## dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) - pos = self._GetInsertionPoint() - field = self._FindField(pos) - edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) - - match_index = None - keycode = event.GetKeyCode() - - if field._fillChar != ' ': - text = slice.replace(field._fillChar, '') - else: - text = slice - text = text.strip() - keep_processing = True # (assume True to start) -## dbg('field._hasList?', field._hasList) - if field._hasList: -## dbg('choices:', field._choices) -## dbg('compareChoices:', field._compareChoices) - choices, choice_required = field._compareChoices, field._choiceRequired - if keycode in (wx.WXK_PRIOR, wx.WXK_UP): - direction = -1 - else: - direction = 1 - match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex) - if( match_index is None - and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] - or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ): - # Select the 1st thing from the list: - match_index = 0 - - if( match_index is not None - and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] - or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown()) - or (keycode == wx.WXK_DOWN and partial_match) ) ): - - # We're allowed to auto-complete: -## dbg('match found') - value = self._GetValue() - newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:] -## dbg('setting value to "%s"' % newvalue) - self._SetValue(newvalue) - self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip()))) - self._OnAutoSelect(field, match_index) - self._CheckValid() # recolor as appopriate - - - if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT): - # treat as left right arrow if unshifted, tab/shift tab if shifted. - if event.ShiftDown(): - if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT): - # remove "shifting" and treat as (forward) tab: - event.m_shiftDown = False - keep_processing = self._OnChangeField(event) - else: - keep_processing = self._OnArrow(event) - # else some other key; keep processing the key - -## dbg('keep processing?', keep_processing, indent=0) - return keep_processing - - - def _OnAutoSelect(self, field, match_index = None): - """ - Function called if autoselect feature is enabled and entire control - is selected: - """ -## dbg('MaskedEditMixin::OnAutoSelect', field._index) - if match_index is not None: - field._autoCompleteIndex = match_index - - - def _autoComplete(self, direction, choices, value, compareNoCase, current_index): - """ - This function gets called in response to Auto-complete events. - It attempts to find a match to the specified value against the - list of choices; if exact match, the index of then next - appropriate value in the list, based on the given direction. - If not an exact match, it will return the index of the 1st value from - the choice list for which the partial value can be extended to match. - If no match found, it will return None. - The function returns a 2-tuple, with the 2nd element being a boolean - that indicates if partial match was necessary. - """ -## dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) - if value is None: -## dbg('nothing to match against', indent=0) - return (None, False) - - partial_match = False - - if compareNoCase: - value = value.lower() - - last_index = len(choices) - 1 - if value in choices: -## dbg('"%s" in', choices) - if current_index is not None and choices[current_index] == value: - index = current_index - else: - index = choices.index(value) - -## dbg('matched "%s" (%d)' % (choices[index], index)) - if direction == -1: -## dbg('going to previous') - if index == 0: index = len(choices) - 1 - else: index -= 1 - else: - if index == len(choices) - 1: index = 0 - else: index += 1 -## dbg('change value to "%s" (%d)' % (choices[index], index)) - match = index - else: - partial_match = True - value = value.strip() -## dbg('no match; try to auto-complete:') - match = None -## dbg('searching for "%s"' % value) - if current_index is None: - indices = range(len(choices)) - if direction == -1: - indices.reverse() - else: - if direction == 1: - indices = range(current_index +1, len(choices)) + range(current_index+1) -## dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) - else: - indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1) -## dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices) -#### dbg('indices:', indices) - for index in indices: - choice = choices[index] - if choice.find(value, 0) == 0: -## dbg('match found:', choice) - match = index - break - else: dbg('choice: "%s" - no match' % choice) - if match is not None: -## dbg('matched', match) - pass - else: -## dbg('no match found') - pass -## dbg(indent=0) - return (match, partial_match) - - - def _AdjustField(self, pos): - """ - This function gets called by default whenever the cursor leaves a field. - The pos argument given is the char position before leaving that field. - By default, floating point, integer and date values are adjusted to be - legal in this function. Derived classes may override this function - to modify the value of the control in a different way when changing fields. - - NOTE: these change the value immediately, and restore the cursor to - the passed location, so that any subsequent code can then move it - based on the operation being performed. - """ - newvalue = value = self._GetValue() - field = self._FindField(pos) - start, end, slice = self._FindFieldExtent(getslice=True) - newfield = field._AdjustField(slice) - newvalue = value[:start] + newfield + value[end:] - - if self._isFloat and newvalue != self._template: - newvalue = self._adjustFloat(newvalue) - - if self._ctrl_constraints._isInt and value != self._template: - newvalue = self._adjustInt(value) - - if self._isDate and value != self._template: - newvalue = self._adjustDate(value, fixcentury=True) - if self._4digityear: - year2dig = self._dateExtent - 2 - if pos == year2dig and value[year2dig] != newvalue[year2dig]: - pos = pos+2 - - if newvalue != value: - self._SetValue(newvalue) - self._SetInsertionPoint(pos) - - - def _adjustKey(self, pos, key): - """ Apply control formatting to the key (e.g. convert to upper etc). """ - field = self._FindField(pos) - if field._forceupper and key in range(97,123): - key = ord( chr(key).upper()) - - if field._forcelower and key in range(97,123): - key = ord( chr(key).lower()) - - return key - - - def _adjustPos(self, pos, key): - """ - Checks the current insertion point position and adjusts it if - necessary to skip over non-editable characters. - """ -## dbg('_adjustPos', pos, key, indent=1) - sel_start, sel_to = self._GetSelection() - # If a numeric or decimal mask, and negatives allowed, reserve the - # first space for sign, and last one if using parens. - if( self._signOk - and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) ) - or self._useParens and pos == self._masklength -1)): -## dbg('adjusted pos:', pos, indent=0) - return pos - - if key not in self._nav: - field = self._FindField(pos) - -## dbg('field._insertRight?', field._insertRight) - if field._insertRight: # if allow right-insert - start, end = field._extent - slice = self._GetValue()[start:end].strip() - field_len = end - start - if pos == end: # if cursor at right edge of field - # if not filled or supposed to stay in field, keep current position -#### dbg('pos==end') -#### dbg('len (slice):', len(slice)) -#### dbg('field_len?', field_len) -#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) -#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) - if len(slice) == field_len and field._moveOnFieldFull: - # move cursor to next field: - pos = self._findNextEntry(pos) - self._SetInsertionPoint(pos) - if pos < sel_to: - self._SetSelection(pos, sel_to) # restore selection - else: - self._SetSelection(pos, pos) # remove selection - else: # leave cursor alone - pass - else: - # if at start of control, move to right edge - if sel_to == sel_start and self._isTemplateChar(pos) and pos != end: - pos = end # move to right edge -## elif sel_start <= start and sel_to == end: -## # select to right edge of field - 1 (to replace char) -## pos = end - 1 -## self._SetInsertionPoint(pos) -## # restore selection -## self._SetSelection(sel_start, pos) - - elif self._signOk and sel_start == 0: # if selected to beginning and signed, - # adjust to past reserved sign position: - pos = self._fields[0]._extent[0] - self._SetInsertionPoint(pos) - # restore selection - self._SetSelection(pos, sel_to) - else: - pass # leave position/selection alone - - # else make sure the user is not trying to type over a template character - # If they are, move them to the next valid entry position - elif self._isTemplateChar(pos): - if( not field._moveOnFieldFull - and (not self._signOk - or (self._signOk - and field._index == 0 - and pos > 0) ) ): # don't move to next field without explicit cursor movement - pass - else: - # find next valid position - pos = self._findNextEntry(pos) - self._SetInsertionPoint(pos) - if pos < sel_to: # restore selection - self._SetSelection(pos, sel_to) -## dbg('adjusted pos:', pos, indent=0) - return pos - - - def _adjustFloat(self, candidate=None): - """ - 'Fixes' an floating point control. Collapses spaces, right-justifies, etc. - """ -## dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) - lenInt,lenFraction = [len(s) for s in self._mask.split('.')] ## Get integer, fraction lengths - - if candidate is None: value = self._GetValue() - else: value = candidate -## dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) - intStr, fracStr = value.split(self._decimalChar) - - intStr = self._fields[0]._AdjustField(intStr) -## dbg('adjusted intStr: "%s"' % intStr) - lenInt = len(intStr) - fracStr = fracStr + ('0'*(lenFraction-len(fracStr))) # add trailing spaces to decimal - -## dbg('intStr "%(intStr)s"' % locals()) -## dbg('lenInt:', lenInt) - - intStr = string.rjust( intStr[-lenInt:], lenInt) -## dbg('right-justifed intStr = "%(intStr)s"' % locals()) - newvalue = intStr + self._decimalChar + fracStr - - if self._signOk: - if len(newvalue) < self._masklength: - newvalue = ' ' + newvalue - signedvalue = self._getSignedValue(newvalue)[0] - if signedvalue is not None: newvalue = signedvalue - - # Finally, align string with decimal position, left-padding with - # fillChar: - newdecpos = newvalue.find(self._decimalChar) - if newdecpos < self._decimalpos: - padlen = self._decimalpos - newdecpos - newvalue = string.join([' ' * padlen] + [newvalue] ,'') - - if self._signOk and self._useParens: - if newvalue.find('(') != -1: - newvalue = newvalue[:-1] + ')' - else: - newvalue = newvalue[:-1] + ' ' - -## dbg('newvalue = "%s"' % newvalue) - if candidate is None: - wx.CallAfter(self._SetValue, newvalue) -## dbg(indent=0) - return newvalue - - - def _adjustInt(self, candidate=None): - """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" -## dbg("MaskedEditMixin::_adjustInt", candidate) - lenInt = self._masklength - if candidate is None: value = self._GetValue() - else: value = candidate - - intStr = self._fields[0]._AdjustField(value) - intStr = intStr.strip() # drop extra spaces -## dbg('adjusted field: "%s"' % intStr) - - if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1: - if self._useParens: - intStr = '(' + intStr + ')' - else: - intStr = '-' + intStr - elif self._isNeg and intStr.find('-') != -1 and self._useParens: - intStr = intStr.replace('-', '(') - - if( self._signOk and ((self._useParens and intStr.find('(') == -1) - or (not self._useParens and intStr.find('-') == -1))): - intStr = ' ' + intStr - if self._useParens: - intStr += ' ' # space for right paren position - - elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1: - # ensure closing right paren: - intStr += ')' - - if self._fields[0]._alignRight: ## Only if right-alignment is enabled - intStr = intStr.rjust( lenInt ) - else: - intStr = intStr.ljust( lenInt ) - - if candidate is None: - wx.CallAfter(self._SetValue, intStr ) - return intStr - - - def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False): - """ - 'Fixes' a date control, expanding the year if it can. - Applies various self-formatting options. - """ -## dbg("MaskedEditMixin::_adjustDate", indent=1) - if candidate is None: text = self._GetValue() - else: text = candidate -## dbg('text=', text) - if self._datestyle == "YMD": - year_field = 0 - else: - year_field = 2 - -## dbg('getYear: "%s"' % getYear(text, self._datestyle)) - year = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars - month = getMonth( text, self._datestyle) - day = getDay( text, self._datestyle) -## dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) - - yearVal = None - yearstart = self._dateExtent - 4 - if( len(year) < 4 - and (fixcentury - or force4digit_year - or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ') - or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ): - ## user entered less than four digits and changing fields or past point where we could - ## enter another digit: - try: - yearVal = int(year) - except: -## dbg('bad year=', year) - year = text[yearstart:self._dateExtent] - - if len(year) < 4 and yearVal: - if len(year) == 2: - # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the - # years pass... - now = wx.DateTime_Now() - century = (now.GetYear() /100) * 100 # "this century" - twodig_year = now.GetYear() - century # "this year" (2 digits) - # if separation between today's 2-digit year and typed value > 50, - # assume last century, - # else assume this century. - # - # Eg: if 2003 and yearVal == 30, => 2030 - # if 2055 and yearVal == 80, => 2080 - # if 2010 and yearVal == 96, => 1996 - # - if abs(yearVal - twodig_year) > 50: - yearVal = (century - 100) + yearVal - else: - yearVal = century + yearVal - year = str( yearVal ) - else: # pad with 0's to make a 4-digit year - year = "%04d" % yearVal - if self._4digityear or force4digit_year: - text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:] -## dbg('newdate: "%s"' % text, indent=0) - return text - - - def _goEnd(self, getPosOnly=False): - """ Moves the insertion point to the end of user-entry """ -## dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) - text = self._GetValue() -#### dbg('text: "%s"' % text) - i = 0 - if len(text.rstrip()): - for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1): -#### dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) - if self._isMaskChar(i): - char = text[i] -#### dbg("text[%d]: '%s'" % (i, char)) - if char != ' ': - i += 1 - break - - if i == 0: - pos = self._goHome(getPosOnly=True) - else: - pos = min(i,self._masklength) - - field = self._FindField(pos) - start, end = field._extent - if field._insertRight and pos < end: - pos = end -## dbg('next pos:', pos) -## dbg(indent=0) - if getPosOnly: - return pos - else: - self._SetInsertionPoint(pos) - - - def _goHome(self, getPosOnly=False): - """ Moves the insertion point to the beginning of user-entry """ -## dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) - text = self._GetValue() - for i in range(self._masklength): - if self._isMaskChar(i): - break - pos = max(i, 0) -## dbg(indent=0) - if getPosOnly: - return pos - else: - self._SetInsertionPoint(max(i,0)) - - - - def _getAllowedChars(self, pos): - """ Returns a string of all allowed user input characters for the provided - mask character plus control options - """ - maskChar = self.maskdict[pos] - okchars = self.maskchardict[maskChar] ## entry, get mask approved characters - field = self._FindField(pos) - if okchars and field._okSpaces: ## Allow spaces? - okchars += " " - if okchars and field._includeChars: ## any additional included characters? - okchars += field._includeChars -#### dbg('okchars[%d]:' % pos, okchars) - return okchars - - - def _isMaskChar(self, pos): - """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) - """ - if pos < self._masklength: - return self.ismasked[pos] - else: - return False - - - def _isTemplateChar(self,Pos): - """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) - """ - if Pos < self._masklength: - return not self._isMaskChar(Pos) - else: - return False - - - def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False): - """ Returns True if character is allowed at the specific position, otherwise False.""" -## dbg('_isCharAllowed', char, pos, checkRegex, indent=1) - field = self._FindField(pos) - right_insert = False - - if self.controlInitialized: - sel_start, sel_to = self._GetSelection() - else: - sel_start, sel_to = pos, pos - - if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight: - start, end = field._extent - field_len = end - start - if self.controlInitialized: - value = self._GetValue() - fstr = value[start:end].strip() - if field._padZero: - while fstr and fstr[0] == '0': - fstr = fstr[1:] - input_len = len(fstr) - if self._signOk and '-' in fstr or '(' in fstr: - input_len -= 1 # sign can move out of field, so don't consider it in length - else: - value = self._template - input_len = 0 # can't get the current "value", so use 0 - - - # if entire field is selected or position is at end and field is not full, - # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar: - if( (sel_start, sel_to) == field._extent - or (pos == end and input_len < field_len)): - pos = end - 1 -## dbg('pos = end - 1 = ', pos, 'right_insert? 1') - right_insert = True - elif( field._allowInsert and sel_start == sel_to - and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar)) - and input_len < field_len ): - pos = sel_to - 1 # where character will go -## dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') - right_insert = True - # else leave pos alone... - else: -## dbg('pos stays ', pos, 'right_insert? 0') - pass - - if self._isTemplateChar( pos ): ## if a template character, return empty -## dbg('%d is a template character; returning False' % pos, indent=0) - return False - - if self._isMaskChar( pos ): - okChars = self._getAllowedChars(pos) - - if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)): - okChars += self._fields[0]._groupChar - - if self._signOk: - if self._isInt or (self._isFloat and pos < self._decimalpos): - okChars += '-' - if self._useParens: - okChars += '(' - elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)): - okChars += ')' - -#### dbg('%s in %s?' % (char, okChars), char in okChars) - approved = char in okChars - - if approved and checkRegex: -## dbg("checking appropriate regex's") - value = self._eraseSelection(self._GetValue()) - if right_insert: - at = pos+1 - else: - at = pos - if allowAutoSelect: - newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True) - else: - newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value) -## dbg('newvalue: "%s"' % newvalue) - - fields = [self._FindField(pos)] + [self._ctrl_constraints] - for field in fields: # includes fields[-1] == "ctrl_constraints" - if field._regexMask and field._filter: -## dbg('checking vs. regex') - start, end = field._extent - slice = newvalue[start:end] - approved = (re.match( field._filter, slice) is not None) -## dbg('approved?', approved) - if not approved: break -## dbg(indent=0) - return approved - else: -## dbg('%d is a !???! character; returning False', indent=0) - return False - - - def _applyFormatting(self): - """ Apply formatting depending on the control's state. - Need to find a way to call this whenever the value changes, in case the control's - value has been changed or set programatically. - """ -## dbg(suspend=1) -## dbg('MaskedEditMixin::_applyFormatting', indent=1) - - # Handle negative numbers - if self._signOk: - text, signpos, right_signpos = self._getSignedValue() -## dbg('text: "%s", signpos:' % text, signpos) - if not text or text[signpos] not in ('-','('): - self._isNeg = False -## dbg('no valid sign found; new sign:', self._isNeg) - if text and signpos != self._signpos: - self._signpos = signpos - elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('): -## dbg('setting _isNeg to True') - self._isNeg = True -## dbg('self._isNeg:', self._isNeg) - - if self._signOk and self._isNeg: - fc = self._signedForegroundColour - else: - fc = self._foregroundColour - - if hasattr(fc, '_name'): - c =fc._name - else: - c = fc -## dbg('setting foreground to', c) - self.SetForegroundColour(fc) - - if self._valid: -## dbg('valid') - if self.IsEmpty(): - bc = self._emptyBackgroundColour - else: - bc = self._validBackgroundColour - else: -## dbg('invalid') - bc = self._invalidBackgroundColour - if hasattr(bc, '_name'): - c =bc._name - else: - c = bc -## dbg('setting background to', c) - self.SetBackgroundColour(bc) - self._Refresh() -## dbg(indent=0, suspend=0) - - - def _getAbsValue(self, candidate=None): - """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). - """ -## dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) - if candidate is None: text = self._GetValue() - else: text = candidate - right_signpos = text.find(')') - - if self._isInt: - if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ': - signpos = text.find('-') - if signpos == -1: -## dbg('no - found; searching for (') - signpos = text.find('(') - elif signpos != -1: -## dbg('- found at', signpos) - pass - - if signpos == -1: -## dbg('signpos still -1') -## dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) - if len(text) < self._masklength: - text = ' ' + text - if len(text) < self._masklength: - text += ' ' - if len(text) > self._masklength and text[-1] in (')', ' '): - text = text[:-1] - else: -## dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) -## dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) - signpos = len(text) - (len(text.lstrip()) + 1) - - if self._useParens and not text.strip(): - signpos -= 1 # empty value; use penultimate space -## dbg('signpos:', signpos) - if signpos >= 0: - text = text[:signpos] + ' ' + text[signpos+1:] - - else: - if self._signOk: - signpos = 0 - text = self._template[0] + text[1:] - else: - signpos = -1 - - if right_signpos != -1: - if self._signOk: - text = text[:right_signpos] + ' ' + text[right_signpos+1:] - elif len(text) > self._masklength: - text = text[:right_signpos] + text[right_signpos+1:] - right_signpos = -1 - - - elif self._useParens and self._signOk: - # figure out where it ought to go: - right_signpos = self._masklength - 1 # initial guess - if not self._ctrl_constraints._alignRight: -## dbg('not right-aligned') - if len(text.strip()) == 0: - right_signpos = signpos + 1 - elif len(text.strip()) < self._masklength: - right_signpos = len(text.rstrip()) -## dbg('right_signpos:', right_signpos) - - groupchar = self._fields[0]._groupChar - try: - value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', '')) - except: -## dbg('invalid number', indent=0) - return None, signpos, right_signpos - - else: # float value - try: - groupchar = self._fields[0]._groupChar - value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', '')) -## dbg('value:', value) - except: - value = None - - if value < 0 and value is not None: - signpos = text.find('-') - if signpos == -1: - signpos = text.find('(') - - text = text[:signpos] + self._template[signpos] + text[signpos+1:] - else: - # look forwards up to the decimal point for the 1st non-digit -## dbg('decimal pos:', self._decimalpos) -## dbg('text: "%s"' % text) - if self._signOk: - signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1) - # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET - if len(text) >= signpos+1 and text[signpos+1] in ('-','('): - signpos += 1 - else: - signpos = -1 -## dbg('signpos:', signpos) - - if self._useParens: - if self._signOk: - right_signpos = self._masklength - 1 - text = text[:right_signpos] + ' ' - if text[signpos] == '(': - text = text[:signpos] + ' ' + text[signpos+1:] - else: - right_signpos = text.find(')') - if right_signpos != -1: - text = text[:-1] - right_signpos = -1 - - if value is None: -## dbg('invalid number') - text = None - -## dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) -## dbg(indent=0) - return text, signpos, right_signpos - - - def _getSignedValue(self, candidate=None): - """ Return a signed value by adding a "-" prefix if the value - is set to negative, or a space if positive. - """ -## dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) - if candidate is None: text = self._GetValue() - else: text = candidate - - - abstext, signpos, right_signpos = self._getAbsValue(text) - if self._signOk: - if abstext is None: -## dbg(indent=0) - return abstext, signpos, right_signpos - - if self._isNeg or text[signpos] in ('-', '('): - if self._useParens: - sign = '(' - else: - sign = '-' - else: - sign = ' ' - if abstext[signpos] not in string.digits: - text = abstext[:signpos] + sign + abstext[signpos+1:] - else: - # this can happen if value passed is too big; sign assumed to be - # in position 0, but if already filled with a digit, prepend sign... - text = sign + abstext - if self._useParens and text.find('(') != -1: - text = text[:right_signpos] + ')' + text[right_signpos+1:] - else: - text = abstext -## dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) -## dbg(indent=0) - return text, signpos, right_signpos - - - def GetPlainValue(self, candidate=None): - """ Returns control's value stripped of the template text. - plainvalue = MaskedEditMixin.GetPlainValue() - """ -## dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) - - if candidate is None: text = self._GetValue() - else: text = candidate - - if self.IsEmpty(): -## dbg('returned ""', indent=0) - return "" - else: - plain = "" - for idx in range( min(len(self._template), len(text)) ): - if self._mask[idx] in maskchars: - plain += text[idx] - - if self._isFloat or self._isInt: -## dbg('plain so far: "%s"' % plain) - plain = plain.replace('(', '-').replace(')', ' ') -## dbg('plain after sign regularization: "%s"' % plain) - - if self._signOk and self._isNeg and plain.count('-') == 0: - # must be in reserved position; add to "plain value" - plain = '-' + plain.strip() - - if self._fields[0]._alignRight: - lpad = plain.count(',') - plain = ' ' * lpad + plain.replace(',','') - else: - plain = plain.replace(',','') -## dbg('plain after pad and group:"%s"' % plain) - -## dbg('returned "%s"' % plain.rstrip(), indent=0) - return plain.rstrip() - - - def IsEmpty(self, value=None): - """ - Returns True if control is equal to an empty value. - (Empty means all editable positions in the template == fillChar.) - """ - if value is None: value = self._GetValue() - if value == self._template and not self._defaultValue: -#### dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") - return True # (all mask chars == fillChar by defn) - elif value == self._template: - empty = True - for pos in range(len(self._template)): -#### dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) -#### dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) - if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]): - empty = False -#### dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) - return empty - else: -#### dbg("IsEmpty? 0 (value doesn't match template)") - return False - - - def IsDefault(self, value=None): - """ - Returns True if the value specified (or the value of the control if not specified) - is equal to the default value. - """ - if value is None: value = self._GetValue() - return value == self._template - - - def IsValid(self, value=None): - """ Indicates whether the value specified (or the current value of the control - if not specified) is considered valid.""" -#### dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) - if value is None: value = self._GetValue() - ret = self._CheckValid(value) -#### dbg(indent=0) - return ret - - - def _eraseSelection(self, value=None, sel_start=None, sel_to=None): - """ Used to blank the selection when inserting a new character. """ -## dbg("MaskedEditMixin::_eraseSelection", indent=1) - if value is None: value = self._GetValue() - if sel_start is None or sel_to is None: - sel_start, sel_to = self._GetSelection() ## check for a range of selected text -## dbg('value: "%s"' % value) -## dbg("current sel_start, sel_to:", sel_start, sel_to) - - newvalue = list(value) - for i in range(sel_start, sel_to): - if self._signOk and newvalue[i] in ('-', '(', ')'): -## dbg('found sign (%s) at' % newvalue[i], i) - - # balance parentheses: - if newvalue[i] == '(': - right_signpos = value.find(')') - if right_signpos != -1: - newvalue[right_signpos] = ' ' - - elif newvalue[i] == ')': - left_signpos = value.find('(') - if left_signpos != -1: - newvalue[left_signpos] = ' ' - - newvalue[i] = ' ' - - elif self._isMaskChar(i): - field = self._FindField(i) - if field._padZero: - newvalue[i] = '0' - else: - newvalue[i] = self._template[i] - - value = string.join(newvalue,"") -## dbg('new value: "%s"' % value) -## dbg(indent=0) - return value - - - def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False): - """ Handles replacement of the character at the current insertion point.""" -## dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) - - text = self._eraseSelection(value) - field = self._FindField(pos) - start, end = field._extent - newtext = "" - newpos = pos - - if pos != sel_start and sel_start == sel_to: - # adjustpos must have moved the position; make selection match: - sel_start = sel_to = pos - -## dbg('field._insertRight?', field._insertRight) - if( field._insertRight # field allows right insert - and ((sel_start, sel_to) == field._extent # and whole field selected - or (sel_start == sel_to # or nothing selected - and (sel_start == end # and cursor at right edge - or (field._allowInsert # or field allows right-insert - and sel_start < end # next to other char in field: - and text[sel_start] != field._fillChar) ) ) ) ): -## dbg('insertRight') - fstr = text[start:end] - erasable_chars = [field._fillChar, ' '] - - if field._padZero: - erasable_chars.append('0') - - erased = '' -#### dbg("fstr[0]:'%s'" % fstr[0]) -#### dbg('field_index:', field._index) -#### dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) -#### dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", -## self._signOk and field._index == 0 and fstr[0] in ('-','(')) - if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')): - erased = fstr[0] -#### dbg('value: "%s"' % text) -#### dbg('fstr: "%s"' % fstr) -#### dbg("erased: '%s'" % erased) - field_sel_start = sel_start - start - field_sel_to = sel_to - start -## dbg('left fstr: "%s"' % fstr[1:field_sel_start]) -## dbg('right fstr: "%s"' % fstr[field_sel_to:end]) - fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end] - if field._alignRight and sel_start != sel_to: - field_len = end - start -## pos += (field_len - len(fstr)) # move cursor right by deleted amount - pos = sel_to -## dbg('setting pos to:', pos) - if field._padZero: - fstr = '0' * (field_len - len(fstr)) + fstr - else: - fstr = fstr.rjust(field_len) # adjust the field accordingly -## dbg('field str: "%s"' % fstr) - - newtext = text[:start] + fstr + text[end:] - if erased in ('-', '(') and self._signOk: - newtext = erased + newtext[1:] -## dbg('newtext: "%s"' % newtext) - - if self._signOk and field._index == 0: - start -= 1 # account for sign position - -#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull) -#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) - if( field._moveOnFieldFull and pos == end - and len(fstr.lstrip()) == end-start): # if field now full - newpos = self._findNextEntry(end) # go to next field - else: - newpos = pos # else keep cursor at current position - - if not newtext: -## dbg('not newtext') - if newpos != pos: -## dbg('newpos:', newpos) - pass - if self._signOk and self._useParens: - old_right_signpos = text.find(')') - - if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start: - # inserting within a left-insert-capable field - field_len = end - start - before = text[start:sel_start] - after = text[sel_to:end].strip() -#### dbg("current field:'%s'" % text[start:end]) -#### dbg("before:'%s'" % before, "after:'%s'" % after) - new_len = len(before) + len(after) + 1 # (for inserted char) -#### dbg('new_len:', new_len) - - if new_len < field_len: - retained = after + self._template[end-(field_len-new_len):end] - elif new_len > end-start: - retained = after[1:] - else: - retained = after - - left = text[0:start] + before -#### dbg("left:'%s'" % left, "retained:'%s'" % retained) - right = retained + text[end:] - else: - left = text[0:pos] - right = text[pos+1:] - - newtext = left + char + right - - if self._signOk and self._useParens: - # Balance parentheses: - left_signpos = newtext.find('(') - - if left_signpos == -1: # erased '('; remove ')' - right_signpos = newtext.find(')') - if right_signpos != -1: - newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:] - - elif old_right_signpos != -1: - right_signpos = newtext.find(')') - - if right_signpos == -1: # just replaced right-paren - if newtext[pos] == ' ': # we just erased '); erase '(' - newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:] - else: # replaced with digit; move ') over - if self._ctrl_constraints._alignRight or self._isFloat: - newtext = newtext[:-1] + ')' - else: - rstripped_text = newtext.rstrip() - right_signpos = len(rstripped_text) -## dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) - newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:] - - if( field._insertRight # if insert-right field (but we didn't start at right edge) - and field._moveOnFieldFull # and should move cursor when full - and len(newtext[start:end].strip()) == end-start): # and field now full - newpos = self._findNextEntry(end) # go to next field -## dbg('newpos = nextentry =', newpos) - else: -## dbg('pos:', pos, 'newpos:', pos+1) - newpos = pos+1 - - - if allowAutoSelect: - new_select_to = newpos # (default return values) - match_field = None - match_index = None - - if field._autoSelect: - match_index, partial_match = self._autoComplete(1, # (always forward) - field._compareChoices, - newtext[start:end], - compareNoCase=field._compareNoCase, - current_index = field._autoCompleteIndex-1) - if match_index is not None and partial_match: - matched_str = newtext[start:end] - newtext = newtext[:start] + field._choices[match_index] + newtext[end:] - new_select_to = end - match_field = field - if field._insertRight: - # adjust position to just after partial match in field - newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip())) - - elif self._ctrl_constraints._autoSelect: - match_index, partial_match = self._autoComplete( - 1, # (always forward) - self._ctrl_constraints._compareChoices, - newtext, - self._ctrl_constraints._compareNoCase, - current_index = self._ctrl_constraints._autoCompleteIndex - 1) - if match_index is not None and partial_match: - matched_str = newtext - newtext = self._ctrl_constraints._choices[match_index] - new_select_to = self._ctrl_constraints._extent[1] - match_field = self._ctrl_constraints - if self._ctrl_constraints._insertRight: - # adjust position to just after partial match in control: - newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip())) - -## dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) -## dbg(indent=0) - return newtext, newpos, new_select_to, match_field, match_index - else: -## dbg('newtext: "%s"' % newtext, 'newpos:', newpos) -## dbg(indent=0) - return newtext, newpos - - - def _OnFocus(self,event): - """ - This event handler is currently necessary to work around new default - behavior as of wxPython2.3.3; - The TAB key auto selects the entire contents of the wxTextCtrl *after* - the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection - *here*, because it hasn't happened yet. So to prevent this behavior, and - preserve the correct selection when the focus event is not due to tab, - we need to pull the following trick: - """ -## dbg('MaskedEditMixin::_OnFocus') - wx.CallAfter(self._fixSelection) - event.Skip() - self.Refresh() - - - def _CheckValid(self, candidate=None): - """ - This is the default validation checking routine; It verifies that the - current value of the control is a "valid value," and has the side - effect of coloring the control appropriately. - """ -## dbg(suspend=1) -## dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) - oldValid = self._valid - if candidate is None: value = self._GetValue() - else: value = candidate -## dbg('value: "%s"' % value) - oldvalue = value - valid = True # assume True - - if not self.IsDefault(value) and self._isDate: ## Date type validation - valid = self._validateDate(value) -## dbg("valid date?", valid) - - elif not self.IsDefault(value) and self._isTime: - valid = self._validateTime(value) -## dbg("valid time?", valid) - - elif not self.IsDefault(value) and (self._isInt or self._isFloat): ## Numeric type - valid = self._validateNumeric(value) -## dbg("valid Number?", valid) - - if valid: # and not self.IsDefault(value): ## generic validation accounts for IsDefault() - ## valid so far; ensure also allowed by any list or regex provided: - valid = self._validateGeneric(value) -## dbg("valid value?", valid) - -## dbg('valid?', valid) - - if not candidate: - self._valid = valid - self._applyFormatting() - if self._valid != oldValid: -## dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) -## dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) - pass -## dbg(indent=0, suspend=0) - return valid - - - def _validateGeneric(self, candidate=None): - """ Validate the current value using the provided list or Regex filter (if any). - """ - if candidate is None: - text = self._GetValue() - else: - text = candidate - - valid = True # assume True - for i in [-1] + self._field_indices: # process global constraints first: - field = self._fields[i] - start, end = field._extent - slice = text[start:end] - valid = field.IsValid(slice) - if not valid: - break - - return valid - - - def _validateNumeric(self, candidate=None): - """ Validate that the value is within the specified range (if specified.)""" - if candidate is None: value = self._GetValue() - else: value = candidate - try: - groupchar = self._fields[0]._groupChar - if self._isFloat: - number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', '')) - else: - number = long( value.replace(groupchar, '').replace('(', '-').replace(')', '')) - if value.strip(): - if self._fields[0]._alignRight: - require_digit_at = self._fields[0]._extent[1]-1 - else: - require_digit_at = self._fields[0]._extent[0] -## dbg('require_digit_at:', require_digit_at) -## dbg("value[rda]: '%s'" % value[require_digit_at]) - if value[require_digit_at] not in list(string.digits): - valid = False - return valid - # else... -## dbg('number:', number) - if self._ctrl_constraints._hasRange: - valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh - else: - valid = True - groupcharpos = value.rfind(groupchar) - if groupcharpos != -1: # group char present -## dbg('groupchar found at', groupcharpos) - if self._isFloat and groupcharpos > self._decimalpos: - # 1st one found on right-hand side is past decimal point -## dbg('groupchar in fraction; illegal') - valid = False - elif self._isFloat: - integer = value[:self._decimalpos].strip() - else: - integer = value.strip() -## dbg("integer:'%s'" % integer) - if integer[0] in ('-', '('): - integer = integer[1:] - if integer[-1] == ')': - integer = integer[:-1] - - parts = integer.split(groupchar) -## dbg('parts:', parts) - for i in range(len(parts)): - if i == 0 and abs(int(parts[0])) > 999: -## dbg('group 0 too long; illegal') - valid = False - break - elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]): -## dbg('group %i (%s) not right size; illegal' % (i, parts[i])) - valid = False - break - except ValueError: -## dbg('value not a valid number') - valid = False - return valid - - - def _validateDate(self, candidate=None): - """ Validate the current date value using the provided Regex filter. - Generally used for character types.BufferType - """ -## dbg('MaskedEditMixin::_validateDate', indent=1) - if candidate is None: value = self._GetValue() - else: value = candidate -## dbg('value = "%s"' % value) - text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it -## dbg('text =', text) - valid = True # assume True until proven otherwise - - try: - # replace fillChar in each field with space: - datestr = text[0:self._dateExtent] - for i in range(3): - field = self._fields[i] - start, end = field._extent - fstr = datestr[start:end] - fstr.replace(field._fillChar, ' ') - datestr = datestr[:start] + fstr + datestr[end:] - - year, month, day = getDateParts( datestr, self._datestyle) - year = int(year) -## dbg('self._dateExtent:', self._dateExtent) - if self._dateExtent == 11: - month = charmonths_dict[month.lower()] - else: - month = int(month) - day = int(day) -## dbg('year, month, day:', year, month, day) - - except ValueError: -## dbg('cannot convert string to integer parts') - valid = False - except KeyError: -## dbg('cannot convert string to integer month') - valid = False - - if valid: - # use wxDateTime to unambiguously try to parse the date: - # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, - # rather than 1-12, so handle accordingly: - if month > 12: - valid = False - else: - month -= 1 - try: -## dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) - dateHandler = wx.DateTimeFromDMY(day,month,year) -## dbg("succeeded") - dateOk = True - except: -## dbg('cannot convert string to valid date') - dateOk = False - if not dateOk: - valid = False - - if valid: - # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, - # so we eliminate them here: - timeStr = text[self._dateExtent+1:].strip() ## time portion of the string - if timeStr: -## dbg('timeStr: "%s"' % timeStr) - try: - checkTime = dateHandler.ParseTime(timeStr) - valid = checkTime == len(timeStr) - except: - valid = False - if not valid: -## dbg('cannot convert string to valid time') - pass - if valid: dbg('valid date') -## dbg(indent=0) - return valid - - - def _validateTime(self, candidate=None): - """ Validate the current time value using the provided Regex filter. - Generally used for character types.BufferType - """ -## dbg('MaskedEditMixin::_validateTime', indent=1) - # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, - # so we eliminate them here: - if candidate is None: value = self._GetValue().strip() - else: value = candidate.strip() -## dbg('value = "%s"' % value) - valid = True # assume True until proven otherwise - - dateHandler = wx.DateTime_Today() - try: - checkTime = dateHandler.ParseTime(value) -## dbg('checkTime:', checkTime, 'len(value)', len(value)) - valid = checkTime == len(value) - except: - valid = False - - if not valid: -## dbg('cannot convert string to valid time') - pass - if valid: dbg('valid time') -## dbg(indent=0) - return valid - - - def _OnKillFocus(self,event): - """ Handler for EVT_KILL_FOCUS event. - """ -## dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) - if self._mask and self._IsEditable(): - self._AdjustField(self._GetInsertionPoint()) - self._CheckValid() ## Call valid handler - - self._LostFocus() ## Provided for subclass use - event.Skip() -## dbg(indent=0) - - - def _fixSelection(self): - """ - This gets called after the TAB traversal selection is made, if the - focus event was due to this, but before the EVT_LEFT_* events if - the focus shift was due to a mouse event. - - The trouble is that, a priori, there's no explicit notification of - why the focus event we received. However, the whole reason we need to - do this is because the default behavior on TAB traveral in a wxTextCtrl is - now to select the entire contents of the window, something we don't want. - So we can *now* test the selection range, and if it's "the whole text" - we can assume the cause, change the insertion point to the start of - the control, and deselect. - """ -## dbg('MaskedEditMixin::_fixSelection', indent=1) - if not self._mask or not self._IsEditable(): -## dbg(indent=0) - return - - sel_start, sel_to = self._GetSelection() -## dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) - - if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space) - and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ): - # This isn't normally allowed, and so assume we got here by the new - # "tab traversal" behavior, so we need to reset the selection - # and insertion point: -## dbg('entire text selected; resetting selection to start of control') - self._goHome() - field = self._FindField(self._GetInsertionPoint()) - edit_start, edit_end = field._extent - if field._selectOnFieldEntry: - self._SetInsertionPoint(edit_start) - self._SetSelection(edit_start, edit_end) - - elif field._insertRight: - self._SetInsertionPoint(edit_end) - self._SetSelection(edit_end, edit_end) - - elif (self._isFloat or self._isInt): - - text, signpos, right_signpos = self._getAbsValue() - if text is None or text == self._template: - integer = self._fields[0] - edit_start, edit_end = integer._extent - - if integer._selectOnFieldEntry: -## dbg('select on field entry:') - self._SetInsertionPoint(edit_start) - self._SetSelection(edit_start, edit_end) - - elif integer._insertRight: -## dbg('moving insertion point to end') - self._SetInsertionPoint(edit_end) - self._SetSelection(edit_end, edit_end) - else: -## dbg('numeric ctrl is empty; start at beginning after sign') - self._SetInsertionPoint(signpos+1) ## Move past minus sign space if signed - self._SetSelection(signpos+1, signpos+1) - - elif sel_start > self._goEnd(getPosOnly=True): -## dbg('cursor beyond the end of the user input; go to end of it') - self._goEnd() - else: -## dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) - pass -## dbg(indent=0) - - - def _Keypress(self,key): - """ Method provided to override OnChar routine. Return False to force - a skip of the 'normal' OnChar process. Called before class OnChar. - """ - return True - - - def _LostFocus(self): - """ Method provided for subclasses. _LostFocus() is called after - the class processes its EVT_KILL_FOCUS event code. - """ - pass - - - def _OnDoubleClick(self, event): - """ selects field under cursor on dclick.""" - pos = self._GetInsertionPoint() - field = self._FindField(pos) - start, end = field._extent - self._SetInsertionPoint(start) - self._SetSelection(start, end) - - - def _Change(self): - """ Method provided for subclasses. Called by internal EVT_TEXT - handler. Return False to override the class handler, True otherwise. - """ - return True - - - def _Cut(self): - """ - Used to override the default Cut() method in base controls, instead - copying the selection to the clipboard and then blanking the selection, - leaving only the mask in the selected area behind. - Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the - derived control because the mixin functions can't override a method of - a sibling class. - """ -## dbg("MaskedEditMixin::_Cut", indent=1) - value = self._GetValue() -## dbg('current value: "%s"' % value) - sel_start, sel_to = self._GetSelection() ## check for a range of selected text -## dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) - do = wxTextDataObject() - do.SetText(value[sel_start:sel_to].strip()) - wxTheClipboard.Open() - wxTheClipboard.SetData(do) - wxTheClipboard.Close() - - if sel_to - sel_start != 0: - self._OnErase() -## dbg(indent=0) - - -# WS Note: overriding Copy is no longer necessary given that you -# can no longer select beyond the last non-empty char in the control. -# -## def _Copy( self ): -## """ -## Override the wxTextCtrl's .Copy function, with our own -## that does validation. Need to strip trailing spaces. -## """ -## sel_start, sel_to = self._GetSelection() -## select_len = sel_to - sel_start -## textval = wxTextCtrl._GetValue(self) -## -## do = wxTextDataObject() -## do.SetText(textval[sel_start:sel_to].strip()) -## wxTheClipboard.Open() -## wxTheClipboard.SetData(do) -## wxTheClipboard.Close() - - - def _getClipboardContents( self ): - """ Subroutine for getting the current contents of the clipboard. - """ - do = wxTextDataObject() - wxTheClipboard.Open() - success = wxTheClipboard.GetData(do) - wxTheClipboard.Close() - - if not success: - return None - else: - # Remove leading and trailing spaces before evaluating contents - return do.GetText().strip() - - - def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False): - """ - Used by paste routine and field choice validation to see - if a given slice of paste text is legal for the area in question: - returns validity, replacement text, and extent of paste in - template. - """ -## dbg(suspend=1) -## dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) - select_length = sel_to - sel_start - maxlength = select_length -## dbg('sel_to - sel_start:', maxlength) - if maxlength == 0: - maxlength = self._masklength - sel_start - item = 'control' - else: - item = 'selection' -## dbg('maxlength:', maxlength) - length_considered = len(paste_text) - if length_considered > maxlength: -## dbg('paste text will not fit into the %s:' % item, indent=0) - if raise_on_invalid: -## dbg(indent=0, suspend=0) - if item == 'control': - raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) - else: - raise ValueError('"%s" will not fit into the selection' % paste_text) - else: -## dbg(indent=0, suspend=0) - return False, None, None - - text = self._template -## dbg('length_considered:', length_considered) - - valid_paste = True - replacement_text = "" - replace_to = sel_start - i = 0 - while valid_paste and i < length_considered and replace_to < self._masklength: - if paste_text[i:] == self._template[replace_to:length_considered]: - # remainder of paste matches template; skip char-by-char analysis -## dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) - replacement_text += paste_text[i:] - replace_to = i = length_considered - continue - # else: - char = paste_text[i] - field = self._FindField(replace_to) - if not field._compareNoCase: - if field._forceupper: char = char.upper() - elif field._forcelower: char = char.lower() - -## dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) -## dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) - if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True): - replacement_text += char -## dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) -## dbg("replacement_text:", '"'+replacement_text+'"') - i += 1 - replace_to += 1 - elif( char == self._template[replace_to] - or (self._signOk and - ( (i == 0 and (char == '-' or (self._useParens and char == '('))) - or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ): - replacement_text += char -## dbg("'%(char)s' == template(%(replace_to)d)" % locals()) -## dbg("replacement_text:", '"'+replacement_text+'"') - i += 1 - replace_to += 1 - else: - next_entry = self._findNextEntry(replace_to, adjustInsert=False) - if next_entry == replace_to: - valid_paste = False - else: - replacement_text += self._template[replace_to:next_entry] -## dbg("skipping template; next_entry =", next_entry) -## dbg("replacement_text:", '"'+replacement_text+'"') - replace_to = next_entry # so next_entry will be considered on next loop - - if not valid_paste and raise_on_invalid: -## dbg('raising exception', indent=0, suspend=0) - raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name)) - - elif i < len(paste_text): - valid_paste = False - if raise_on_invalid: -## dbg('raising exception', indent=0, suspend=0) - raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) - -## dbg('valid_paste?', valid_paste) - if valid_paste: -## dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) - pass -## dbg(indent=0, suspend=0) - return valid_paste, replacement_text, replace_to - - - def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): - """ - Used to override the base control's .Paste() function, - with our own that does validation. - Note: _Paste must be called from a Paste() override in the - derived control because the mixin functions can't override a - method of a sibling class. - """ -## dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) - if value is None: - paste_text = self._getClipboardContents() - else: - paste_text = value - - if paste_text is not None: -## dbg('paste text: "%s"' % paste_text) - # (conversion will raise ValueError if paste isn't legal) - sel_start, sel_to = self._GetSelection() -## dbg('selection:', (sel_start, sel_to)) - - # special case: handle allowInsert fields properly - field = self._FindField(sel_start) - edit_start, edit_end = field._extent - new_pos = None - if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end: - new_pos = sel_start + len(paste_text) # store for subsequent positioning - paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip() -## dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) - sel_to = sel_start + len(paste_text) - - # Another special case: paste won't fit, but it's a right-insert field where entire - # non-empty value is selected, and there's room if the selection is expanded leftward: - if( len(paste_text) > sel_to - sel_start - and field._insertRight - and sel_start > edit_start - and sel_to >= edit_end - and not self._GetValue()[edit_start:sel_start].strip() ): - # text won't fit within selection, but left of selection is empty; - # check to see if we can expand selection to accomodate the value: - empty_space = sel_start - edit_start - amount_needed = len(paste_text) - (sel_to - sel_start) - if amount_needed <= empty_space: - sel_start -= amount_needed -## dbg('expanded selection to:', (sel_start, sel_to)) - - - # another special case: deal with signed values properly: - if self._signOk: - signedvalue, signpos, right_signpos = self._getSignedValue() - paste_signpos = paste_text.find('-') - if paste_signpos == -1: - paste_signpos = paste_text.find('(') - - # if paste text will result in signed value: -#### dbg('paste_signpos != -1?', paste_signpos != -1) -#### dbg('sel_start:', sel_start, 'signpos:', signpos) -#### dbg('field._insertRight?', field._insertRight) -#### dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) - if paste_signpos != -1 and (sel_start <= signpos - or (field._insertRight and sel_start - len(paste_text) <= signpos)): - signed = True - else: - signed = False - # remove "sign" from paste text, so we can auto-adjust for sign type after paste: - paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','') -## dbg('unsigned paste text: "%s"' % paste_text) - else: - signed = False - - # another special case: deal with insert-right fields when selection is empty and - # cursor is at end of field: -#### dbg('field._insertRight?', field._insertRight) -#### dbg('sel_start == edit_end?', sel_start == edit_end) -#### dbg('sel_start', sel_start, 'sel_to', sel_to) - if field._insertRight and sel_start == edit_end and sel_start == sel_to: - sel_start -= len(paste_text) - if sel_start < 0: - sel_start = 0 -## dbg('adjusted selection:', (sel_start, sel_to)) - - try: - valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) - except: -## dbg('exception thrown', indent=0) - raise - - if not valid_paste: -## dbg('paste text not legal for the selection or portion of the control following the cursor;') - if not wx.Validator_IsSilent(): - wx.Bell() -## dbg(indent=0) - return False - # else... - text = self._eraseSelection() - - new_text = text[:sel_start] + replacement_text + text[replace_to:] - if new_text: - new_text = string.ljust(new_text,self._masklength) - if signed: - new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text) - if new_text: - if self._useParens: - new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:] - else: - new_text = new_text[:signpos] + '-' + new_text[signpos+1:] - if not self._isNeg: - self._isNeg = 1 - -## dbg("new_text:", '"'+new_text+'"') - - if not just_return_value: - if new_text != self._GetValue(): - self.modified = True - if new_text == '': - self.ClearValue() - else: - wx.CallAfter(self._SetValue, new_text) - if new_pos is None: - new_pos = sel_start + len(replacement_text) - wx.CallAfter(self._SetInsertionPoint, new_pos) - else: -## dbg(indent=0) - return new_text - elif just_return_value: -## dbg(indent=0) - return self._GetValue() -## dbg(indent=0) - - def _Undo(self): - """ Provides an Undo() method in base controls. """ -## dbg("MaskedEditMixin::_Undo", indent=1) - value = self._GetValue() - prev = self._prevValue -## dbg('current value: "%s"' % value) -## dbg('previous value: "%s"' % prev) - if prev is None: -## dbg('no previous value', indent=0) - return - - elif value != prev: - # Determine what to select: (relies on fixed-length strings) - # (This is a lot harder than it would first appear, because - # of mask chars that stay fixed, and so break up the "diff"...) - - # Determine where they start to differ: - i = 0 - length = len(value) # (both are same length in masked control) - - while( value[:i] == prev[:i] ): - i += 1 - sel_start = i - 1 - - - # handle signed values carefully, so undo from signed to unsigned or vice-versa - # works properly: - if self._signOk: - text, signpos, right_signpos = self._getSignedValue(candidate=prev) - if self._useParens: - if prev[signpos] == '(' and prev[right_signpos] == ')': - self._isNeg = True - else: - self._isNeg = False - # eliminate source of "far-end" undo difference if using balanced parens: - value = value.replace(')', ' ') - prev = prev.replace(')', ' ') - elif prev[signpos] == '-': - self._isNeg = True - else: - self._isNeg = False - - # Determine where they stop differing in "undo" result: - sm = difflib.SequenceMatcher(None, a=value, b=prev) - i, j, k = sm.find_longest_match(sel_start, length, sel_start, length) -## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) - - if k == 0: # no match found; select to end - sel_to = length - else: - code_5tuples = sm.get_opcodes() - for op, i1, i2, j1, j2 in code_5tuples: -## dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) - pass - - diff_found = False - # look backward through operations needed to produce "previous" value; - # first change wins: - for next_op in range(len(code_5tuples)-1, -1, -1): - op, i1, i2, j1, j2 = code_5tuples[next_op] -## dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) - if op == 'insert' and prev[j1:j2] != self._template[j1:j2]: -## dbg('insert found: selection =>', (j1, j2)) - sel_start = j1 - sel_to = j2 - diff_found = True - break - elif op == 'delete' and value[i1:i2] != self._template[i1:i2]: - field = self._FindField(i2) - edit_start, edit_end = field._extent - if field._insertRight and i2 == edit_end: - sel_start = i2 - sel_to = i2 - else: - sel_start = i1 - sel_to = j1 -## dbg('delete found: selection =>', (sel_start, sel_to)) - diff_found = True - break - elif op == 'replace': -## dbg('replace found: selection =>', (j1, j2)) - sel_start = j1 - sel_to = j2 - diff_found = True - break - - - if diff_found: - # now go forwards, looking for earlier changes: - for next_op in range(len(code_5tuples)): - op, i1, i2, j1, j2 = code_5tuples[next_op] - field = self._FindField(i1) - if op == 'equal': - continue - elif op == 'replace': -## dbg('setting sel_start to', i1) - sel_start = i1 - break - elif op == 'insert' and not value[i1:i2]: -## dbg('forward %s found' % op) - if prev[j1:j2].strip(): -## dbg('item to insert non-empty; setting sel_start to', j1) - sel_start = j1 - break - elif not field._insertRight: -## dbg('setting sel_start to inserted space:', j1) - sel_start = j1 - break - elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip(): - continue - else: - # we've got what we need - break - - - if not diff_found: -## dbg('no insert,delete or replace found (!)') - # do "left-insert"-centric processing of difference based on l.c.s.: - if i == j and j != sel_start: # match starts after start of selection - sel_to = sel_start + (j-sel_start) # select to start of match - else: - sel_to = j # (change ends at j) - - - # There are several situations where the calculated difference is - # not what we want to select. If changing sign, or just adding - # group characters, we really don't want to highlight the characters - # changed, but instead leave the cursor where it is. - # Also, there a situations in which the difference can be ambiguous; - # Consider: - # - # current value: 11234 - # previous value: 1111234 - # - # Where did the cursor actually lie and which 1s were selected on the delete - # operation? - # - # Also, difflib can "get it wrong;" Consider: - # - # current value: " 128.66" - # previous value: " 121.86" - # - # difflib produces the following opcodes, which are sub-optimal: - # equal value[0:9] ( 12) prev[0:9] ( 12) - # insert value[9:9] () prev[9:11] (1.) - # equal value[9:10] (8) prev[11:12] (8) - # delete value[10:11] (.) prev[12:12] () - # equal value[11:12] (6) prev[12:13] (6) - # delete value[12:13] (6) prev[13:13] () - # - # This should have been: - # equal value[0:9] ( 12) prev[0:9] ( 12) - # replace value[9:11] (8.6) prev[9:11] (1.8) - # equal value[12:13] (6) prev[12:13] (6) - # - # But it didn't figure this out! - # - # To get all this right, we use the previous selection recorded to help us... - - if (sel_start, sel_to) != self._prevSelection: -## dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) - - prev_sel_start, prev_sel_to = self._prevSelection - field = self._FindField(sel_start) - - if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')') - or self._curValue[sel_start] in ('-', '(', ')')): - # change of sign; leave cursor alone... - sel_start, sel_to = self._prevSelection - - elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar - or self._prevValue[sel_start:sel_to] == field._groupChar): - # do not highlight grouping changes - sel_start, sel_to = self._prevSelection - - else: - calc_select_len = sel_to - sel_start - prev_select_len = prev_sel_to - prev_sel_start - -## dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) -## dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) - - if prev_select_len >= calc_select_len: - # old selection was bigger; trust it: - sel_start, sel_to = self._prevSelection - - elif( sel_to > prev_sel_to # calculated select past last selection - and prev_sel_to < len(self._template) # and prev_sel_to not at end of control - and sel_to == len(self._template) ): # and calculated selection goes to end of control - - i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length) -## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) - if k > 0: - # difflib must not have optimized opcodes properly; - sel_to = j - - else: - # look for possible ambiguous diff: - - # if last change resulted in no selection, test from resulting cursor position: - if prev_sel_start == prev_sel_to: - calc_select_len = sel_to - sel_start - field = self._FindField(prev_sel_start) - - # determine which way to search from last cursor position for ambiguous change: - if field._insertRight: - test_sel_start = prev_sel_start - test_sel_to = prev_sel_start + calc_select_len - else: - test_sel_start = prev_sel_start - calc_select_len - test_sel_to = prev_sel_start - else: - test_sel_start, test_sel_to = prev_sel_start, prev_sel_to - -## dbg('test selection:', (test_sel_start, test_sel_to)) -## dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to]) -## dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to]) - - # if calculated selection spans characters, and same characters - # "before" the previous insertion point are present there as well, - # select the ones related to the last known selection instead. - if( sel_start != sel_to - and test_sel_to < len(self._template) - and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ): - - sel_start, sel_to = test_sel_start, test_sel_to - -## dbg('sel_start, sel_to:', sel_start, sel_to) -## dbg('previous value: "%s"' % self._prevValue) - self._SetValue(self._prevValue) - self._SetInsertionPoint(sel_start) - self._SetSelection(sel_start, sel_to) - else: -## dbg('no difference between previous value') - pass -## dbg(indent=0) - - - def _OnClear(self, event): - """ Provides an action for context menu delete operation """ - self.ClearValue() - - - def _OnContextMenu(self, event): -## dbg('MaskedEditMixin::OnContextMenu()', indent=1) - menu = wxMenu() - menu.Append(wxID_UNDO, "Undo", "") - menu.AppendSeparator() - menu.Append(wxID_CUT, "Cut", "") - menu.Append(wxID_COPY, "Copy", "") - menu.Append(wxID_PASTE, "Paste", "") - menu.Append(wxID_CLEAR, "Delete", "") - menu.AppendSeparator() - menu.Append(wxID_SELECTALL, "Select All", "") - - EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z) - EVT_MENU(menu, wxID_CUT, self._OnCtrl_X) - EVT_MENU(menu, wxID_COPY, self._OnCtrl_C) - EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V) - EVT_MENU(menu, wxID_CLEAR, self._OnClear) - EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A) - - # ## WSS: The base control apparently handles - # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE - # and wxID_CLEAR menu items even if the menu is one - # we created. However, it doesn't do undo properly, - # so we're keeping track of previous values ourselves. - # Therefore, we have to override the default update for - # that item on the menu: - EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI) - self._contextMenu = menu - - self.PopupMenu(menu, event.GetPosition()) - menu.Destroy() - self._contextMenu = None -## dbg(indent=0) - - def _UndoUpdateUI(self, event): - if self._prevValue is None or self._prevValue == self._curValue: - self._contextMenu.Enable(wxID_UNDO, False) - else: - self._contextMenu.Enable(wxID_UNDO, True) - - - def _OnCtrlParametersChanged(self): - """ - Overridable function to allow derived classes to take action as a - result of parameter changes prior to possibly changing the value - of the control. - """ - pass - - ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- -# ## TRICKY BIT: to avoid a ton of boiler-plate, and to -# ## automate the getter/setter generation for each valid -# ## control parameter so we never forget to add the -# ## functions when adding parameters, this loop -# ## programmatically adds them to the class: -# ## (This makes it easier for Designers like Boa to -# ## deal with masked controls.) -# -# ## To further complicate matters, this is done with an -# ## extra level of inheritance, so that "general" classes like -# ## MaskedTextCtrl can have all possible attributes, -# ## while derived classes, like TimeCtrl and MaskedNumCtrl -# ## can prevent exposure of those optional attributes of their base -# ## class that do not make sense for their derivation. Therefore, -# ## we define -# ## BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) -# ## and -# ## MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). -# ## -# ## This allows us to then derive: -# ## MaskedNumCtrl( BaseMaskedTextCtrl ) -# ## -# ## and not have to expose all the same accessor functions for the -# ## derived control when they don't all make sense for it. -# ## -class MaskedEditAccessorsMixin: - - # Define the default set of attributes exposed by the most generic masked controls: - exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys() - exposed_basectrl_params.remove('index') - exposed_basectrl_params.remove('extent') - exposed_basectrl_params.remove('foregroundColour') # (base class already has this) - - for param in exposed_basectrl_params: - propname = param[0].upper() + param[1:] - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - if param.find('Colour') != -1: - # add non-british spellings, for backward-compatibility - propname.replace('Colour', 'Color') - - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - - - -class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): - """ - This is the primary derivation from MaskedEditMixin. It provides - a general masked text control that can be configured with different - masks. It's actually a "base masked textCtrl", so that the - MaskedTextCtrl class can be derived from it, and add those - accessor functions to it that are appropriate to the general class, - whilst other classes can derive from BaseMaskedTextCtrl, and - only define those accessor functions that are appropriate for - those derivations. - """ - - def __init__( self, parent, id=-1, value = '', - pos = wx.DefaultPosition, - size = wx.DefaultSize, - style = wx.TE_PROCESS_TAB, - validator=wx.DefaultValidator, ## placeholder provided for data-transfer logic - name = 'maskedTextCtrl', - setupEventHandling = True, ## setup event handling by default - **kwargs): - - wx.TextCtrl.__init__(self, parent, id, value='', - pos=pos, size = size, - style=style, validator=validator, - name=name) - - self.controlInitialized = True - MaskedEditMixin.__init__( self, name, **kwargs ) - - self._SetInitialValue(value) - - if setupEventHandling: - ## Setup event handlers - self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection - self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator - self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick - self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. - self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress - self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep - ## track of previous value for undo - - - def __repr__(self): - return "" % self.GetValue() - - - def _GetSelection(self): - """ - Allow mixin to get the text selection of this control. - REQUIRED by any class derived from MaskedEditMixin. - """ - return self.GetSelection() - - def _SetSelection(self, sel_start, sel_to): - """ - Allow mixin to set the text selection of this control. - REQUIRED by any class derived from MaskedEditMixin. - """ -#### dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) - return self.SetSelection( sel_start, sel_to ) - - def SetSelection(self, sel_start, sel_to): - """ - This is just for debugging... - """ -## dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) - wx.TextCtrl.SetSelection(self, sel_start, sel_to) - - - def _GetInsertionPoint(self): - return self.GetInsertionPoint() - - def _SetInsertionPoint(self, pos): -#### dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals()) - self.SetInsertionPoint(pos) - - def SetInsertionPoint(self, pos): - """ - This is just for debugging... - """ -## dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals()) - wx.TextCtrl.SetInsertionPoint(self, pos) - - - def _GetValue(self): - """ - Allow mixin to get the raw value of the control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - return self.GetValue() - - def _SetValue(self, value): - """ - Allow mixin to set the raw value of the control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ -## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1) - # Record current selection and insertion point, for undo - self._prevSelection = self._GetSelection() - self._prevInsertionPoint = self._GetInsertionPoint() - wx.TextCtrl.SetValue(self, value) -## dbg(indent=0) - - def SetValue(self, value): - """ - This function redefines the externally accessible .SetValue to be - a smart "paste" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ -## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1) - - if not self._mask: - wx.TextCtrl.SetValue(self, value) # revert to base control behavior - return - - # empty previous contents, replacing entire value: - self._SetInsertionPoint(0) - self._SetSelection(0, self._masklength) - if self._signOk and self._useParens: - signpos = value.find('-') - if signpos != -1: - value = value[:signpos] + '(' + value[signpos+1:].strip() + ')' - elif value.find(')') == -1 and len(value) < self._masklength: - value += ' ' # add place holder for reserved space for right paren - - if( len(value) < self._masklength # value shorter than control - and (self._isFloat or self._isInt) # and it's a numeric control - and self._ctrl_constraints._alignRight ): # and it's a right-aligned control - -## dbg('len(value)', len(value), ' < self._masklength', self._masklength) - # try to intelligently "pad out" the value to the right size: - value = self._template[0:self._masklength - len(value)] + value - if self._isFloat and value.find('.') == -1: - value = value[1:] -## dbg('padded value = "%s"' % value) - - # make SetValue behave the same as if you had typed the value in: - try: - value = self._Paste(value, raise_on_invalid=True, just_return_value=True) - if self._isFloat: - self._isNeg = False # (clear current assumptions) - value = self._adjustFloat(value) - elif self._isInt: - self._isNeg = False # (clear current assumptions) - value = self._adjustInt(value) - elif self._isDate and not self.IsValid(value) and self._4digityear: - value = self._adjustDate(value, fixcentury=True) - except ValueError: - # If date, year might be 2 digits vs. 4; try adjusting it: - if self._isDate and self._4digityear: - dateparts = value.split(' ') - dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) - value = string.join(dateparts, ' ') -## dbg('adjusted value: "%s"' % value) - value = self._Paste(value, raise_on_invalid=True, just_return_value=True) - else: -## dbg('exception thrown', indent=0) - raise - - self._SetValue(value) # note: to preserve similar capability, .SetValue() - # does not change IsModified() -#### dbg('queuing insertion after .SetValue', self._masklength) - wx.CallAfter(self._SetInsertionPoint, self._masklength) - wx.CallAfter(self._SetSelection, self._masklength, self._masklength) -## dbg(indent=0) - - - def Clear(self): - """ Blanks the current control value by replacing it with the default value.""" -## dbg("MaskedTextCtrl::Clear - value reset to default value (template)") - if self._mask: - self.ClearValue() - else: - wx.TextCtrl.Clear(self) # else revert to base control behavior - - - def _Refresh(self): - """ - Allow mixin to refresh the base control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ -## dbg('MaskedTextCtrl::_Refresh', indent=1) - wx.TextCtrl.Refresh(self) -## dbg(indent=0) - - - def Refresh(self): - """ - This function redefines the externally accessible .Refresh() to - validate the contents of the masked control as it refreshes. - NOTE: this must be done in the class derived from the base wx control. - """ -## dbg('MaskedTextCtrl::Refresh', indent=1) - self._CheckValid() - self._Refresh() -## dbg(indent=0) - - - def _IsEditable(self): - """ - Allow mixin to determine if the base control is editable with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - return wx.TextCtrl.IsEditable(self) - - - def Cut(self): - """ - This function redefines the externally accessible .Cut to be - a smart "erase" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ - if self._mask: - self._Cut() # call the mixin's Cut method - else: - wx.TextCtrl.Cut(self) # else revert to base control behavior - - - def Paste(self): - """ - This function redefines the externally accessible .Paste to be - a smart "paste" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ - if self._mask: - self._Paste() # call the mixin's Paste method - else: - wx.TextCtrl.Paste(self, value) # else revert to base control behavior - - - def Undo(self): - """ - This function defines the undo operation for the control. (The default - undo is 1-deep.) - """ - if self._mask: - self._Undo() - else: - wx.TextCtrl.Undo(self) # else revert to base control behavior - - - def IsModified(self): - """ - This function overrides the raw wxTextCtrl method, because the - masked edit mixin uses SetValue to change the value, which doesn't - modify the state of this attribute. So, we keep track on each - keystroke to see if the value changes, and if so, it's been - modified. - """ - return wx.TextCtrl.IsModified(self) or self.modified - - - def _CalcSize(self, size=None): - """ - Calculate automatic size if allowed; use base mixin function. - """ - return self._calcSize(size) - - -class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): - """ - This extra level of inheritance allows us to add the generic set of - masked edit parameters only to this class while allowing other - classes to derive from the "base" masked text control, and provide - a smaller set of valid accessor functions. - """ - pass - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- -## Because calling SetSelection programmatically does not fire EVT_COMBOBOX -## events, we have to do it ourselves when we auto-complete. -class MaskedComboBoxSelectEvent(wx.PyCommandEvent): - def __init__(self, id, selection = 0, object=None): - wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id) - - self.__selection = selection - self.SetEventObject(object) - - def GetSelection(self): - """Retrieve the value of the control at the time - this event was generated.""" - return self.__selection - - -class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): - """ - This masked edit control adds the ability to use a masked input - on a combobox, and do auto-complete of such values. - """ - def __init__( self, parent, id=-1, value = '', - pos = wx.DefaultPosition, - size = wx.DefaultSize, - choices = [], - style = wx.CB_DROPDOWN, - validator = wx.DefaultValidator, - name = "maskedComboBox", - setupEventHandling = True, ## setup event handling by default): - **kwargs): - - - # This is necessary, because wxComboBox currently provides no - # method for determining later if this was specified in the - # constructor for the control... - self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY - - kwargs['choices'] = choices ## set up maskededit to work with choice list too - - ## Since combobox completion is case-insensitive, always validate same way - if not kwargs.has_key('compareNoCase'): - kwargs['compareNoCase'] = True - - MaskedEditMixin.__init__( self, name, **kwargs ) - - self._choices = self._ctrl_constraints._choices -## dbg('self._choices:', self._choices) - - if self._ctrl_constraints._alignRight: - choices = [choice.rjust(self._masklength) for choice in choices] - else: - choices = [choice.ljust(self._masklength) for choice in choices] - - wx.ComboBox.__init__(self, parent, id, value='', - pos=pos, size = size, - choices=choices, style=style|wx.WANTS_CHARS, - validator=validator, - name=name) - - self.controlInitialized = True - - # Set control font - fixed width by default - self._setFont() - - if self._autofit: - self.SetClientSize(self._CalcSize()) - - if value: - # ensure value is width of the mask of the control: - if self._ctrl_constraints._alignRight: - value = value.rjust(self._masklength) - else: - value = value.ljust(self._masklength) - - if self.__readonly: - self.SetStringSelection(value) - else: - self._SetInitialValue(value) - - - self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice) - self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice) - - if setupEventHandling: - ## Setup event handlers - self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection - self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator - self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick - self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu - self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress - self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown ) ## for special processing of up/down keys - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys - ## (next in evt chain) - self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep - ## track of previous value for undo - - - - def __repr__(self): - return "" % self.GetValue() - - - def _CalcSize(self, size=None): - """ - Calculate automatic size if allowed; augment base mixin function - to account for the selector button. - """ - size = self._calcSize(size) - return (size[0]+20, size[1]) - - - def _GetSelection(self): - """ - Allow mixin to get the text selection of this control. - REQUIRED by any class derived from MaskedEditMixin. - """ - return self.GetMark() - - def _SetSelection(self, sel_start, sel_to): - """ - Allow mixin to set the text selection of this control. - REQUIRED by any class derived from MaskedEditMixin. - """ - return self.SetMark( sel_start, sel_to ) - - - def _GetInsertionPoint(self): - return self.GetInsertionPoint() - - def _SetInsertionPoint(self, pos): - self.SetInsertionPoint(pos) - - - def _GetValue(self): - """ - Allow mixin to get the raw value of the control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - return self.GetValue() - - def _SetValue(self, value): - """ - Allow mixin to set the raw value of the control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - # For wxComboBox, ensure that values are properly padded so that - # if varying length choices are supplied, they always show up - # in the window properly, and will be the appropriate length - # to match the mask: - if self._ctrl_constraints._alignRight: - value = value.rjust(self._masklength) - else: - value = value.ljust(self._masklength) - - # Record current selection and insertion point, for undo - self._prevSelection = self._GetSelection() - self._prevInsertionPoint = self._GetInsertionPoint() - wx.ComboBox.SetValue(self, value) - # text change events don't always fire, so we check validity here - # to make certain formatting is applied: - self._CheckValid() - - def SetValue(self, value): - """ - This function redefines the externally accessible .SetValue to be - a smart "paste" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ - if not self._mask: - wx.ComboBox.SetValue(value) # revert to base control behavior - return - # else... - # empty previous contents, replacing entire value: - self._SetInsertionPoint(0) - self._SetSelection(0, self._masklength) - - if( len(value) < self._masklength # value shorter than control - and (self._isFloat or self._isInt) # and it's a numeric control - and self._ctrl_constraints._alignRight ): # and it's a right-aligned control - # try to intelligently "pad out" the value to the right size: - value = self._template[0:self._masklength - len(value)] + value -## dbg('padded value = "%s"' % value) - - # For wxComboBox, ensure that values are properly padded so that - # if varying length choices are supplied, they always show up - # in the window properly, and will be the appropriate length - # to match the mask: - elif self._ctrl_constraints._alignRight: - value = value.rjust(self._masklength) - else: - value = value.ljust(self._masklength) - - - # make SetValue behave the same as if you had typed the value in: - try: - value = self._Paste(value, raise_on_invalid=True, just_return_value=True) - if self._isFloat: - self._isNeg = False # (clear current assumptions) - value = self._adjustFloat(value) - elif self._isInt: - self._isNeg = False # (clear current assumptions) - value = self._adjustInt(value) - elif self._isDate and not self.IsValid(value) and self._4digityear: - value = self._adjustDate(value, fixcentury=True) - except ValueError: - # If date, year might be 2 digits vs. 4; try adjusting it: - if self._isDate and self._4digityear: - dateparts = value.split(' ') - dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) - value = string.join(dateparts, ' ') -## dbg('adjusted value: "%s"' % value) - value = self._Paste(value, raise_on_invalid=True, just_return_value=True) - else: - raise - - self._SetValue(value) -#### dbg('queuing insertion after .SetValue', self._masklength) - wx.CallAfter(self._SetInsertionPoint, self._masklength) - wx.CallAfter(self._SetSelection, self._masklength, self._masklength) - - - def _Refresh(self): - """ - Allow mixin to refresh the base control with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - wx.ComboBox.Refresh(self) - - def Refresh(self): - """ - This function redefines the externally accessible .Refresh() to - validate the contents of the masked control as it refreshes. - NOTE: this must be done in the class derived from the base wx control. - """ - self._CheckValid() - self._Refresh() - - - def _IsEditable(self): - """ - Allow mixin to determine if the base control is editable with this function. - REQUIRED by any class derived from MaskedEditMixin. - """ - return not self.__readonly - - - def Cut(self): - """ - This function redefines the externally accessible .Cut to be - a smart "erase" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ - if self._mask: - self._Cut() # call the mixin's Cut method - else: - wx.ComboBox.Cut(self) # else revert to base control behavior - - - def Paste(self): - """ - This function redefines the externally accessible .Paste to be - a smart "paste" of the text in question, so as not to corrupt the - masked control. NOTE: this must be done in the class derived - from the base wx control. - """ - if self._mask: - self._Paste() # call the mixin's Paste method - else: - wx.ComboBox.Paste(self) # else revert to base control behavior - - - def Undo(self): - """ - This function defines the undo operation for the control. (The default - undo is 1-deep.) - """ - if self._mask: - self._Undo() - else: - wx.ComboBox.Undo() # else revert to base control behavior - - - def Append( self, choice, clientData=None ): - """ - This function override is necessary so we can keep track of any additions to the list - of choices, because wxComboBox doesn't have an accessor for the choice list. - The code here is the same as in the SetParameters() mixin function, but is - done for the individual value as appended, so the list can be built incrementally - without speed penalty. - """ - if self._mask: - if type(choice) not in (types.StringType, types.UnicodeType): - raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) - elif not self.IsValid(choice): - raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) - - if not self._ctrl_constraints._choices: - self._ctrl_constraints._compareChoices = [] - self._ctrl_constraints._choices = [] - self._hasList = True - - compareChoice = choice.strip() - - if self._ctrl_constraints._compareNoCase: - compareChoice = compareChoice.lower() - - if self._ctrl_constraints._alignRight: - choice = choice.rjust(self._masklength) - else: - choice = choice.ljust(self._masklength) - if self._ctrl_constraints._fillChar != ' ': - choice = choice.replace(' ', self._fillChar) -## dbg('updated choice:', choice) - - - self._ctrl_constraints._compareChoices.append(compareChoice) - self._ctrl_constraints._choices.append(choice) - self._choices = self._ctrl_constraints._choices # (for shorthand) - - if( not self.IsValid(choice) and - (not self._ctrl_constraints.IsEmpty(choice) or - (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ): - raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name)) - - wx.ComboBox.Append(self, choice, clientData) - - - - def Clear( self ): - """ - This function override is necessary so we can keep track of any additions to the list - of choices, because wxComboBox doesn't have an accessor for the choice list. - """ - if self._mask: - self._choices = [] - self._ctrl_constraints._autoCompleteIndex = -1 - if self._ctrl_constraints._choices: - self.SetCtrlParameters(choices=[]) - wx.ComboBox.Clear(self) - - - def _OnCtrlParametersChanged(self): - """ - Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so - we can update the base control: - """ - if self.controlInitialized and self._choices != self._ctrl_constraints._choices: - wx.ComboBox.Clear(self) - self._choices = self._ctrl_constraints._choices - for choice in self._choices: - wx.ComboBox.Append( self, choice ) - - - def GetMark(self): - """ - This function is a hack to make up for the fact that wxComboBox has no - method for returning the selected portion of its edit control. It - works, but has the nasty side effect of generating lots of intermediate - events. - """ -## dbg(suspend=1) # turn off debugging around this function -## dbg('MaskedComboBox::GetMark', indent=1) - if self.__readonly: -## dbg(indent=0) - return 0, 0 # no selection possible for editing -## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have! - sel_start = sel_to = self.GetInsertionPoint() -## dbg("current sel_start:", sel_start) - value = self.GetValue() -## dbg('value: "%s"' % value) - - self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any) - - wx.ComboBox.Cut(self) - newvalue = self.GetValue() -## dbg("value after Cut operation:", newvalue) - - if newvalue != value: # something was selected; calculate extent -## dbg("something selected") - sel_to = sel_start + len(value) - len(newvalue) - wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change) - wx.ComboBox.SetInsertionPoint(self, sel_start) - wx.ComboBox.SetMark(self, sel_start, sel_to) - - self._ignoreChange = False # tell _OnTextChange() to pay attn again - -## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) - return sel_start, sel_to - - - def SetSelection(self, index): - """ - Necessary for bookkeeping on choice selection, to keep current value - current. - """ -## dbg('MaskedComboBox::SetSelection(%d)' % index) - if self._mask: - self._prevValue = self._curValue - self._curValue = self._choices[index] - self._ctrl_constraints._autoCompleteIndex = index - wx.ComboBox.SetSelection(self, index) - - - def OnKeyDown(self, event): - """ - This function is necessary because navigation and control key - events do not seem to normally be seen by the wxComboBox's - EVT_CHAR routine. (Tabs don't seem to be visible no matter - what... {:-( ) - """ - if event.GetKeyCode() in self._nav + self._control: - self._OnChar(event) - return - else: - event.Skip() # let mixin default KeyDown behavior occur - - - def OnSelectChoice(self, event): - """ - This function appears to be necessary, because the processing done - on the text of the control somehow interferes with the combobox's - selection mechanism for the arrow keys. - """ -## dbg('MaskedComboBox::OnSelectChoice', indent=1) - - if not self._mask: - event.Skip() - return - - value = self.GetValue().strip() - - if self._ctrl_constraints._compareNoCase: - value = value.lower() - - if event.GetKeyCode() == wx.WXK_UP: - direction = -1 - else: - direction = 1 - match_index, partial_match = self._autoComplete( - direction, - self._ctrl_constraints._compareChoices, - value, - self._ctrl_constraints._compareNoCase, - current_index = self._ctrl_constraints._autoCompleteIndex) - if match_index is not None: -## dbg('setting selection to', match_index) - # issue appropriate event to outside: - self._OnAutoSelect(self._ctrl_constraints, match_index=match_index) - self._CheckValid() - keep_processing = False - else: - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - field = self._FindField(pos) - if self.IsEmpty() or not field._hasList: -## dbg('selecting 1st value in list') - self._OnAutoSelect(self._ctrl_constraints, match_index=0) - self._CheckValid() - keep_processing = False - else: - # attempt field-level auto-complete -## dbg(indent=0) - keep_processing = self._OnAutoCompleteField(event) -## dbg('keep processing?', keep_processing, indent=0) - return keep_processing - - - def _OnAutoSelect(self, field, match_index): - """ - Override mixin (empty) autocomplete handler, so that autocompletion causes - combobox to update appropriately. - """ -## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) -## field._autoCompleteIndex = match_index - if field == self._ctrl_constraints: - self.SetSelection(match_index) -## dbg('issuing combo selection event') - self.GetEventHandler().ProcessEvent( - MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) ) - self._CheckValid() -## dbg('field._autoCompleteIndex:', match_index) -## dbg('self.GetSelection():', self.GetSelection()) -## dbg(indent=0) - - - def _OnReturn(self, event): - """ - For wxComboBox, it seems that if you hit return when the dropdown is - dropped, the event that dismisses the dropdown will also blank the - control, because of the implementation of wxComboBox. So here, - we look and if the selection is -1, and the value according to - (the base control!) is a value in the list, then we schedule a - programmatic wxComboBox.SetSelection() call to pick the appropriate - item in the list. (and then do the usual OnReturn bit.) - """ -## dbg('MaskedComboBox::OnReturn', indent=1) -## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) - if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: - wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) - - event.m_keyCode = wx.WXK_TAB - event.Skip() -## dbg(indent=0) - - -class MaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): - """ - This extra level of inheritance allows us to add the generic set of - masked edit parameters only to this class while allowing other - classes to derive from the "base" masked combobox control, and provide - a smaller set of valid accessor functions. - """ - pass - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -class IpAddrCtrlAccessorsMixin: - # Define IpAddrCtrl's list of attributes having their own - # Get/Set functions, exposing only those that make sense for - # an IP address control. - - exposed_basectrl_params = ( - 'fields', - 'retainFieldValidation', - 'formatcodes', - 'fillChar', - 'defaultValue', - 'description', - - 'useFixedWidthFont', - 'signedForegroundColour', - 'emptyBackgroundColour', - 'validBackgroundColour', - 'invalidBackgroundColour', - - 'emptyInvalid', - 'validFunc', - 'validRequired', - ) - - for param in exposed_basectrl_params: - propname = param[0].upper() + param[1:] - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - if param.find('Colour') != -1: - # add non-british spellings, for backward-compatibility - propname.replace('Colour', 'Color') - - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - -class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): - """ - This class is a particular type of MaskedTextCtrl that accepts - and understands the semantics of IP addresses, reformats input - as you move from field to field, and accepts '.' as a navigation - character, so that typing an IP address can be done naturally. - """ - - - - def __init__( self, parent, id=-1, value = '', - pos = wx.DefaultPosition, - size = wx.DefaultSize, - style = wx.TE_PROCESS_TAB, - validator = wx.DefaultValidator, - name = 'IpAddrCtrl', - setupEventHandling = True, ## setup event handling by default - **kwargs): - - if not kwargs.has_key('mask'): - kwargs['mask'] = mask = "###.###.###.###" - if not kwargs.has_key('formatcodes'): - kwargs['formatcodes'] = 'F_Sr<' - if not kwargs.has_key('validRegex'): - kwargs['validRegex'] = "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}" - - - BaseMaskedTextCtrl.__init__( - self, parent, id=id, value = value, - pos=pos, size=size, - style = style, - validator = validator, - name = name, - setupEventHandling = setupEventHandling, - **kwargs) - - - # set up individual field parameters as well: - field_params = {} - field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))" - - # require "valid" string; this prevents entry of any value > 255, but allows - # intermediate constructions; overall control validation requires well-formatted value. - field_params['formatcodes'] = 'V' - - if field_params: - for i in self._field_indices: - self.SetFieldParameters(i, **field_params) - - # This makes '.' act like tab: - self._AddNavKey('.', handler=self.OnDot) - self._AddNavKey('>', handler=self.OnDot) # for "shift-." - - - def OnDot(self, event): -## dbg('IpAddrCtrl::OnDot', indent=1) - pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) - oldvalue = self.GetValue() - edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) - if not event.ShiftDown(): - if pos > edit_start and pos < edit_end: - # clip data in field to the right of pos, if adjusting fields - # when not at delimeter; (assumption == they hit '.') - newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:] - self._SetValue(newvalue) - self._SetInsertionPoint(pos) -## dbg(indent=0) - return self._OnChangeField(event) - - - - def GetAddress(self): - value = BaseMaskedTextCtrl.GetValue(self) - return value.replace(' ','') # remove spaces from the value - - - def _OnCtrl_S(self, event): -## dbg("IpAddrCtrl::_OnCtrl_S") - if self._demo: - print "value:", self.GetAddress() - return False - - def SetValue(self, value): -## dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1) - if type(value) not in (types.StringType, types.UnicodeType): -## dbg(indent=0) - raise ValueError('%s must be a string', str(value)) - - bValid = True # assume True - parts = value.split('.') - if len(parts) != 4: - bValid = False - else: - for i in range(4): - part = parts[i] - if not 0 <= len(part) <= 3: - bValid = False - break - elif part.strip(): # non-empty part - try: - j = string.atoi(part) - if not 0 <= j <= 255: - bValid = False - break - else: - parts[i] = '%3d' % j - except: - bValid = False - break - else: - # allow empty sections for SetValue (will result in "invalid" value, - # but this may be useful for initializing the control: - parts[i] = ' ' # convert empty field to 3-char length - - if not bValid: -## dbg(indent=0) - raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value)) - else: -## dbg('parts:', parts) - value = string.join(parts, '.') - BaseMaskedTextCtrl.SetValue(self, value) -## dbg(indent=0) - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- -## these are helper subroutines: - -def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): - """ addseparators = add separator character every three numerals if True - """ - fmt0 = fmtstring.split('.') - fmt1 = fmt0[0] - fmt2 = fmt0[1] - val = origvalue.split('.')[0].strip() - ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) - if neg: - ret = '-' + ret[1:] - return (ret,len(fmt1)) - - -def isDateType( fmtstring ): - """ Checks the mask and returns True if it fits an allowed - date or datetime format. - """ - dateMasks = ("^##/##/####", - "^##-##-####", - "^##.##.####", - "^####/##/##", - "^####-##-##", - "^####.##.##", - "^##/CCC/####", - "^##.CCC.####", - "^##/##/##$", - "^##/##/## ", - "^##/CCC/##$", - "^##.CCC.## ",) - reString = "|".join(dateMasks) - filter = re.compile( reString) - if re.match(filter,fmtstring): return True - return False - -def isTimeType( fmtstring ): - """ Checks the mask and returns True if it fits an allowed - time format. - """ - reTimeMask = "^##:##(:##)?( (AM|PM))?" - filter = re.compile( reTimeMask ) - if re.match(filter,fmtstring): return True - return False - - -def isFloatingPoint( fmtstring): - filter = re.compile("[ ]?[#]+\.[#]+\n") - if re.match(filter,fmtstring+"\n"): return True - return False - - -def isInteger( fmtstring ): - filter = re.compile("[#]+\n") - if re.match(filter,fmtstring+"\n"): return True - return False - - -def getDateParts( dateStr, dateFmt ): - if len(dateStr) > 11: clip = dateStr[0:11] - else: clip = dateStr - if clip[-2] not in string.digits: - clip = clip[:-1] # (got part of time; drop it) - - dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') - slices = clip.split(dateSep) - if dateFmt == "MDY": - y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts - elif dateFmt == "DMY": - y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts - elif dateFmt == "YMD": - y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts - else: - y,m,d = None, None, None - if not y: - return None - else: - return y,m,d - - -def getDateSepChar(dateStr): - clip = dateStr[0:10] - dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') - return dateSep - - -def makeDate( year, month, day, dateFmt, dateStr): - sep = getDateSepChar( dateStr) - if dateFmt == "MDY": - return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts - elif dateFmt == "DMY": - return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts - elif dateFmt == "YMD": - return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts - else: - return none - - -def getYear(dateStr,dateFmt): - parts = getDateParts( dateStr, dateFmt) - return parts[0] - -def getMonth(dateStr,dateFmt): - parts = getDateParts( dateStr, dateFmt) - return parts[1] - -def getDay(dateStr,dateFmt): - parts = getDateParts( dateStr, dateFmt) - return parts[2] - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- -class test(wx.PySimpleApp): - def OnInit(self): - from wx.lib.rcsizer import RowColSizer - self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) - self.panel = wx.Panel( self.frame, -1) - self.sizer = RowColSizer() - self.labels = [] - self.editList = [] - rowcount = 4 - - id, id1 = wx.NewId(), wx.NewId() - self.command1 = wx.Button( self.panel, id, "&Close" ) - self.command2 = wx.Button( self.panel, id1, "&AutoFormats" ) - self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) - self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) - self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) -## self.panel.SetDefaultItem(self.command1 ) - self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) - - self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) - self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) - self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) - self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) - self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) - self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) - - - label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. -Note that all controls have been auto-sized by including F in the format code. -Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" - label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." - - self.label1 = wx.StaticText( self.panel, -1, label) - self.label2 = wx.StaticText( self.panel, -1, "Description") - self.label3 = wx.StaticText( self.panel, -1, "Mask Value") - self.label4 = wx.StaticText( self.panel, -1, "Format") - self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") - self.label6 = wx.StaticText( self.panel, -1, "wxMaskedEdit Ctrl") - self.label7 = wx.StaticText( self.panel, -1, label2) - self.label7.SetForegroundColour("Blue") - self.label1.SetForegroundColour("Blue") - self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - - self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) - self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) - self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) - self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) - self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) - self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) - self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) - - # The following list is of the controls for the demo. Feel free to play around with - # the options! - controls = [ - #description mask excl format regexp range,list,initial - ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''), - ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''), - ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''), - ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''), - ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''), - ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'), - ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''), - ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'), - ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''), - ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '), - ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '), - ("Month selector", "XXX", "", 'F', "", (), - ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), - ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (), - ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") - ] - - for control in controls: - self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) - self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) - self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) - self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) - - if control in controls[:]:#-2]: - newControl = MaskedTextCtrl( self.panel, -1, "", - mask = control[1], - excludeChars = control[2], - formatcodes = control[3], - includeChars = "", - validRegex = control[4], - validRange = control[5], - choices = control[6], - defaultValue = control[7], - demo = True) - if control[6]: newControl.SetCtrlParameters(choiceRequired = True) - else: - newControl = MaskedComboBox( self.panel, -1, "", - choices = control[7], - choiceRequired = True, - mask = control[1], - formatcodes = control[3], - excludeChars = control[2], - includeChars = "", - validRegex = control[4], - validRange = control[5], - demo = True) - self.editList.append( newControl ) - - self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) - rowcount += 1 - - self.sizer.AddGrowableCol(4) - - self.panel.SetSizer(self.sizer) - self.panel.SetAutoLayout(1) - - self.frame.Show(1) - self.MainLoop() - - return True - - def onClick(self, event): - self.frame.Close() - - def onClickPage(self, event): - self.page2 = test2(self.frame,-1,"") - self.page2.Show(True) - - def _onCheck1(self,event): - """ Set required value on/off """ - value = event.IsChecked() - if value: - for control in self.editList: - control.SetCtrlParameters(emptyInvalid=True) - control.Refresh() - else: - for control in self.editList: - control.SetCtrlParameters(emptyInvalid=False) - control.Refresh() - self.panel.Refresh() - - def _onCheck2(self,event): - """ Highlight empty values""" - value = event.IsChecked() - if value: - for control in self.editList: - control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') - control.Refresh() - else: - for control in self.editList: - control.SetCtrlParameters( emptyBackgroundColour = 'White') - control.Refresh() - self.panel.Refresh() - - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -class test2(wx.Frame): - def __init__(self, parent, id, caption): - wx.Frame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) - from wx.lib.rcsizer import RowColSizer - self.panel = wx.Panel( self, -1) - self.sizer = RowColSizer() - self.labels = [] - self.texts = [] - rowcount = 4 - - label = """\ -All these controls have been created by passing a single parameter, the AutoFormat code. -The class contains an internal dictionary of types and formats (autoformats). -To see a great example of validations in action, try entering a bad email address, then tab out.""" - - self.label1 = wx.StaticText( self.panel, -1, label) - self.label2 = wx.StaticText( self.panel, -1, "Description") - self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") - self.label4 = wx.StaticText( self.panel, -1, "wxMaskedEdit Control") - self.label1.SetForegroundColour("Blue") - self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) - - self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) - self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) - self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) - self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) - - id, id1 = wx.NewId(), wx.NewId() - self.command1 = wx.Button( self.panel, id, "&Close") - self.command2 = wx.Button( self.panel, id1, "&Print Formats") - self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) - self.panel.SetDefaultItem(self.command1) - self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) - - # The following list is of the controls for the demo. Feel free to play around with - # the options! - controls = [ - ("Phone No","USPHONEFULLEXT"), - ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), - ("US Date MMDDYYYY","USDATEMMDDYYYY/"), - ("Time (with seconds)","TIMEHHMMSS"), - ("Military Time\n(without seconds)","24HRTIMEHHMM"), - ("Social Sec#","USSOCIALSEC"), - ("Credit Card","CREDITCARD"), - ("Expiration MM/YY","EXPDATEMMYY"), - ("Percentage","PERCENT"), - ("Person's Age","AGE"), - ("US Zip Code","USZIP"), - ("US Zip+4","USZIPPLUS4"), - ("Email Address","EMAIL"), - ("IP Address", "(derived control IpAddrCtrl)") - ] - - for control in controls: - self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) - self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) - if control in controls[:-1]: - self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", - autoformat = control[1], - demo = True), - row=rowcount,col=2,flag=wx.ALL,border=5) - else: - self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), - row=rowcount,col=2,flag=wx.ALL,border=5) - rowcount += 1 - - self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) - self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) - self.sizer.AddGrowableCol(3) - - self.panel.SetSizer(self.sizer) - self.panel.SetAutoLayout(1) - - def onClick(self, event): - self.Close() - - def onClickPrint(self, event): - for format in masktags.keys(): - sep = "+------------------------+" - print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) - -## ---------- ---------- ---------- ---------- ---------- ---------- ---------- - -if __name__ == "__main__": - app = test(False) - -i=1 -## -## Current Issues: -## =================================== -## -## 1. WS: For some reason I don't understand, the control is generating two (2) -## EVT_TEXT events for every one (1) .SetValue() of the underlying control. -## I've been unsuccessful in determining why or in my efforts to make just one -## occur. So, I've added a hack to save the last seen value from the -## control in the EVT_TEXT handler, and if *different*, call event.Skip() -## to propagate it down the event chain, and let the application see it. -## -## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the -## behavior of the underlying control that I can't fix. The problems are: -## a) The background coloring doesn't work in the text field of the control; -## instead, there's a only border around it that assumes the correct color. -## b) The control will not pass WXK_TAB to the event handler, no matter what -## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to -## indicate that we want these events. As a result, MaskedComboBox -## doesn't do the nice field-tabbing that MaskedTextCtrl does. -## c) Auto-complete had to be reimplemented for the control because programmatic -## setting of the value of the text field does not set up the auto complete -## the way that the control processing keystrokes does. (But I think I've -## implemented a fairly decent approximation.) Because of this the control -## also won't auto-complete on dropdown, and there's no event I can catch -## to work around this problem. -## d) There is no method provided for getting the selection; the hack I've -## implemented has its flaws, not the least of which is that due to the -## strategy that I'm using, the paste buffer is always replaced by the -## contents of the control's selection when in focus, on each keystroke; -## this makes it impossible to paste anything into a MaskedComboBox -## at the moment... :-( -## e) The other deficient behavior, likely induced by the workaround for (d), -## is that you can can't shift-left to select more than one character -## at a time. -## -## -## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their -## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in -## wxWidgets, shift-tab won't take you backwards through the fields of -## a MaskedTextCtrl like it should. Until then Shifted arrow keys will -## work like shift-tab and tab ought to. -## - -## To-Do's: -## =============================## -## 1. Add Popup list for auto-completable fields that simulates combobox on individual -## fields. Example: City validates against list of cities, or zip vs zip code list. -## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" -## control. -## 3. Fix shift-left selection for MaskedComboBox. -## 5. Transform notion of "decimal control" to be less "entire control"-centric, -## so that monetary symbols can be included and still have the appropriate -## semantics. (Big job, as currently written, but would make control even -## more useful for business applications.) - - -## CHANGELOG: -## ==================== -## Version 1.5 -## (Reported) bugs fixed: -## 1. Crash ensues if you attempt to change the mask of a read-only -## MaskedComboBox after initial construction. -## 2. Changed strategy of defining Get/Set property functions so that -## these are now generated dynamically at runtime, rather than as -## part of the class definition. (This makes it possible to have -## more general base classes that have many more options for configuration -## without requiring that derivations support the same options.) -## 3. Fixed IsModified for _Paste() and _OnErase(). -## -## Enhancements: -## 1. Fixed "attribute function inheritance," since base control is more -## generic than subsequent derivations, not all property functions of a -## generic control should be exposed in those derivations. New strategy -## uses base control classes (eg. BaseMaskedTextCtrl) that should be -## used to derive new class types, and mixed with their own mixins to -## only expose those attributes from the generic masked controls that -## make sense for the derivation. (This makes Boa happier.) -## 2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less -## "parochial." -## -## Version 1.4 -## (Reported) bugs fixed: -## 1. Right-click menu allowed "cut" operation that destroyed mask -## (was implemented by base control) -## 2. MaskedComboBox didn't allow .Append() of mixed-case values; all -## got converted to lower case. -## 3. MaskedComboBox selection didn't deal with spaces in values -## properly when autocompleting, and didn't have a concept of "next" -## match for handling choice list duplicates. -## 4. Size of MaskedComboBox was always default. -## 5. Email address regexp allowed some "non-standard" things, and wasn't -## general enough. -## 6. Couldn't easily reset MaskedComboBox contents programmatically. -## 7. Couldn't set emptyInvalid during construction. -## 8. Under some versions of wxPython, readonly comboboxes can apparently -## return a GetInsertionPoint() result (655535), causing masked control -## to fail. -## 9. Specifying an empty mask caused the controls to traceback. -## 10. Can't specify float ranges for validRange. -## 11. '.' from within a the static portion of a restricted IP address -## destroyed the mask from that point rightward; tab when cursor is -## before 1st field takes cursor past that field. -## -## Enhancements: -## 12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) -## 13. Added auto-select option on char input for masked controls with -## choice lists. -## 14. Added '>' formatcode, allowing insert within a given or each field -## as appropriate, rather than requiring "overwrite". This makes single -## field controls that just have validation rules (eg. EMAIL) much more -## friendly. The same flag controls left shift when deleting vs just -## blanking the value, and for right-insert fields, allows right-insert -## at any non-blank (non-sign) position in the field. -## 15. Added option to use to indicate negative values for numeric controls. -## 16. Improved OnFocus handling of numeric controls. -## 17. Enhanced Home/End processing to allow operation on a field level, -## using ctrl key. -## 18. Added individual Get/Set functions for control parameters, for -## simplified integration with Boa Constructor. -## 19. Standardized "Colour" parameter names to match wxPython, with -## non-british spellings still supported for backward-compatibility. -## 20. Added '&' mask specification character for punctuation only (no letters -## or digits). -## 21. Added (in a separate file) wxMaskedCtrl() factory function to provide -## unified interface to the masked edit subclasses. -## -## -## Version 1.3 -## 1. Made it possible to configure grouping, decimal and shift-decimal characters, -## to make controls more usable internationally. -## 2. Added code to smart "adjust" value strings presented to .SetValue() -## for right-aligned numeric format controls if they are shorter than -## than the control width, prepending the missing portion, prepending control -## template left substring for the missing characters, so that setting -## numeric values is easier. -## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved -## for b-c), as this makes more sense. -## -## Version 1.2 -## 1. Fixed .SetValue() to replace the current value, rather than the current -## selection. Also changed it to generate ValueError if presented with -## either a value which doesn't follow the format or won't fit. Also made -## set value adjust numeric and date controls as if user entered the value. -## Expanded doc explaining how SetValue() works. -## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to -## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. -## 3. Made all date autoformats automatically pick implied "datestyle". -## 4. Added IsModified override, since base wxTextCtrl never reports modified if -## .SetValue used to change the value, which is what the masked edit controls -## use internally. -## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when -## using tab to "leave field" and auto-adjust. -## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, -## and bug in ._Paste() that didn't account for signs in signed masks either. -## 7. Fixed issues with _adjustPos for right-insert fields causing improper -## selection/replacement of values -## 8. Fixed _OnHome handler to properly handle extending current selection to -## beginning of control. -## 9. Exposed all (valid) autoformats to demo, binding descriptions to -## autoformats. -## 10. Fixed a couple of bugs in email regexp. -## 11. Made maskchardict an instance var, to make mask chars to be more -## amenable to international use. -## 12. Clarified meaning of '-' formatcode in doc. -## 13. Fixed a couple of coding bugs being flagged by Python2.1. -## 14. Fixed several issues with sign positioning, erasure and validity -## checking for "numeric" masked controls. -## 15. Added validation to IpAddrCtrl.SetValue(). -## -## Version 1.1 -## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) -## vs. literal font facename, and use wxTELETYPE as the font family -## if so specified. -## 2. Switched to use of dbg module vs. locally defined version. -## 3. Revamped entire control structure to use Field classes to hold constraint -## and formatting data, to make code more hierarchical, allow for more -## sophisticated masked edit construction. -## 4. Better strategy for managing options, and better validation on keywords. -## 5. Added 'V' format code, which requires that in order for a character -## to be accepted, it must result in a string that passes the validRegex. -## 6. Added 'S' format code which means "select entire field when navigating -## to new field." -## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) -## 8. Added '<' format code to allow fields to require explicit cursor movement -## to leave field. -## 9. Added validFunc option to other validation mechanisms, that allows derived -## classes to add dynamic validation constraints to the control. -## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also -## fixed failure to obey case conversion codes when pasting. -## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... -## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' -## 13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, -## individual field validation to prevent field values > 255, and require explicit -## tab/. to change fields. -## 14. Added handler for left double-click to select field under cursor. -## 15. Fixed handling for "Read-only" styles. -## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor -## attribute, for more consistent and controllable coloring. -## 17. Added retainFieldValidation parameter, allowing top-level constraints -## such as "validRequired" to be set independently of field-level equivalent. -## (needed in TimeCtrl for bounds constraints.) -## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed -## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, -## etc. -## 19. Fixed maskchar setting for upper/lowercase, to work in all locales. -## -## -## Version 1.0 -## 1. Decimal point behavior restored for decimal and integer type controls: -## decimal point now trucates the portion > 0. -## 2. Return key now works like the tab character and moves to the next field, -## provided no default button is set for the form panel on which the control -## resides. -## 3. Support added in _FindField() for subclasses controls (like timecontrol) -## to determine where the current insertion point is within the mask (i.e. -## which sub-'field'). See method documentation for more info and examples. -## 4. Added Field class and support for all constraints to be field-specific -## in addition to being globally settable for the control. -## Choices for each field are validated for length and pastability into -## the field in question, raising ValueError if not appropriate for the control. -## Also added selective additional validation based on individual field constraints. -## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all -## auto-complete fields with choice lists, supplying the 1st entry in -## the choice list if the field is empty, and cycling through the list in -## the appropriate direction if already a match. WXK_DOWN will also auto- -## complete if the field is partially completed and a match can be made. -## SHIFT-WXK_UP/DOWN will also take you to the next field after any -## auto-completion performed. -## 5. Added autoCompleteKeycodes=[] parameters for allowing further -## customization of the control. Any keycode supplied as a member -## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If -## requireFieldChoice is set, then a valid value from each non-empty -## choice list will be required for the value of the control to validate. -## 6. Fixed "auto-sizing" to be relative to the font actually used, rather -## than making assumptions about character width. -## 7. Fixed GetMaskParameter(), which was non-functional in previous version. -## 8. Fixed exceptions raised to provide info on which control had the error. -## 9. Fixed bug in choice management of MaskedComboBox. -## 10. Fixed bug in IpAddrCtrl causing traceback if field value was of -## the form '# #'. Modified control code for IpAddrCtrl so that '.' -## in the middle of a field clips the rest of that field, similar to -## decimal and integer controls. -## -## -## Version 0.0.7 -## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. -## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). -## 3. New support for selecting text within the control.(thanks Will Sadkin!) -## Shift-End and Shift-Home now select text as you would expect -## Control-Shift-End selects to the end of the mask string, even if value not entered. -## Control-A selects all *entered* text, Shift-Control-A selects everything in the control. -## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- -## for some reason I couldn't find the original email but thanks!!!) -## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome, -## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. -## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). -## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) -## 7. New mechanism for replacing default behavior for any given key, using -## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available -## for easier subclassing of the control. -## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs -## with insertion point/selection modification. Changed Ctrl-X to use standard "cut" -## semantics, erasing the selection, rather than erasing the entire control. -## 9. Added option for an "default value" (ie. the template) for use when a single fillChar -## is not desired in every position. Added IsDefault() function to mean "does the value -## equal the template?" and modified .IsEmpty() to mean "do all of the editable -## positions in the template == the fillChar?" -## 10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, -## now included. -## 11. MaskedComboBox now adds the capability to validate from list of valid values. -## Example: City validates against list of cities, or zip vs zip code list. -## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being -## passed to the next handler in the event chain, causing updates to the -## control to be invisible to the parent code. -## 13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within -## the control, that auto-reformats as you move between cells. -## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. -## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} -## 16. Fixed major bugs in date validation, due to the fact that -## wxDateTime.ParseDate is too liberal, and will accept any form that -## makes any kind of sense, regardless of the datestyle you specified -## for the control. Unfortunately, the strategy used to fix it only -## works for versions of wxPython post 2.3.3.1, as a C++ assert box -## seems to show up on an invalid date otherwise, instead of a catchable -## exception. -## 17. Enhanced date adjustment to automatically adjust heuristic based on -## current year, making last century/this century determination on -## 2-digit year based on distance between today's year and value; -## if > 50 year separation, assume last century (and don't assume last -## century is 20th.) -## 18. Added autoformats and support for including HHMMSS as well as HHMM for -## date times, and added similar time, and militaray time autoformats. -## 19. Enhanced tabbing logic so that tab takes you to the next field if the -## control is a multi-field control. -## 20. Added stub method called whenever the control "changes fields", that -## can be overridden by subclasses (eg. IpAddrCtrl.) -## 21. Changed a lot of code to be more functionally-oriented so side-effects -## aren't as problematic when maintaining code and/or adding features. -## Eg: IsValid() now does not have side-effects; it merely reflects the -## validity of the value of the control; to determine validity AND recolor -## the control, _CheckValid() should be used with a value argument of None. -## Similarly, made most reformatting function take an optional candidate value -## rather than just using the current value of the control, and only -## have them change the value of the control if a candidate is not specified. -## In this way, you can do validation *before* changing the control. -## 22. Changed validRequired to mean "disallow chars that result in invalid -## value." (Old meaning now represented by emptyInvalid.) (This was -## possible once I'd made the changes in (19) above.) -## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they -## can be set/modified/retrieved after construction. Removed individual -## parameter setting functions, in favor of this mechanism, so that -## all adjustment of the control based on changing parameter values can -## be handled in one place with unified mechanism. -## 24. Did a *lot* of testing and fixing re: numeric values. Added ability -## to type "grouping char" (ie. ',') and validate as appropriate. -## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. -## 26. Fixed assumption about "decimal or integer" masks so that they're only -## made iff there's no validRegex associated with the field. (This -## is so things like zipcodes which look like integers can have more -## restrictive validation (ie. must be 5 digits.) -## 27. Added a ton more doc strings to explain use and derivation requirements -## and did regularization of the naming conventions. -## 28. Fixed a range bug in _adjustKey preventing z from being handled properly. -## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to -## reformat the value and move the next field as appropriate. (shift-'.', -## ie. '>' moves to the previous field. - -## Version 0.0.6 -## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending -## in '0'. -## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the -## year position, the control will expand the value to four digits, using numerals below -## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). -## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} -## 3. revalid parameter renamed validRegex to conform to standard for all validation -## parameters (see 2 new ones below). -## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types. -## Allows the developer to specify a valid low/high range of values. -## 5. New optional init parameter = validList. Used for character types. Allows developer -## to send a list of values to the control to be used for specific validation. -## See the Last Name Only example - it is list restricted to Smith/Jones/Williams. -## 6. Date type fields now use wxDateTime's parser to validate the date and time. -## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing -## me toward this solution! -## 7. Date fields now automatically expand 2-digit years when it can. For example, -## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year -## date is entered it will be expanded in any case when the user tabs out of the -## field. -## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, -## SetSignedForeColor allow accessto override default class coloring behavior. -## 9. Documentation updated and improved. -## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. -## Two new options (checkboxes) - test highlight empty and disallow empty. -## 11. Home and End now work more intuitively, moving to the first and last user-entry -## value, respectively. -## 12. New class function: SetRequired(bool). Sets the control's entry required flag -## (i.e. disallow empty values if True). -## -## Version 0.0.5 -## 1. get_plainValue method renamed to GetPlainValue following the wxWidgets -## StudlyCaps(tm) standard (thanks Paul Moore). ;) -## 2. New format code 'F' causes the control to auto-fit (auto-size) itself -## based on the length of the mask template. -## 3. Class now supports "autoformat" codes. These can be passed to the class -## on instantiation using the parameter autoformat="code". If the code is in -## the dictionary, it will self set the mask, formatting, and validation string. -## I have included a number of samples, but I am hoping that someone out there -## can help me to define a whole bunch more. -## 4. I have added a second page to the demo (as well as a second demo class, test2) -## to showcase how autoformats work. The way they self-format and self-size is, -## I must say, pretty cool. -## 5. Comments added and some internal cosmetic revisions re: matching the code -## standards for class submission. -## 6. Regex validation is now done in real time - field turns yellow immediately -## and stays yellow until the entered value is valid -## 7. Cursor now skips over template characters in a more intuitive way (before the -## next keypress). -## 8. Change, Keypress and LostFocus methods added for convenience of subclasses. -## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, -## and EVT_KILL_FOCUS, respectively. -## 9. Decimal and numeric handlers have been rewritten and now work more intuitively. -## -## Version 0.0.4 -## 1. New .IsEmpty() method returns True if the control's value is equal to the -## blank template string -## 2. Control now supports a new init parameter: revalid. Pass a regular expression -## that the value will have to match when the control loses focus. If invalid, -## the control's BackgroundColor will turn yellow, and an internal flag is set (see next). -## 3. Demo now shows revalid functionality. Try entering a partial value, such as a -## partial social security number. -## 4. New .IsValid() value returns True if the control is empty, or if the value matches -## the revalid expression. If not, .IsValid() returns False. -## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never -## presses the decimal point. -## 6. Cursor now goes to the beginning of the field if the user clicks in an -## "empty" field intead of leaving the insertion point in the middle of the -## field. -## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. -## 8. New formatcodes init parameter replaces other init params and adds functions. -## String passed to control on init controls: -## _ Allow spaces -## ! Force upper -## ^ Force lower -## R Show negative #s in red -## , Group digits -## - Signed numerals -## 0 Numeric fields get leading zeros -## 9. Ctrl-X in any field clears the current value. -## 10. Code refactored and made more modular (esp in OnChar method). Should be more -## easy to read and understand. -## 11. Demo enhanced. -## 12. Now has _doc_. -## -## Version 0.0.3 -## 1. GetPlainValue() now returns the value without the template characters; -## so, for example, a social security number (123-33-1212) would return as -## 123331212; also removes white spaces from numeric/decimal values, so -## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value. -## 2. Press '.' in an integer style masked control and truncate any trailing digits. -## 3. Code moderately refactored. Internal names improved for clarity. Additional -## internal documentation. -## 4. Home and End keys now supported to move cursor to beginning or end of field. -## 5. Un-signed integers and decimals now supported. -## 6. Cosmetic improvements to the demo. -## 7. Class renamed to MaskedTextCtrl. -## 8. Can now specify include characters that will override the basic -## controls: for example, includeChars = "@." for email addresses -## 9. Added mask character 'C' -> allow any upper or lowercase character -## 10. .SetSignColor(str:color) sets the foreground color for negative values -## in signed controls (defaults to red) -## 11. Overview documentation written. -## -## Version 0.0.2 -## 1. Tab now works properly when pressed in last position -## 2. Decimal types now work (e.g. #####.##) -## 3. Signed decimal or numeric values supported (i.e. negative numbers) -## 4. Negative decimal or numeric values now can show in red. -## 5. Can now specify an "exclude list" with the excludeChars parameter. -## See date/time formatted example - you can only enter A or P in the -## character mask space (i.e. AM/PM). -## 6. Backspace now works properly, including clearing data from a selected -## region but leaving template characters intact. Also delete key. -## 7. Left/right arrows now work properly. -## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3 -## diff --git a/wxPython/wx/lib/maskednumctrl.py b/wxPython/wx/lib/maskednumctrl.py deleted file mode 100644 index 9a3d234793..0000000000 --- a/wxPython/wx/lib/maskednumctrl.py +++ /dev/null @@ -1,1613 +0,0 @@ -#---------------------------------------------------------------------------- -# Name: wxPython.lib.maskednumctrl.py -# Author: Will Sadkin -# Created: 09/06/2003 -# Copyright: (c) 2003 by Will Sadkin -# RCS-ID: $Id$ -# License: wxWidgets license -#---------------------------------------------------------------------------- -# NOTE: -# This was written to provide a numeric edit control for wxPython that -# does things like right-insert (like a calculator), and does grouping, etc. -# (ie. the features of MaskedTextCtrl), but allows Get/Set of numeric -# values, rather than text. -# -# MaskedNumCtrl permits integer, and floating point values to be set -# retrieved or set via .GetValue() and .SetValue() (type chosen based on -# fraction width, and provides an EVT_MASKEDNUM() event function for trapping -# changes to the control. -# -# It supports negative numbers as well as the naturals, and has the option -# of not permitting leading zeros or an empty control; if an empty value is -# not allowed, attempting to delete the contents of the control will result -# in a (selected) value of zero, thus preserving a legitimate numeric value. -# Similarly, replacing the contents of the control with '-' will result in -# a selected (absolute) value of -1. -# -# MaskedNumCtrl also supports range limits, with the option of either -# enforcing them or simply coloring the text of the control if the limits -# are exceeded. -# -# MaskedNumCtrl is intended to support fixed-point numeric entry, and -# is derived from BaseMaskedTextCtrl. As such, it supports a limited range -# of values to comply with a fixed-width entry mask. -#---------------------------------------------------------------------------- -# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Updated for wx namespace -# -# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o wxMaskedEditMixin -> MaskedEditMixin -# o wxMaskedTextCtrl -> MaskedTextCtrl -# o wxMaskedNumNumberUpdatedEvent -> MaskedNumNumberUpdatedEvent -# o wxMaskedNumCtrl -> MaskedNumCtrl -# - -""" -

-MaskedNumCtrl: -

    -
  • allows you to get and set integer or floating point numbers as value,
  • -
  • provides bounds support and optional value limiting,
  • -
  • has the right-insert input style that MaskedTextCtrl supports,
  • -
  • provides optional automatic grouping, sign control and format, grouping and decimal -character selection, etc. etc.
  • -
-

-Being derived from MaskedTextCtrl, the control only allows -fixed-point notation. That is, it has a fixed (though reconfigurable) -maximum width for the integer portion and optional fixed width -fractional portion. -

-Here's the API: -

-    MaskedNumCtrl(
-         parent, id = -1,
-         value = 0,
-         pos = wx.DefaultPosition,
-         size = wx.DefaultSize,
-         style = 0,
-         validator = wx.DefaultValidator,
-         name = "maskednumber",
-         integerWidth = 10,
-         fractionWidth = 0,
-         allowNone = False,
-         allowNegative = True,
-         useParensForNegatives = False,
-         groupDigits = False,
-         groupChar = ',',
-         decimalChar = '.',
-         min = None,
-         max = None,
-         limited = False,
-         selectOnEntry = True,
-         foregroundColour = "Black",
-         signedForegroundColour = "Red",
-         emptyBackgroundColour = "White",
-         validBackgroundColour = "White",
-         invalidBackgroundColour = "Yellow",
-         autoSize = True         
-         )
-
-
    -
    value -
    If no initial value is set, the default will be zero, or - the minimum value, if specified. If an illegal string is specified, - a ValueError will result. (You can always later set the initial - value with SetValue() after instantiation of the control.) -
    -
    integerWidth -
    Indicates how many places to the right of any decimal point - should be allowed in the control. This will, perforce, limit - the size of the values that can be entered. This number need - not include space for grouping characters or the sign, if either - of these options are enabled, as the resulting underlying - mask is automatically by the control. The default of 10 - will allow any 32 bit integer value. The minimum value - for integerWidth is 1. -
    -
    fractionWidth -
    Indicates how many decimal places to show for numeric value. - If default (0), then the control will display and return only - integer or long values. -
    -
    allowNone -
    Boolean indicating whether or not the control is allowed to be - empty, representing a value of None for the control. -
    -
    allowNegative -
    Boolean indicating whether or not control is allowed to hold - negative numbers. -
    -
    useParensForNegatives -
    If true, this will cause negative numbers to be displayed with ()s - rather than -, (although '-' will still trigger a negative number.) -
    -
    groupDigits -
    Indicates whether or not grouping characters should be allowed and/or - inserted when leaving the control or the decimal character is entered. -
    -
    groupChar -
    What grouping character will be used if allowed. (By default ',') -
    -
    decimalChar -
    If fractionWidth is > 0, what character will be used to represent - the decimal point. (By default '.') -
    -
    min -
    The minimum value that the control should allow. This can be also be - adjusted with SetMin(). If the control is not limited, any value - below this bound will result in a background colored with the current - invalidBackgroundColour. If the min specified will not fit into the - control, the min setting will be ignored. -
    -
    max -
    The maximum value that the control should allow. This can be - adjusted with SetMax(). If the control is not limited, any value - above this bound will result in a background colored with the current - invalidBackgroundColour. If the max specified will not fit into the - control, the max setting will be ignored. -
    -
    limited -
    Boolean indicating whether the control prevents values from - exceeding the currently set minimum and maximum values (bounds). - If False and bounds are set, out-of-bounds values will - result in a background colored with the current invalidBackgroundColour. -
    -
    selectOnEntry -
    Boolean indicating whether or not the value in each field of the - control should be automatically selected (for replacement) when - that field is entered, either by cursor movement or tabbing. - This can be desirable when using these controls for rapid data entry. -
    -
    foregroundColour -
    Color value used for positive values of the control. -
    -
    signedForegroundColour -
    Color value used for negative values of the control. -
    -
    emptyBackgroundColour -
    What background color to use when the control is considered - "empty." (allow_none must be set to trigger this behavior.) -
    -
    validBackgroundColour -
    What background color to use when the control value is - considered valid. -
    -
    invalidBackgroundColour -
    Color value used for illegal values or values out-of-bounds of the - control when the bounds are set but the control is not limited. -
    -
    autoSize -
    Boolean indicating whether or not the control should set its own - width based on the integer and fraction widths. True by default. - Note: Setting this to False will produce seemingly odd - behavior unless the control is large enough to hold the maximum - specified value given the widths and the sign positions; if not, - the control will appear to "jump around" as the contents scroll. - (ie. autoSize is highly recommended.) -
-
-
-
EVT_MASKEDNUM(win, id, func) -
Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when -the value changes. Notice that this event will always be sent when the -control's contents changes - whether this is due to user input or -comes from the program itself (for example, if SetValue() is called.) -
-
-
SetValue(int|long|float|string) -
Sets the value of the control to the value specified, if -possible. The resulting actual value of the control may be -altered to conform to the format of the control, changed -to conform with the bounds set on the control if limited, -or colored if not limited but the value is out-of-bounds. -A ValueError exception will be raised if an invalid value -is specified. -
-
GetValue() -
Retrieves the numeric value from the control. The value -retrieved will be either be returned as a long if the -fractionWidth is 0, or a float otherwise. -
-
-
SetParameters(**kwargs) -
Allows simultaneous setting of various attributes -of the control after construction. Keyword arguments -allowed are the same parameters as supported in the constructor. -
-
-
SetIntegerWidth(value) -
Resets the width of the integer portion of the control. The -value must be >= 1, or an AttributeError exception will result. -This value should account for any grouping characters that might -be inserted (if grouping is enabled), but does not need to account -for the sign, as that is handled separately by the control. -
GetIntegerWidth() -
Returns the current width of the integer portion of the control, -not including any reserved sign position. -
-
-
SetFractionWidth(value) -
Resets the width of the fractional portion of the control. The -value must be >= 0, or an AttributeError exception will result. If -0, the current value of the control will be truncated to an integer -value. -
GetFractionWidth() -
Returns the current width of the fractional portion of the control. -
-
-
SetMin(min=None) -
Resets the minimum value of the control. If a value of None -is provided, then the control will have no explicit minimum value. -If the value specified is greater than the current maximum value, -then the function returns False and the minimum will not change from -its current setting. On success, the function returns True. -
-If successful and the current value is lower than the new lower -bound, if the control is limited, the value will be automatically -adjusted to the new minimum value; if not limited, the value in the -control will be colored as invalid. -
-If min > the max value allowed by the width of the control, -the function will return False, and the min will not be set. -
-
GetMin() -
Gets the current lower bound value for the control. -It will return None if no lower bound is currently specified. -
-
-
SetMax(max=None) -
Resets the maximum value of the control. If a value of None -is provided, then the control will have no explicit maximum value. -If the value specified is less than the current minimum value, then -the function returns False and the maximum will not change from its -current setting. On success, the function returns True. -
-If successful and the current value is greater than the new upper -bound, if the control is limited the value will be automatically -adjusted to this maximum value; if not limited, the value in the -control will be colored as invalid. -
-If max > the max value allowed by the width of the control, -the function will return False, and the max will not be set. -
-
GetMax() -
Gets the current upper bound value for the control. -It will return None if no upper bound is currently specified. -
-
-
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. -
GetBounds() -
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. -
-
-
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. -
-
-
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. -If the control's value currently exceeds the bounds, it will then -be limited accordingly. -If called with a value of False, this function will disable value -limiting, but coloring of out-of-bounds values will still take -place if bounds have been set for the control. -
GetLimited() -
IsLimited() -
Returns True if the control is currently limiting the -value to fall within the current bounds. -
-
-
SetAllowNone(bool) -
If called with a value of True, this function will cause the control -to allow the value to be empty, representing a value of None. -If called with a value of False, this function will prevent the value -from being None. If the value of the control is currently None, -ie. the control is empty, then the value will be changed to that -of the lower bound of the control, or 0 if no lower bound is set. -
GetAllowNone() -
IsNoneAllowed() -
Returns True if the control currently allows its -value to be None. -
-
-
SetAllowNegative(bool) -
If called with a value of True, this function will cause the -control to allow the value to be negative (and reserve space for -displaying the sign. If called with a value of False, and the -value of the control is currently negative, the value of the -control will be converted to the absolute value, and then -limited appropriately based on the existing bounds of the control -(if any). -
GetAllowNegative() -
IsNegativeAllowed() -
Returns True if the control currently permits values -to be negative. -
-
-
SetGroupDigits(bool) -
If called with a value of True, this will make the control -automatically add and manage grouping characters to the presented -value in integer portion of the control. -
GetGroupDigits() -
IsGroupingAllowed() -
Returns True if the control is currently set to group digits. -
-
-
SetGroupChar() -
Sets the grouping character for the integer portion of the -control. (The default grouping character this is ','. -
GetGroupChar() -
Returns the current grouping character for the control. -
-
-
SetSelectOnEntry() -
If called with a value of True, this will make the control -automatically select the contents of each field as it is entered -within the control. (The default is True.) -
GetSelectOnEntry() -
Returns True if the control currently auto selects -the field values on entry. -
-
-
SetAutoSize(bool) -
Resets the autoSize attribute of the control. -
GetAutoSize() -
Returns the current state of the autoSize attribute for the control. -
-
-
- -""" - -import copy -import string -import types - -import wx - -from sys import maxint -MAXINT = maxint # (constants should be in upper case) -MININT = -maxint-1 - -from wx.tools.dbg import Logger -from wx.lib.maskededit import MaskedEditMixin, BaseMaskedTextCtrl, Field -dbg = Logger() -dbg(enable=0) - -#---------------------------------------------------------------------------- - -wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wx.NewEventType() -EVT_MASKEDNUM = wx.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED, 1) - -#---------------------------------------------------------------------------- - -class MaskedNumNumberUpdatedEvent(wx.PyCommandEvent): - def __init__(self, id, value = 0, object=None): - wx.PyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id) - - self.__value = value - self.SetEventObject(object) - - def GetValue(self): - """Retrieve the value of the control at the time - this event was generated.""" - return self.__value - - -#---------------------------------------------------------------------------- -class MaskedNumCtrlAccessorsMixin: - # Define wxMaskedNumCtrl's list of attributes having their own - # Get/Set functions, ignoring those that make no sense for - # an numeric control. - exposed_basectrl_params = ( - 'decimalChar', - 'shiftDecimalChar', - 'groupChar', - 'useParensForNegatives', - 'defaultValue', - 'description', - - 'useFixedWidthFont', - 'autoSize', - 'signedForegroundColour', - 'emptyBackgroundColour', - 'validBackgroundColour', - 'invalidBackgroundColour', - - 'emptyInvalid', - 'validFunc', - 'validRequired', - ) - for param in exposed_basectrl_params: - propname = param[0].upper() + param[1:] - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - if param.find('Colour') != -1: - # add non-british spellings, for backward-compatibility - propname.replace('Colour', 'Color') - - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - - -#---------------------------------------------------------------------------- - -class MaskedNumCtrl(BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin): - - - valid_ctrl_params = { - 'integerWidth': 10, # by default allow all 32-bit integers - 'fractionWidth': 0, # by default, use integers - 'decimalChar': '.', # by default, use '.' for decimal point - 'allowNegative': True, # by default, allow negative numbers - 'useParensForNegatives': False, # by default, use '-' to indicate negatives - 'groupDigits': True, # by default, don't insert grouping - 'groupChar': ',', # by default, use ',' for grouping - 'min': None, # by default, no bounds set - 'max': None, - 'limited': False, # by default, no limiting even if bounds set - 'allowNone': False, # by default, don't allow empty value - 'selectOnEntry': True, # by default, select the value of each field on entry - 'foregroundColour': "Black", - 'signedForegroundColour': "Red", - 'emptyBackgroundColour': "White", - 'validBackgroundColour': "White", - 'invalidBackgroundColour': "Yellow", - 'useFixedWidthFont': True, # by default, use a fixed-width font - 'autoSize': True, # by default, set the width of the control based on the mask - } - - - def __init__ ( - self, parent, id=-1, value = 0, - pos = wx.DefaultPosition, size = wx.DefaultSize, - style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator, - name = "maskednum", - **kwargs ): - - dbg('MaskedNumCtrl::__init__', indent=1) - - # Set defaults for control: - dbg('setting defaults:') - for key, param_value in MaskedNumCtrl.valid_ctrl_params.items(): - # This is done this way to make setattr behave consistently with - # "private attribute" name mangling - setattr(self, '_' + key, copy.copy(param_value)) - - # Assign defaults for all attributes: - init_args = copy.deepcopy(MaskedNumCtrl.valid_ctrl_params) - dbg('kwargs:', kwargs) - for key, param_value in kwargs.items(): - key = key.replace('Color', 'Colour') - if key not in MaskedNumCtrl.valid_ctrl_params.keys(): - raise AttributeError('invalid keyword argument "%s"' % key) - else: - init_args[key] = param_value - dbg('init_args:', indent=1) - for key, param_value in init_args.items(): - dbg('%s:' % key, param_value) - dbg(indent=0) - - # Process initial fields for the control, as part of construction: - if type(init_args['integerWidth']) != types.IntType: - raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth'])) - elif init_args['integerWidth'] < 1: - raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth'])) - - fields = {} - - if init_args.has_key('fractionWidth'): - if type(init_args['fractionWidth']) != types.IntType: - raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth)) - elif init_args['fractionWidth'] < 0: - raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth'])) - self._fractionWidth = init_args['fractionWidth'] - - if self._fractionWidth: - fracmask = '.' + '#{%d}' % self._fractionWidth - dbg('fracmask:', fracmask) - fields[1] = Field(defaultValue='0'*self._fractionWidth) - else: - fracmask = '' - - self._integerWidth = init_args['integerWidth'] - if init_args['groupDigits']: - self._groupSpace = (self._integerWidth - 1) / 3 - else: - self._groupSpace = 0 - intmask = '#{%d}' % (self._integerWidth + self._groupSpace) - if self._fractionWidth: - emptyInvalid = False - else: - emptyInvalid = True - fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) - dbg('intmask:', intmask) - - # don't bother to reprocess these arguments: - del init_args['integerWidth'] - del init_args['fractionWidth'] - - self._autoSize = init_args['autoSize'] - if self._autoSize: - formatcodes = 'FR<' - else: - formatcodes = 'R<' - - - mask = intmask+fracmask - - # initial value of state vars - self._oldvalue = 0 - self._integerEnd = 0 - self._typedSign = False - - # Construct the base control: - BaseMaskedTextCtrl.__init__( - self, parent, id, '', - pos, size, style, validator, name, - mask = mask, - formatcodes = formatcodes, - fields = fields, - validFunc=self.IsInBounds, - setupEventHandling = False) - - self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection - self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator - self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick - self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. - self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress - self.Bind(wx.EVT_TEXT, self.OnTextChange ) ## color control appropriately & keep - ## track of previous value for undo - - # Establish any additional parameters, with appropriate error checking - self.SetParameters(**init_args) - - # Set the value requested (if possible) -## wxCallAfter(self.SetValue, value) - self.SetValue(value) - - # Ensure proper coloring: - self.Refresh() - dbg('finished MaskedNumCtrl::__init__', indent=0) - - - def SetParameters(self, **kwargs): - """ - This routine is used to initialize and reconfigure the control: - """ - dbg('MaskedNumCtrl::SetParameters', indent=1) - maskededit_kwargs = {} - reset_fraction_width = False - - - if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth) - or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth) - or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits) - or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ): - - fields = {} - - if kwargs.has_key('fractionWidth'): - if type(kwargs['fractionWidth']) != types.IntType: - raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth'])) - elif kwargs['fractionWidth'] < 0: - raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth'])) - else: - if self._fractionWidth != kwargs['fractionWidth']: - self._fractionWidth = kwargs['fractionWidth'] - - if self._fractionWidth: - fracmask = '.' + '#{%d}' % self._fractionWidth - fields[1] = Field(defaultValue='0'*self._fractionWidth) - emptyInvalid = False - else: - emptyInvalid = True - fracmask = '' - dbg('fracmask:', fracmask) - - if kwargs.has_key('integerWidth'): - if type(kwargs['integerWidth']) != types.IntType: - dbg(indent=0) - raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth'])) - elif kwargs['integerWidth'] < 0: - dbg(indent=0) - raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth'])) - else: - self._integerWidth = kwargs['integerWidth'] - - if kwargs.has_key('groupDigits'): - self._groupDigits = kwargs['groupDigits'] - - if self._groupDigits: - self._groupSpace = (self._integerWidth - 1) / 3 - else: - self._groupSpace = 0 - - intmask = '#{%d}' % (self._integerWidth + self._groupSpace) - dbg('intmask:', intmask) - fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) - maskededit_kwargs['fields'] = fields - - # don't bother to reprocess these arguments: - if kwargs.has_key('integerWidth'): - del kwargs['integerWidth'] - if kwargs.has_key('fractionWidth'): - del kwargs['fractionWidth'] - - maskededit_kwargs['mask'] = intmask+fracmask - - if kwargs.has_key('groupChar'): - old_groupchar = self._groupChar # save so we can reformat properly - dbg("old_groupchar: '%s'" % old_groupchar) - maskededit_kwargs['groupChar'] = kwargs['groupChar'] - if kwargs.has_key('decimalChar'): - old_decimalchar = self._decimalChar - dbg("old_decimalchar: '%s'" % old_decimalchar) - maskededit_kwargs['decimalChar'] = kwargs['decimalChar'] - - # for all other parameters, assign keyword args as appropriate: - for key, param_value in kwargs.items(): - key = key.replace('Color', 'Colour') - if key not in MaskedNumCtrl.valid_ctrl_params.keys(): - raise AttributeError('invalid keyword argument "%s"' % key) - elif key not in MaskedEditMixin.valid_ctrl_params.keys(): - setattr(self, '_' + key, param_value) - elif key in ('mask', 'autoformat'): # disallow explicit setting of mask - raise AttributeError('invalid keyword argument "%s"' % key) - else: - maskededit_kwargs[key] = param_value - dbg('kwargs:', kwargs) - - # reprocess existing format codes to ensure proper resulting format: - formatcodes = self.GetCtrlParameter('formatcodes') - if kwargs.has_key('allowNegative'): - if kwargs['allowNegative'] and '-' not in formatcodes: - formatcodes += '-' - maskededit_kwargs['formatcodes'] = formatcodes - elif not kwargs['allowNegative'] and '-' in formatcodes: - formatcodes = formatcodes.replace('-','') - maskededit_kwargs['formatcodes'] = formatcodes - - if kwargs.has_key('groupDigits'): - if kwargs['groupDigits'] and ',' not in formatcodes: - formatcodes += ',' - maskededit_kwargs['formatcodes'] = formatcodes - elif not kwargs['groupDigits'] and ',' in formatcodes: - formatcodes = formatcodes.replace(',','') - maskededit_kwargs['formatcodes'] = formatcodes - - if kwargs.has_key('selectOnEntry'): - self._selectOnEntry = kwargs['selectOnEntry'] - dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes) - if kwargs['selectOnEntry'] and 'S' not in formatcodes: - formatcodes += 'S' - maskededit_kwargs['formatcodes'] = formatcodes - elif not kwargs['selectOnEntry'] and 'S' in formatcodes: - formatcodes = formatcodes.replace('S','') - maskededit_kwargs['formatcodes'] = formatcodes - - if kwargs.has_key('autoSize'): - self._autoSize = kwargs['autoSize'] - if kwargs['autoSize'] and 'F' not in formatcodes: - formatcodes += 'F' - maskededit_kwargs['formatcodes'] = formatcodes - elif not kwargs['autoSize'] and 'F' in formatcodes: - formatcodes = formatcodes.replace('F', '') - maskededit_kwargs['formatcodes'] = formatcodes - - - if 'r' in formatcodes and self._fractionWidth: - # top-level mask should only be right insert if no fractional - # part will be shown; ie. if reconfiguring control, remove - # previous "global" setting. - formatcodes = formatcodes.replace('r', '') - maskededit_kwargs['formatcodes'] = formatcodes - - - if kwargs.has_key('limited'): - if kwargs['limited'] and not self._limited: - maskededit_kwargs['validRequired'] = True - elif not kwargs['limited'] and self._limited: - maskededit_kwargs['validRequired'] = False - self._limited = kwargs['limited'] - - dbg('maskededit_kwargs:', maskededit_kwargs) - if maskededit_kwargs.keys(): - self.SetCtrlParameters(**maskededit_kwargs) - - # Record end of integer and place cursor there: - integerEnd = self._fields[0]._extent[1] - self.SetInsertionPoint(0) - self.SetInsertionPoint(integerEnd) - self.SetSelection(integerEnd, integerEnd) - - # Go ensure all the format codes necessary are present: - orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes') - if 'r' not in intformat: - intformat += 'r' - if '>' not in intformat: - intformat += '>' - if intformat != orig_intformat: - if self._fractionWidth: - self.SetFieldParameters(0, formatcodes=intformat) - else: - self.SetCtrlParameters(formatcodes=intformat) - - # Set min and max as appropriate: - if kwargs.has_key('min'): - min = kwargs['min'] - if( self._max is None - or min is None - or (self._max is not None and self._max >= min) ): - dbg('examining min') - if min is not None: - try: - textmin = self._toGUI(min, apply_limits = False) - except ValueError: - dbg('min will not fit into control; ignoring', indent=0) - raise - dbg('accepted min') - self._min = min - else: - dbg('ignoring min') - - - if kwargs.has_key('max'): - max = kwargs['max'] - if( self._min is None - or max is None - or (self._min is not None and self._min <= max) ): - dbg('examining max') - if max is not None: - try: - textmax = self._toGUI(max, apply_limits = False) - except ValueError: - dbg('max will not fit into control; ignoring', indent=0) - raise - dbg('accepted max') - self._max = max - else: - dbg('ignoring max') - - if kwargs.has_key('allowNegative'): - self._allowNegative = kwargs['allowNegative'] - - # Ensure current value of control obeys any new restrictions imposed: - text = self._GetValue() - dbg('text value: "%s"' % text) - if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1: - text = text.replace(old_groupchar, self._groupChar) - if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1: - text = text.replace(old_decimalchar, self._decimalChar) - if text != self._GetValue(): - wx.TextCtrl.SetValue(self, text) - - value = self.GetValue() - - dbg('self._allowNegative?', self._allowNegative) - if not self._allowNegative and self._isNeg: - value = abs(value) - dbg('abs(value):', value) - self._isNeg = False - - elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '': - if self._min > 0: - value = self._min - else: - value = 0 - - sel_start, sel_to = self.GetSelection() - if self.IsLimited() and self._min is not None and value < self._min: - dbg('Set to min value:', self._min) - self._SetValue(self._toGUI(self._min)) - - elif self.IsLimited() and self._max is not None and value > self._max: - dbg('Setting to max value:', self._max) - self._SetValue(self._toGUI(self._max)) - else: - # reformat current value as appropriate to possibly new conditions - dbg('Reformatting value:', value) - sel_start, sel_to = self.GetSelection() - self._SetValue(self._toGUI(value)) - self.Refresh() # recolor as appropriate - dbg('finished MaskedNumCtrl::SetParameters', indent=0) - - - - def _GetNumValue(self, value): - """ - This function attempts to "clean up" a text value, providing a regularized - convertable string, via atol() or atof(), for any well-formed numeric text value. - """ - return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip() - - - def GetFraction(self, candidate=None): - """ - Returns the fractional portion of the value as a float. If there is no - fractional portion, the value returned will be 0.0. - """ - if not self._fractionWidth: - return 0.0 - else: - fracstart, fracend = self._fields[1]._extent - if candidate is None: - value = self._toGUI(BaseMaskedTextCtrl.GetValue(self)) - else: - value = self._toGUI(candidate) - fracstring = value[fracstart:fracend].strip() - if not value: - return 0.0 - else: - return string.atof(fracstring) - - def _OnChangeSign(self, event): - dbg('MaskedNumCtrl::_OnChangeSign', indent=1) - self._typedSign = True - MaskedEditMixin._OnChangeSign(self, event) - dbg(indent=0) - - - def _disallowValue(self): - dbg('MaskedNumCtrl::_disallowValue') - # limited and -1 is out of bounds - if self._typedSign: - self._isNeg = False - if not wx.Validator_IsSilent(): - wx.Bell() - sel_start, sel_to = self._GetSelection() - dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) - wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position - wx.CallAfter(self.SetSelection, sel_start, sel_to) - - def _SetValue(self, value): - """ - This routine supersedes the base masked control _SetValue(). It is - needed to ensure that the value of the control is always representable/convertable - to a numeric return value (via GetValue().) This routine also handles - automatic adjustment and grouping of the value without explicit intervention - by the user. - """ - - dbg('MaskedNumCtrl::_SetValue("%s")' % value, indent=1) - - if( (self._fractionWidth and value.find(self._decimalChar) == -1) or - (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) : - value = self._toGUI(value) - - numvalue = self._GetNumValue(value) - dbg('cleansed value: "%s"' % numvalue) - replacement = None - - if numvalue == "": - if self._allowNone: - dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value) - BaseMaskedTextCtrl._SetValue(self, value) - self.Refresh() - return - elif self._min > 0 and self.IsLimited(): - replacement = self._min - else: - replacement = 0 - dbg('empty value; setting replacement:', replacement) - - if replacement is None: - # Go get the integer portion about to be set and verify its validity - intstart, intend = self._fields[0]._extent - dbg('intstart, intend:', intstart, intend) - dbg('raw integer:"%s"' % value[intstart:intend]) - int = self._GetNumValue(value[intstart:intend]) - numval = self._fromGUI(value) - - dbg('integer: "%s"' % int) - try: - fracval = self.GetFraction(value) - except ValueError, e: - dbg('Exception:', e, 'must be out of bounds; disallow value') - self._disallowValue() - dbg(indent=0) - return - - if fracval == 0.0: - dbg('self._isNeg?', self._isNeg) - if int == '-' and self._oldvalue < 0 and not self._typedSign: - dbg('just a negative sign; old value < 0; setting replacement of 0') - replacement = 0 - self._isNeg = False - elif int[:2] == '-0' and self._fractionWidth == 0: - if self._oldvalue < 0: - dbg('-0; setting replacement of 0') - replacement = 0 - self._isNeg = False - elif not self._limited or (self._min < -1 and self._max >= -1): - dbg('-0; setting replacement of -1') - replacement = -1 - self._isNeg = True - else: - # limited and -1 is out of bounds - self._disallowValue() - dbg(indent=0) - return - - elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0: - if not self._limited or (self._min < -1 and self._max >= -1): - dbg('just a negative sign; setting replacement of -1') - replacement = -1 - else: - # limited and -1 is out of bounds - self._disallowValue() - dbg(indent=0) - return - - elif( self._typedSign - and int.find('-') != -1 - and self._limited - and not self._min <= numval <= self._max): - # changed sign resulting in value that's now out-of-bounds; - # disallow - self._disallowValue() - dbg(indent=0) - return - - if replacement is None: - if int and int != '-': - try: - string.atol(int) - except ValueError: - # integer requested is not legal. This can happen if the user - # is attempting to insert a digit in the middle of the control - # resulting in something like " 3 45". Disallow such actions: - dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) - if not wx.Validator_IsSilent(): - wx.Bell() - sel_start, sel_to = self._GetSelection() - dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) - wx.CallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position - wx.CallAfter(self.SetSelection, sel_start, sel_to) - dbg(indent=0) - return - - if int[0] == '0' and len(int) > 1: - dbg('numvalue: "%s"' % numvalue.replace(' ', '')) - if self._fractionWidth: - value = self._toGUI(string.atof(numvalue)) - else: - value = self._toGUI(string.atol(numvalue)) - dbg('modified value: "%s"' % value) - - self._typedSign = False # reset state var - - if replacement is not None: - # Value presented wasn't a legal number, but control should do something - # reasonable instead: - dbg('setting replacement value:', replacement) - self._SetValue(self._toGUI(replacement)) - sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement))) # find where it put the 1, so we can select it - sel_to = sel_start + len(str(abs(replacement))) - dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) - wx.CallAfter(self.SetInsertionPoint, sel_start) - wx.CallAfter(self.SetSelection, sel_start, sel_to) - dbg(indent=0) - return - - # Otherwise, apply appropriate formatting to value: - - # Because we're intercepting the value and adjusting it - # before a sign change is detected, we need to do this here: - if '-' in value or '(' in value: - self._isNeg = True - else: - self._isNeg = False - - dbg('value:"%s"' % value, 'self._useParens:', self._useParens) - if self._fractionWidth: - adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) - else: - adjvalue = self._adjustInt(self._GetNumValue(value)) - dbg('adjusted value: "%s"' % adjvalue) - - - sel_start, sel_to = self._GetSelection() # record current insertion point - dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) - BaseMaskedTextCtrl._SetValue(self, adjvalue) - # After all actions so far scheduled, check that resulting cursor - # position is appropriate, and move if not: - wx.CallAfter(self._CheckInsertionPoint) - - dbg('finished MaskedNumCtrl::_SetValue', indent=0) - - def _CheckInsertionPoint(self): - # If current insertion point is before the end of the integer and - # its before the 1st digit, place it just after the sign position: - dbg('MaskedNumCtrl::CheckInsertionPoint', indent=1) - sel_start, sel_to = self._GetSelection() - text = self._GetValue() - if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): - text, signpos, right_signpos = self._getSignedValue() - dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) - self.SetInsertionPoint(signpos+1) - self.SetSelection(signpos+1, signpos+1) - dbg(indent=0) - - - def _OnErase( self, event ): - """ - This overrides the base control _OnErase, so that erasing around - grouping characters auto selects the digit before or after the - grouping character, so that the erasure does the right thing. - """ - dbg('MaskedNumCtrl::_OnErase', indent=1) - - #if grouping digits, make sure deletes next to group char always - # delete next digit to appropriate side: - if self._groupDigits: - key = event.GetKeyCode() - value = BaseMaskedTextCtrl.GetValue(self) - sel_start, sel_to = self._GetSelection() - - if key == wx.WXK_BACK: - # if 1st selected char is group char, select to previous digit - if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: - self.SetInsertionPoint(sel_start-1) - self.SetSelection(sel_start-1, sel_to) - - # elif previous char is group char, select to previous digit - elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: - self.SetInsertionPoint(sel_start-2) - self.SetSelection(sel_start-2, sel_to) - - elif key == wx.WXK_DELETE: - if( sel_to < len(self._mask) - 2 + (1 *self._useParens) - and sel_start == sel_to - and value[sel_to] == self._groupChar ): - self.SetInsertionPoint(sel_start) - self.SetSelection(sel_start, sel_to+2) - - elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) - and value[sel_start:sel_to] == self._groupChar ): - self.SetInsertionPoint(sel_start) - self.SetSelection(sel_start, sel_to+1) - - BaseMaskedTextCtrl._OnErase(self, event) - dbg(indent=0) - - - def OnTextChange( self, event ): - """ - Handles an event indicating that the text control's value - has changed, and issue EVT_MaskedNum event. - NOTE: using wxTextCtrl.SetValue() to change the control's - contents from within a EVT_CHAR handler can cause double - text events. So we check for actual changes to the text - before passing the events on. - """ - dbg('MaskedNumCtrl::OnTextChange', indent=1) - if not BaseMaskedTextCtrl._OnTextChange(self, event): - dbg(indent=0) - return - - # else... legal value - - value = self.GetValue() - if value != self._oldvalue: - try: - self.GetEventHandler().ProcessEvent( - MaskedNumNumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) - except ValueError: - dbg(indent=0) - return - # let normal processing of the text continue - event.Skip() - self._oldvalue = value # record for next event - dbg(indent=0) - - def _GetValue(self): - """ - Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the - control with this function. - """ - return wx.TextCtrl.GetValue(self) - - - def GetValue(self): - """ - Returns the current numeric value of the control. - """ - return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) - - def SetValue(self, value): - """ - Sets the value of the control to the value specified. - The resulting actual value of the control may be altered to - conform with the bounds set on the control if limited, - or colored if not limited but the value is out-of-bounds. - A ValueError exception will be raised if an invalid value - is specified. - """ - BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) - - - def SetIntegerWidth(self, value): - self.SetParameters(integerWidth=value) - def GetIntegerWidth(self): - return self._integerWidth - - def SetFractionWidth(self, value): - self.SetParameters(fractionWidth=value) - def GetFractionWidth(self): - return self._fractionWidth - - - - def SetMin(self, min=None): - """ - Sets the minimum value of the control. If a value of None - is provided, then the control will have no explicit minimum value. - If the value specified is greater than the current maximum value, - then the function returns False and the minimum will not change from - its current setting. On success, the function returns True. - - If successful and the current value is lower than the new lower - bound, if the control is limited, the value will be automatically - adjusted to the new minimum value; if not limited, the value in the - control will be colored as invalid. - - If min > the max value allowed by the width of the control, - the function will return False, and the min will not be set. - """ - dbg('MaskedNumCtrl::SetMin(%s)' % repr(min), indent=1) - if( self._max is None - or min is None - or (self._max is not None and self._max >= min) ): - try: - self.SetParameters(min=min) - bRet = True - except ValueError: - bRet = False - else: - bRet = False - dbg(indent=0) - return bRet - - def GetMin(self): - """ - Gets the lower bound value of the control. It will return - None if not specified. - """ - return self._min - - - def SetMax(self, max=None): - """ - Sets the maximum value of the control. If a value of None - is provided, then the control will have no explicit maximum value. - If the value specified is less than the current minimum value, then - the function returns False and the maximum will not change from its - current setting. On success, the function returns True. - - If successful and the current value is greater than the new upper - bound, if the control is limited the value will be automatically - adjusted to this maximum value; if not limited, the value in the - control will be colored as invalid. - - If max > the max value allowed by the width of the control, - the function will return False, and the max will not be set. - """ - if( self._min is None - or max is None - or (self._min is not None and self._min <= max) ): - try: - self.SetParameters(max=max) - bRet = True - except ValueError: - bRet = False - else: - bRet = False - - return bRet - - - def GetMax(self): - """ - Gets the maximum value of the control. It will return the current - maximum integer, or None if not specified. - """ - return self._max - - - def SetBounds(self, min=None, max=None): - """ - This function is a convenience function for setting the min and max - values at the same time. The function only applies the maximum bound - if setting the minimum bound is successful, and returns True - only if both operations succeed. - NOTE: leaving out an argument will remove the corresponding bound. - """ - ret = self.SetMin(min) - return ret and self.SetMax(max) - - - def GetBounds(self): - """ - This function returns a two-tuple (min,max), indicating the - current bounds of the control. Each value can be None if - that bound is not set. - """ - return (self._min, self._max) - - - def SetLimited(self, limited): - """ - If called with a value of True, this function will cause the control - to limit the value to fall within the bounds currently specified. - If the control's value currently exceeds the bounds, it will then - be limited accordingly. - - If called with a value of False, this function will disable value - limiting, but coloring of out-of-bounds values will still take - place if bounds have been set for the control. - """ - self.SetParameters(limited = limited) - - - def IsLimited(self): - """ - Returns True if the control is currently limiting the - value to fall within the current bounds. - """ - return self._limited - - def GetLimited(self): - """ (For regularization of property accessors) """ - return self.IsLimited - - - def IsInBounds(self, value=None): - """ - Returns True if no value is specified and the current value - of the control falls within the current bounds. This function can - also be called with a value to see if that value would fall within - the current bounds of the given control. - """ - dbg('IsInBounds(%s)' % repr(value), indent=1) - if value is None: - value = self.GetValue() - else: - try: - value = self._GetNumValue(self._toGUI(value)) - except ValueError, e: - dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) - return False - if value.strip() == '': - value = None - elif self._fractionWidth: - value = float(value) - else: - value = long(value) - - min = self.GetMin() - max = self.GetMax() - if min is None: min = value - if max is None: max = value - - # if bounds set, and value is None, return False - if value == None and (min is not None or max is not None): - dbg('finished IsInBounds', indent=0) - return 0 - else: - dbg('finished IsInBounds', indent=0) - return min <= value <= max - - - def SetAllowNone(self, allow_none): - """ - Change the behavior of the validation code, allowing control - to have a value of None or not, as appropriate. If the value - of the control is currently None, and allow_none is False, the - value of the control will be set to the minimum value of the - control, or 0 if no lower bound is set. - """ - self._allowNone = allow_none - if not allow_none and self.GetValue() is None: - min = self.GetMin() - if min is not None: self.SetValue(min) - else: self.SetValue(0) - - - def IsNoneAllowed(self): - return self._allowNone - def GetAllowNone(self): - """ (For regularization of property accessors) """ - return self.IsNoneAllowed() - - def SetAllowNegative(self, value): - self.SetParameters(allowNegative=value) - def IsNegativeAllowed(self): - return self._allowNegative - def GetAllowNegative(self): - """ (For regularization of property accessors) """ - return self.IsNegativeAllowed() - - def SetGroupDigits(self, value): - self.SetParameters(groupDigits=value) - def IsGroupingAllowed(self): - return self._groupDigits - def GetGroupDigits(self): - """ (For regularization of property accessors) """ - return self.IsGroupingAllowed() - - def SetGroupChar(self, value): - self.SetParameters(groupChar=value) - def GetGroupChar(self): - return self._groupChar - - def SetDecimalChar(self, value): - self.SetParameters(decimalChar=value) - def GetDecimalChar(self): - return self._decimalChar - - def SetSelectOnEntry(self, value): - self.SetParameters(selectOnEntry=value) - def GetSelectOnEntry(self): - return self._selectOnEntry - - def SetAutoSize(self, value): - self.SetParameters(autoSize=value) - def GetAutoSize(self): - return self._autoSize - - - # (Other parameter accessors are inherited from base class) - - - def _toGUI( self, value, apply_limits = True ): - """ - Conversion function used to set the value of the control; does - type and bounds checking and raises ValueError if argument is - not a valid value. - """ - dbg('MaskedNumCtrl::_toGUI(%s)' % repr(value), indent=1) - if value is None and self.IsNoneAllowed(): - dbg(indent=0) - return self._template - - elif type(value) in (types.StringType, types.UnicodeType): - value = self._GetNumValue(value) - dbg('cleansed num value: "%s"' % value) - if value == "": - if self.IsNoneAllowed(): - dbg(indent=0) - return self._template - else: - dbg('exception raised:', e, indent=0) - raise ValueError ('wxMaskedNumCtrl requires numeric value, passed %s'% repr(value) ) - # else... - try: - if self._fractionWidth or value.find('.') != -1: - value = float(value) - else: - value = long(value) - except Exception, e: - dbg('exception raised:', e, indent=0) - raise ValueError ('MaskedNumCtrl requires numeric value, passed %s'% repr(value) ) - - elif type(value) not in (types.IntType, types.LongType, types.FloatType): - dbg(indent=0) - raise ValueError ( - 'MaskedNumCtrl requires numeric value, passed %s'% repr(value) ) - - if not self._allowNegative and value < 0: - raise ValueError ( - 'control configured to disallow negative values, passed %s'% repr(value) ) - - if self.IsLimited() and apply_limits: - min = self.GetMin() - max = self.GetMax() - if not min is None and value < min: - dbg(indent=0) - raise ValueError ( - 'value %d is below minimum value of control'% value ) - if not max is None and value > max: - dbg(indent=0) - raise ValueError ( - 'value %d exceeds value of control'% value ) - - adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) - dbg('len(%s):' % self._mask, len(self._mask)) - dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) - dbg('adjustwidth:', adjustwidth) - if self._fractionWidth == 0: - s = str(long(value)).rjust(self._integerWidth) - else: - format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) - s = format % float(value) - dbg('s:"%s"' % s, 'len(s):', len(s)) - if len(s) > (adjustwidth - self._groupSpace): - dbg(indent=0) - raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) - elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): - dbg(indent=0) - raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) - - s = s.rjust(adjustwidth).replace('.', self._decimalChar) - if self._signOk and self._useParens: - if s.find('-') != -1: - s = s.replace('-', '(') + ')' - else: - s += ' ' - dbg('returned: "%s"' % s, indent=0) - return s - - - def _fromGUI( self, value ): - """ - Conversion function used in getting the value of the control. - """ - dbg(suspend=0) - dbg('MaskedNumCtrl::_fromGUI(%s)' % value, indent=1) - # One or more of the underlying text control implementations - # issue an intermediate EVT_TEXT when replacing the control's - # value, where the intermediate value is an empty string. - # So, to ensure consistency and to prevent spurious ValueErrors, - # we make the following test, and react accordingly: - # - if value.strip() == '': - if not self.IsNoneAllowed(): - dbg('empty value; not allowed,returning 0', indent = 0) - if self._fractionWidth: - return 0.0 - else: - return 0 - else: - dbg('empty value; returning None', indent = 0) - return None - else: - value = self._GetNumValue(value) - dbg('Num value: "%s"' % value) - if self._fractionWidth: - try: - dbg(indent=0) - return float( value ) - except ValueError: - dbg("couldn't convert to float; returning None") - return None - else: - raise - else: - try: - dbg(indent=0) - return int( value ) - except ValueError: - try: - dbg(indent=0) - return long( value ) - except ValueError: - dbg("couldn't convert to long; returning None") - return None - - else: - raise - else: - dbg('exception occurred; returning None') - return None - - - def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): - """ - Preprocessor for base control paste; if value needs to be right-justified - to fit in control, do so prior to paste: - """ - dbg('MaskedNumCtrl::_Paste (value = "%s")' % value) - if value is None: - paste_text = self._getClipboardContents() - else: - paste_text = value - - # treat paste as "replace number", if appropriate: - sel_start, sel_to = self._GetSelection() - if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent: - paste_text = self._toGUI(paste_text) - self._SetSelection(0, len(self._mask)) - - return MaskedEditMixin._Paste(self, - paste_text, - raise_on_invalid=raise_on_invalid, - just_return_value=just_return_value) - - - -#=========================================================================== - -if __name__ == '__main__': - - import traceback - - class myDialog(wx.Dialog): - def __init__(self, parent, id, title, - pos = wx.DefaultPosition, size = wx.DefaultSize, - style = wx.DEFAULT_DIALOG_STYLE ): - wx.Dialog.__init__(self, parent, id, title, pos, size, style) - - self.int_ctrl = MaskedNumCtrl(self, wx.NewId(), size=(55,20)) - self.OK = wx.Button( self, wx.ID_OK, "OK") - self.Cancel = wx.Button( self, wx.ID_CANCEL, "Cancel") - - vs = wx.BoxSizer( wx.VERTICAL ) - vs.Add( self.int_ctrl, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) - hs = wx.BoxSizer( wx.HORIZONTAL ) - hs.Add( self.OK, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) - hs.Add( self.Cancel, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) - vs.Add(hs, 0, wx.ALIGN_CENTRE|wx.ALL, 5 ) - - self.SetAutoLayout( True ) - self.SetSizer( vs ) - vs.Fit( self ) - vs.SetSizeHints( self ) - self.Bind(EVT_MASKEDNUM, self.OnChange, self.int_ctrl) - - def OnChange(self, event): - print 'value now', event.GetValue() - - class TestApp(wx.App): - def OnInit(self): - try: - self.frame = wx.Frame(None, -1, "Test", (20,20), (120,100) ) - self.panel = wx.Panel(self.frame, -1) - button = wx.Button(self.panel, -1, "Push Me", (20, 20)) - self.Bind(wx.EVT_BUTTON, self.OnClick, button) - except: - traceback.print_exc() - return False - return True - - def OnClick(self, event): - dlg = myDialog(self.panel, -1, "test MaskedNumCtrl") - dlg.int_ctrl.SetValue(501) - dlg.int_ctrl.SetInsertionPoint(1) - dlg.int_ctrl.SetSelection(1,2) - rc = dlg.ShowModal() - print 'final value', dlg.int_ctrl.GetValue() - del dlg - self.frame.Destroy() - - def Show(self): - self.frame.Show(True) - - try: - app = TestApp(0) - app.Show() - app.MainLoop() - except: - traceback.print_exc() - -i=0 -## To-Do's: -## =============================## -## 1. Add support for printf-style format specification. -## 2. Add option for repositioning on 'illegal' insertion point. -## -## Version 1.1 -## 1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. -## 2. Added autoSize parameter, to allow manual sizing of the control. -## 3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of -## nonsensical parameter methods from the control, so it will work -## properly with Boa. -## 4. Fixed allowNone bug found by user sameerc1@grandecom.net - diff --git a/wxPython/wx/lib/timectrl.py b/wxPython/wx/lib/timectrl.py deleted file mode 100644 index af42f0836b..0000000000 --- a/wxPython/wx/lib/timectrl.py +++ /dev/null @@ -1,1315 +0,0 @@ -#---------------------------------------------------------------------------- -# Name: timectrl.py -# Author: Will Sadkin -# Created: 09/19/2002 -# Copyright: (c) 2002 by Will Sadkin, 2002 -# RCS-ID: $Id$ -# License: wxWindows license -#---------------------------------------------------------------------------- -# NOTE: -# This was written way it is because of the lack of masked edit controls -# in wxWindows/wxPython. I would also have preferred to derive this -# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl -# component of that control is inaccessible through the interface exposed in -# wxPython. -# -# TimeCtrl does not use validators, because it does careful manipulation -# of the cursor in the text window on each keystroke, and validation is -# cursor-position specific, so the control intercepts the key codes before the -# validator would fire. -# -# TimeCtrl now also supports .SetValue() with either strings or wxDateTime -# values, as well as range limits, with the option of either enforcing them -# or simply coloring the text of the control if the limits are exceeded. -# -# Note: this class now makes heavy use of wxDateTime for parsing and -# regularization, but it always does so with ephemeral instances of -# wxDateTime, as the C++/Python validity of these instances seems to not -# persist. Because "today" can be a day for which an hour can "not exist" -# or be counted twice (1 day each per year, for DST adjustments), the date -# portion of all wxDateTimes used/returned have their date portion set to -# Jan 1, 1970 (the "epoch.") -#---------------------------------------------------------------------------- -# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o Updated for V2.5 compatability -# o wx.SpinCtl has some issues that cause the control to -# lock up. Noted in other places using it too, it's not this module -# that's at fault. -# -# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) -# -# o wxMaskedTextCtrl -> MaskedTextCtrl -# o wxTimeCtrl -> TimeCtrl -# - -""" -

-TimeCtrl 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 TimeCtrl, and the up/down arrows act -like a spin control. TimeCtrl also allows for an actual spin button to be attached -to the control, so that it acts like the up/down arrow keys. -

-The ! or c key sets the value of the control to the current time. -

-Here's the API for TimeCtrl: -

-    TimeCtrl(
-         parent, id = -1,
-         value = '12:00:00 AM',
-         pos = wx.DefaultPosition,
-         size = wx.DefaultSize,
-         style = wxTE_PROCESS_TAB,
-         validator = wx.DefaultValidator,
-         name = "time",
-         format = 'HHMMSS',         
-         fmt24hr = False,
-         displaySeconds = True,
-         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 wx.DefaultSize is specified. -
    style -
    By default, TimeCtrl will process TAB events, by allowing tab to the - different cells within the control. -
    validator -
    By default, TimeCtrl just uses the default (empty) validator, as all - of its validation for entry control is handled internally. However, a validator - can be supplied to provide data transfer capability to the control. -
    -
    format -
    This parameter can be used instead of the fmt24hr and displaySeconds - parameters, respectively; it provides a shorthand way to specify the time - format you want. Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and - '24HHMM'. If the format is specified, the other two arguments will be ignored. -
    -
    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. (This value is ignored if the format - parameter is specified.) -
    -
    displaySeconds -
    If True, control will include a seconds field; if False, it will - just show hours and minutes. (This value is ignored if the format - parameter is specified.) -
    -
    spinButton -
    If specified, this button's events will be bound to the behavior of the - TimeCtrl, working like up/down cursor key events. (See BindSpinButton.) -
    -
    min -
    Defines the lower bound for "valid" selections in the control. - By default, TimeCtrl doesn't have bounds. You must set both upper and lower - bounds to make the control pay attention to them, (as only one bound makes no sense - with times.) "Valid" times will fall between the min and max "pie wedge" of the - clock. -
    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(SpinBtton) -
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 copy -import string -import types - -import wx - -from wx.tools.dbg import Logger -from wx.lib.maskededit import BaseMaskedTextCtrl, Field - -dbg = Logger() -dbg(enable=0) - -try: - from mx import DateTime - accept_mx = True -except ImportError: - accept_mx = False - -# This class of event fires whenever the value of the time changes in the control: -wxEVT_TIMEVAL_UPDATED = wx.NewEventType() -EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1) - -class TimeUpdatedEvent(wx.PyCommandEvent): - def __init__(self, id, value ='12:00:00 AM'): - wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id) - self.value = value - def GetValue(self): - """Retrieve the value of the time control at the time this event was generated""" - return self.value - -class TimeCtrlAccessorsMixin: - # Define TimeCtrl's list of attributes having their own - # Get/Set functions, ignoring those that make no sense for - # an numeric control. - exposed_basectrl_params = ( - 'defaultValue', - 'description', - - 'useFixedWidthFont', - 'emptyBackgroundColour', - 'validBackgroundColour', - 'invalidBackgroundColour', - - 'validFunc', - 'validRequired', - ) - for param in exposed_basectrl_params: - propname = param[0].upper() + param[1:] - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - if param.find('Colour') != -1: - # add non-british spellings, for backward-compatibility - propname.replace('Colour', 'Color') - - exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) - exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) - - -class TimeCtrl(BaseMaskedTextCtrl): - - valid_ctrl_params = { - 'format' : 'HHMMSS', # default format code - 'displaySeconds' : True, # by default, shows seconds - 'min': None, # by default, no bounds set - 'max': None, - 'limited': False, # by default, no limiting even if bounds set - 'useFixedWidthFont': True, # by default, use a fixed-width font - 'oob_color': "Yellow" # by default, the default MaskedTextCtrl "invalid" color - } - - def __init__ ( - self, parent, id=-1, value = '12:00:00 AM', - pos = wx.DefaultPosition, size = wx.DefaultSize, - fmt24hr=False, - spinButton = None, - style = wx.TE_PROCESS_TAB, - validator = wx.DefaultValidator, - name = "time", - **kwargs ): - - # set defaults for control: - dbg('setting defaults:') - for key, param_value in TimeCtrl.valid_ctrl_params.items(): - # This is done this way to make setattr behave consistently with - # "private attribute" name mangling - setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) - - # create locals from current defaults, so we can override if - # specified in kwargs, and handle uniformly: - min = self.__min - max = self.__max - limited = self.__limited - self.__posCurrent = 0 - # handle deprecated keword argument name: - if kwargs.has_key('display_seconds'): - kwargs['displaySeconds'] = kwargs['display_seconds'] - del kwargs['display_seconds'] - if not kwargs.has_key('displaySeconds'): - kwargs['displaySeconds'] = True - - # (handle positional arg (from original release) differently from rest of kwargs:) - self.__fmt24hr = False - if not kwargs.has_key('format'): - if fmt24hr: - if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: - kwargs['format'] = '24HHMMSS' - del kwargs['displaySeconds'] - else: - kwargs['format'] = '24HHMM' - else: - if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: - kwargs['format'] = 'HHMMSS' - del kwargs['displaySeconds'] - else: - kwargs['format'] = 'HHMM' - - if not kwargs.has_key('useFixedWidthFont'): - # allow control over font selection: - kwargs['useFixedWidthFont'] = self.__useFixedWidthFont - - maskededit_kwargs = self.SetParameters(**kwargs) - - # allow for explicit size specification: - if size != wx.DefaultSize: - # override (and remove) "autofit" autoformat code in standard time formats: - maskededit_kwargs['formatcodes'] = 'T!' - - # This allows range validation if set - maskededit_kwargs['validFunc'] = self.IsInBounds - - # This allows range limits to affect insertion into control or not - # dynamically without affecting individual field constraint validation - maskededit_kwargs['retainFieldValidation'] = True - - # Now we can initialize the base control: - BaseMaskedTextCtrl.__init__( - self, parent, id=id, - pos=pos, size=size, - style = style, - validator = validator, - name = name, - setupEventHandling = False, - **maskededit_kwargs) - - - # This makes ':' act like tab (after we fix each ':' key event to remove "shift") - self._SetKeyHandler(':', self._OnChangeField) - - - # This makes the up/down keys act like spin button controls: - self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) - self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) - - - # This allows ! and c/C to set the control to the current time: - self._SetKeyHandler('!', self.__OnSetToNow) - self._SetKeyHandler('c', self.__OnSetToNow) - self._SetKeyHandler('C', self.__OnSetToNow) - - - # Set up event handling ourselves, so we can insert special - # processing on the ":' key to remove the "shift" attribute - # *before* the default handlers have been installed, so - # that : takes you forward, not back, and so we can issue - # EVT_TIMEUPDATE events on changes: - - self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection - self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator - self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field - self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick - self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. - self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event, - ## then call BaseMaskedTextCtrl._OnChar with - ## the possibly modified event. - self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events - - - # Validate initial value and set if appropriate - try: - self.SetBounds(min, max) - self.SetLimited(limited) - self.SetValue(value) - except: - self.SetValue('12:00:00 AM') - - if spinButton: - self.BindSpinButton(spinButton) # bind spin button up/down events to this control - - - def SetParameters(self, **kwargs): - dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) - maskededit_kwargs = {} - reset_format = False - - if kwargs.has_key('display_seconds'): - kwargs['displaySeconds'] = kwargs['display_seconds'] - del kwargs['display_seconds'] - if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): - del kwargs['displaySeconds'] # always apply format if specified - - # assign keyword args as appropriate: - for key, param_value in kwargs.items(): - if key not in TimeCtrl.valid_ctrl_params.keys(): - raise AttributeError('invalid keyword argument "%s"' % key) - - if key == 'format': - # handle both local or generic 'maskededit' autoformat codes: - if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': - self.__displaySeconds = True - self.__fmt24hr = False - elif param_value == 'HHMM' or param_value == 'TIMEHHMM': - self.__displaySeconds = False - self.__fmt24hr = False - elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': - self.__displaySeconds = True - self.__fmt24hr = True - elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': - self.__displaySeconds = False - self.__fmt24hr = True - else: - raise AttributeError('"%s" is not a valid format' % param_value) - reset_format = True - - elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'): - self.__displaySeconds = param_value - reset_format = True - - elif key == "min": min = param_value - elif key == "max": max = param_value - elif key == "limited": limited = param_value - - elif key == "useFixedWidthFont": - maskededit_kwargs[key] = param_value - - elif key == "oob_color": - maskededit_kwargs['invalidBackgroundColor'] = param_value - - if reset_format: - if self.__fmt24hr: - if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' - else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' - - # Set hour field to zero-pad, right-insert, require explicit field change, - # select entire field on entry, and require a resultant valid entry - # to allow character entry: - hourfield = Field(formatcodes='0r" % self.GetValue() - - - def SetValue(self, value): - """ - Validating SetValue function for time values: - This function will do dynamic type checking on the value argument, - and convert wxDateTime, mxDateTime, or 12/24 format time string - into the appropriate format string for the control. - """ - dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) - try: - strtime = self._toGUI(self.__validateValue(value)) - except: - dbg('validation failed', indent=0) - raise - - dbg('strtime:', strtime) - self._SetValue(strtime) - dbg(indent=0) - - def GetValue(self, - as_wxDateTime = False, - as_mxDateTime = False, - as_wxTimeSpan = False, - as_mxDateTimeDelta = False): - - - if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: - value = self.GetWxDateTime() - if as_wxDateTime: - pass - elif as_mxDateTime: - value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) - elif as_wxTimeSpan: - value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) - elif as_mxDateTimeDelta: - value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) - else: - value = BaseMaskedTextCtrl.GetValue(self) - return value - - - def SetWxDateTime(self, wxdt): - """ - Because SetValue can take a wxDateTime, this is now just an alias. - """ - self.SetValue(wxdt) - - - def GetWxDateTime(self, value=None): - """ - This function is the conversion engine for TimeCtrl; it takes - one of the following types: - time string - wxDateTime - wxTimeSpan - mxDateTime - mxDateTimeDelta - and converts it to a wxDateTime that always has Jan 1, 1970 as its date - portion, so that range comparisons around values can work using - wxDateTime's built-in comparison function. If a value is not - provided to convert, the string value of the control will be used. - If the value is not one of the accepted types, a ValueError will be - raised. - """ - global accept_mx - dbg(suspend=1) - dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) - if value is None: - dbg('getting control value') - value = self.GetValue() - dbg('value = "%s"' % value) - - if type(value) == types.UnicodeType: - value = str(value) # convert to regular string - - valid = True # assume true - if type(value) == types.StringType: - - # Construct constant wxDateTime, then try to parse the string: - wxdt = wx.DateTimeFromDMY(1, 0, 1970) - dbg('attempting conversion') - value = value.strip() # (parser doesn't like leading spaces) - checkTime = wxdt.ParseTime(value) - valid = checkTime == len(value) # entire string parsed? - dbg('checkTime == len(value)?', valid) - - if not valid: - dbg(indent=0, suspend=0) - raise ValueError('cannot convert string "%s" to valid time' % value) - - else: - if isinstance(value, wx.DateTime): - hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() - elif isinstance(value, wx.TimeSpan): - totalseconds = value.GetSeconds() - hour = totalseconds / 3600 - minute = totalseconds / 60 - (hour * 60) - second = totalseconds - ((hour * 3600) + (minute * 60)) - - elif accept_mx and isinstance(value, DateTime.DateTimeType): - hour, minute, second = value.hour, value.minute, value.second - elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): - hour, minute, second = value.hour, value.minute, value.second - else: - # Not a valid function argument - if accept_mx: - error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) - else: - error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) - dbg(indent=0, suspend=0) - raise ValueError(error) - - wxdt = wx.DateTimeFromDMY(1, 0, 1970) - wxdt.SetHour(hour) - wxdt.SetMinute(minute) - wxdt.SetSecond(second) - - dbg('wxdt:', wxdt, indent=0, suspend=0) - return wxdt - - - def SetMxDateTime(self, mxdt): - """ - Because SetValue can take an mxDateTime, (if DateTime is importable), - this is now just an alias. - """ - self.SetValue(value) - - - def GetMxDateTime(self, value=None): - if value is None: - t = self.GetValue(as_mxDateTime=True) - else: - # Convert string 1st to wxDateTime, then use components, since - # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: - wxdt = self.GetWxDateTime(value) - hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() - t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) - return t - - - def SetMin(self, min=None): - """ - Sets the minimum value of the control. If a value of None - is provided, then the control will have no explicit minimum value. - If the value specified is greater than the current maximum value, - then the function returns 0 and the minimum will not change from - its current setting. On success, the function returns 1. - - If successful and the current value is lower than the new lower - bound, if the control is limited, the value will be automatically - adjusted to the new minimum value; if not limited, the value in the - control will be colored as invalid. - """ - dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) - if min is not None: - try: - min = self.GetWxDateTime(min) - self.__min = self._toGUI(min) - except: - dbg('exception occurred', indent=0) - return False - else: - self.__min = min - - if self.IsLimited() and not self.IsInBounds(): - self.SetLimited(self.__limited) # force limited value: - else: - self._CheckValid() - ret = True - dbg('ret:', ret, indent=0) - return ret - - - def GetMin(self, as_string = False): - """ - Gets the minimum value of the control. - If None, it will return None. Otherwise it will return - the current minimum bound on the control, as a wxDateTime - by default, or as a string if as_string argument is True. - """ - dbg(suspend=1) - dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) - if self.__min is None: - dbg('(min == None)') - ret = self.__min - elif as_string: - ret = self.__min - dbg('ret:', ret) - else: - try: - ret = self.GetWxDateTime(self.__min) - except: - dbg(suspend=0) - dbg('exception occurred', indent=0) - dbg('ret:', repr(ret)) - dbg(indent=0, suspend=0) - return ret - - - def SetMax(self, max=None): - """ - Sets the maximum value of the control. If a value of None - is provided, then the control will have no explicit maximum value. - If the value specified is less than the current minimum value, then - the function returns False and the maximum will not change from its - current setting. On success, the function returns True. - - If successful and the current value is greater than the new upper - bound, if the control is limited the value will be automatically - adjusted to this maximum value; if not limited, the value in the - control will be colored as invalid. - """ - dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) - if max is not None: - try: - max = self.GetWxDateTime(max) - self.__max = self._toGUI(max) - except: - dbg('exception occurred', indent=0) - return False - else: - self.__max = max - dbg('max:', repr(self.__max)) - if self.IsLimited() and not self.IsInBounds(): - self.SetLimited(self.__limited) # force limited value: - else: - self._CheckValid() - ret = True - dbg('ret:', ret, indent=0) - return ret - - - def GetMax(self, as_string = False): - """ - Gets the minimum value of the control. - If None, it will return None. Otherwise it will return - the current minimum bound on the control, as a wxDateTime - by default, or as a string if as_string argument is True. - """ - dbg(suspend=1) - dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) - if self.__max is None: - dbg('(max == None)') - ret = self.__max - elif as_string: - ret = self.__max - dbg('ret:', ret) - else: - try: - ret = self.GetWxDateTime(self.__max) - except: - dbg(suspend=0) - dbg('exception occurred', indent=0) - raise - dbg('ret:', repr(ret)) - dbg(indent=0, suspend=0) - return ret - - - def SetBounds(self, min=None, max=None): - """ - This function is a convenience function for setting the min and max - values at the same time. The function only applies the maximum bound - if setting the minimum bound is successful, and returns True - only if both operations succeed. - NOTE: leaving out an argument will remove the corresponding bound. - """ - ret = self.SetMin(min) - return ret and self.SetMax(max) - - - def GetBounds(self, as_string = False): - """ - This function returns a two-tuple (min,max), indicating the - current bounds of the control. Each value can be None if - that bound is not set. - """ - return (self.GetMin(as_string), self.GetMax(as_string)) - - - def SetLimited(self, limited): - """ - If called with a value of True, this function will cause the control - to limit the value to fall within the bounds currently specified. - If the control's value currently exceeds the bounds, it will then - be limited accordingly. - - If called with a value of 0, this function will disable value - limiting, but coloring of out-of-bounds values will still take - place if bounds have been set for the control. - """ - dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) - self.__limited = limited - - if not limited: - self.SetMaskParameters(validRequired = False) - self._CheckValid() - dbg(indent=0) - return - - dbg('requiring valid value') - self.SetMaskParameters(validRequired = True) - - min = self.GetMin() - max = self.GetMax() - if min is None or max is None: - dbg('both bounds not set; no further action taken') - return # can't limit without 2 bounds - - elif not self.IsInBounds(): - # set value to the nearest bound: - try: - value = self.GetWxDateTime() - except: - dbg('exception occurred', indent=0) - raise - - if min <= max: # valid range doesn't span midnight - dbg('min <= max') - # which makes the "nearest bound" computation trickier... - - # determine how long the "invalid" pie wedge is, and cut - # this interval in half for comparison purposes: - - # Note: relies on min and max and value date portions - # always being the same. - interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max - - half_interval = wx.TimeSpan( - 0, # hours - 0, # minutes - interval.GetSeconds() / 2, # seconds - 0) # msec - - if value < min: # min is on next day, so use value on - # "next day" for "nearest" interval calculation: - cmp_value = value + wx.TimeSpan(24, 0, 0, 0) - else: # "before midnight; ok - cmp_value = value - - if (cmp_value - max) > half_interval: - dbg('forcing value to min (%s)' % min.FormatTime()) - self.SetValue(min) - else: - dbg('forcing value to max (%s)' % max.FormatTime()) - self.SetValue(max) - else: - dbg('max < min') - # therefore max < value < min guaranteed to be true, - # so "nearest bound" calculation is much easier: - if (value - max) >= (min - value): - # current value closer to min; pick that edge of pie wedge - dbg('forcing value to min (%s)' % min.FormatTime()) - self.SetValue(min) - else: - dbg('forcing value to max (%s)' % max.FormatTime()) - self.SetValue(max) - - dbg(indent=0) - - - - def IsLimited(self): - """ - Returns True if the control is currently limiting the - value to fall within any current bounds. Note: can - be set even if there are no current bounds. - """ - return self.__limited - - - def IsInBounds(self, value=None): - """ - Returns True if no value is specified and the current value - of the control falls within the current bounds. As the clock - is a "circle", both minimum and maximum bounds must be set for - a value to ever be considered "out of bounds". This function can - also be called with a value to see if that value would fall within - the current bounds of the given control. - """ - if value is not None: - try: - value = self.GetWxDateTime(value) # try to regularize passed value - except ValueError: - dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) - raise - - dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) - if self.__min is None or self.__max is None: - dbg(indent=0) - return True - - elif value is None: - try: - value = self.GetWxDateTime() - except: - dbg('exception occurred', indent=0) - - dbg('value:', value.FormatTime()) - - # Get wxDateTime representations of bounds: - min = self.GetMin() - max = self.GetMax() - - midnight = wx.DateTimeFromDMY(1, 0, 1970) - if min <= max: # they don't span midnight - ret = min <= value <= max - - else: - # have to break into 2 tests; to be in bounds - # either "min" <= value (<= midnight of *next day*) - # or midnight <= value <= "max" - ret = min <= value or (midnight <= value <= max) - dbg('in bounds?', ret, indent=0) - return ret - - - def IsValid( self, value ): - """ - Can be used to determine if a given value would be a legal and - in-bounds value for the control. - """ - try: - self.__validateValue(value) - return True - except ValueError: - return False - - def SetFormat(self, format): - self.SetParameters(format=format) - - def GetFormat(self): - if self.__displaySeconds: - if self.__fmt24hr: return '24HHMMSS' - else: return 'HHMMSS' - else: - if self.__fmt24hr: return '24HHMM' - else: return 'HHMM' - -#------------------------------------------------------------------------------------------------------------- -# these are private functions and overrides: - - - def __OnTextChange(self, event=None): - dbg('TimeCtrl::OnTextChange', indent=1) - - # Allow Maskedtext base control to color as appropriate, - # and Skip the EVT_TEXT event (if appropriate.) - ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() - ## call is generating two (2) EVT_TEXT events. (!) - ## The the only mechanism I can find to mask this problem is to - ## keep track of last value seen, and declare a valid EVT_TEXT - ## event iff the value has actually changed. The masked edit - ## OnTextChange routine does this, and returns True on a valid event, - ## False otherwise. - if not BaseMaskedTextCtrl._OnTextChange(self, event): - return - - dbg('firing TimeUpdatedEvent...') - evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) - evt.SetEventObject(self) - self.GetEventHandler().ProcessEvent(evt) - dbg(indent=0) - - - def SetInsertionPoint(self, pos): - """ - Records the specified position and associated cell before calling base class' function. - This is necessary to handle the optional spin button, because the insertion - point is lost when the focus shifts to the spin button. - """ - dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) - BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire) - self.__posCurrent = self.GetInsertionPoint() - dbg(indent=0) - - - def SetSelection(self, sel_start, sel_to): - dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) - - # Adjust selection range to legal extent if not already - if sel_start < 0: - sel_start = 0 - - if self.__posCurrent != sel_start: # force selection and insertion point to match - self.SetInsertionPoint(sel_start) - cell_start, cell_end = self._FindField(sel_start)._extent - if not cell_start <= sel_to <= cell_end: - sel_to = cell_end - - self.__bSelection = sel_start != sel_to - BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) - dbg(indent=0) - - - def __OnSpin(self, key): - """ - This is the function that gets called in response to up/down arrow or - bound spin button events. - """ - self.__IncrementValue(key, self.__posCurrent) # changes the value - - # Ensure adjusted control regains focus and has adjusted portion - # selected: - self.SetFocus() - start, end = self._FindField(self.__posCurrent)._extent - self.SetInsertionPoint(start) - self.SetSelection(start, end) - dbg('current position:', self.__posCurrent) - - - def __OnSpinUp(self, event): - """ - Event handler for any bound spin button on EVT_SPIN_UP; - causes control to behave as if up arrow was pressed. - """ - dbg('TimeCtrl::OnSpinUp', indent=1) - self.__OnSpin(wx.WXK_UP) - keep_processing = False - dbg(indent=0) - return keep_processing - - - def __OnSpinDown(self, event): - """ - Event handler for any bound spin button on EVT_SPIN_DOWN; - causes control to behave as if down arrow was pressed. - """ - dbg('TimeCtrl::OnSpinDown', indent=1) - self.__OnSpin(wx.WXK_DOWN) - keep_processing = False - dbg(indent=0) - return keep_processing - - - def __OnChar(self, event): - """ - Handler to explicitly look for ':' keyevents, and if found, - clear the m_shiftDown field, so it will behave as forward tab. - It then calls the base control's _OnChar routine with the modified - event instance. - """ - dbg('TimeCtrl::OnChar', indent=1) - keycode = event.GetKeyCode() - dbg('keycode:', keycode) - if keycode == ord(':'): - dbg('colon seen! removing shift attribute') - event.m_shiftDown = False - BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress - dbg(indent=0) - - - def __OnSetToNow(self, event): - """ - This is the key handler for '!' and 'c'; this allows the user to - quickly set the value of the control to the current time. - """ - self.SetValue(wx.DateTime_Now().FormatTime()) - keep_processing = False - return keep_processing - - - def __LimitSelection(self, event): - """ - Event handler for motion events; this handler - changes limits the selection to the new cell boundaries. - """ - dbg('TimeCtrl::LimitSelection', indent=1) - pos = self.GetInsertionPoint() - self.__posCurrent = pos - sel_start, sel_to = self.GetSelection() - selection = sel_start != sel_to - if selection: - # only allow selection to end of current cell: - start, end = self._FindField(sel_start)._extent - if sel_to < pos: sel_to = start - elif sel_to > pos: sel_to = end - - dbg('new pos =', self.__posCurrent, 'select to ', sel_to) - self.SetInsertionPoint(self.__posCurrent) - self.SetSelection(self.__posCurrent, sel_to) - if event: event.Skip() - dbg(indent=0) - - - def __IncrementValue(self, key, pos): - dbg('TimeCtrl::IncrementValue', key, pos, indent=1) - text = self.GetValue() - field = self._FindField(pos) - dbg('field: ', field._index) - start, end = field._extent - slice = text[start:end] - if key == wx.WXK_UP: increment = 1 - else: increment = -1 - - if slice in ('A', 'P'): - if slice == 'A': newslice = 'P' - elif slice == 'P': newslice = 'A' - newvalue = text[:start] + newslice + text[end:] - - elif field._index == 0: - # adjusting this field is trickier, as its value can affect the - # am/pm setting. So, we use wxDateTime to generate a new value for us: - # (Use a fixed date not subject to DST variations:) - converter = wx.DateTimeFromDMY(1, 0, 1970) - dbg('text: "%s"' % text) - converter.ParseTime(text.strip()) - currenthour = converter.GetHour() - dbg('current hour:', currenthour) - newhour = (currenthour + increment) % 24 - dbg('newhour:', newhour) - converter.SetHour(newhour) - dbg('converter.GetHour():', converter.GetHour()) - newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue() - - else: # minute or second field; handled the same way: - newslice = "%02d" % ((int(slice) + increment) % 60) - newvalue = text[:start] + newslice + text[end:] - - try: - self.SetValue(newvalue) - - except ValueError: # must not be in bounds: - if not wx.Validator_IsSilent(): - wx.Bell() - dbg(indent=0) - - - def _toGUI( self, wxdt ): - """ - This function takes a wxdt as an unambiguous representation of a time, and - converts it to a string appropriate for the format of the control. - """ - if self.__fmt24hr: - if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') - else: strval = wxdt.Format('%H:%M') - else: - if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') - else: strval = wxdt.Format('%I:%M %p') - - return strval - - - def __validateValue( self, value ): - """ - This function converts the value to a wxDateTime if not already one, - does bounds checking and raises ValueError if argument is - not a valid value for the control as currently specified. - It is used by both the SetValue() and the IsValid() methods. - """ - dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) - if not value: - dbg(indent=0) - raise ValueError('%s not a valid time value' % repr(value)) - - valid = True # assume true - try: - value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so - except: - dbg('exception occurred', indent=0) - raise - - if self.IsLimited() and not self.IsInBounds(value): - dbg(indent=0) - raise ValueError ( - 'value %s is not within the bounds of the control' % str(value) ) - dbg(indent=0) - return value - -#---------------------------------------------------------------------------- -# Test jig for TimeCtrl: - -if __name__ == '__main__': - import traceback - - class TestPanel(wx.Panel): - def __init__(self, parent, id, - pos = wx.DefaultPosition, size = wx.DefaultSize, - fmt24hr = 0, test_mx = 0, - style = wx.TAB_TRAVERSAL ): - - wx.Panel.__init__(self, parent, id, pos, size, style) - - self.test_mx = test_mx - - self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) - sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) - self.tc.BindSpinButton(sb) - - sizer = wx.BoxSizer( wx.HORIZONTAL ) - sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) - sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) - - self.SetAutoLayout( True ) - self.SetSizer( sizer ) - sizer.Fit( self ) - sizer.SetSizeHints( self ) - - self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) - - def OnTimeChange(self, event): - dbg('OnTimeChange: value = ', event.GetValue()) - wxdt = self.tc.GetWxDateTime() - dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) - if self.test_mx: - mxdt = self.tc.GetMxDateTime() - dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) - - - class MyApp(wx.App): - def OnInit(self): - import sys - fmt24hr = '24' in sys.argv - test_mx = 'mx' in sys.argv - try: - frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) - panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) - frame.Show(True) - except: - traceback.print_exc() - return False - return True - - try: - app = MyApp(0) - app.MainLoop() - except: - traceback.print_exc() -i=0 -## Version 1.2 -## 1. Changed parameter name display_seconds to displaySeconds, to follow -## other masked edit conventions. -## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. -## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of -## nonsensical parameter methods from the control, so it will work -## properly with Boa. diff --git a/wxPython/wxPython/lib/maskedctrl.py b/wxPython/wxPython/lib/maskedctrl.py index c022bf40e3..197cd9db65 100644 --- a/wxPython/wxPython/lib/maskedctrl.py +++ b/wxPython/wxPython/lib/maskedctrl.py @@ -2,9 +2,21 @@ ## backwards compatibility. Some names will also have a 'wx' added on if ## that is how they used to be named in the old wxPython package. -import wx.lib.maskedctrl +import wx.lib.masked.ctrl -__doc__ = wx.lib.maskedctrl.__doc__ +__doc__ = wx.lib.masked.ctrl.__doc__ -controlTypes = wx.lib.maskedctrl.controlTypes -wxMaskedCtrl = wx.lib.maskedctrl.MaskedCtrl +MASKEDTEXT = wx.lib.masked.ctrl.TEXT +MASKEDCOMBO = wx.lib.masked.ctrl.COMBO +IPADDR = wx.lib.masked.ctrl.IPADDR +TIME = wx.lib.masked.ctrl.TIME +NUMBER = wx.lib.masked.ctrl.NUMBER + +class controlTypes: + MASKEDTEXT = wx.lib.masked.ctrl.TEXT + MASKEDCOMBO = wx.lib.masked.ctrl.COMBO + IPADDR = wx.lib.masked.ctrl.IPADDR + TIME = wx.lib.masked.ctrl.TIME + NUMBER = wx.lib.masked.ctrl.NUMBER + +wxMaskedCtrl = wx.lib.masked.Ctrl diff --git a/wxPython/wxPython/lib/maskededit.py b/wxPython/wxPython/lib/maskededit.py index 42e4331cd5..7c4da9d1c1 100644 --- a/wxPython/wxPython/lib/maskededit.py +++ b/wxPython/wxPython/lib/maskededit.py @@ -1,16 +1,15 @@ ## This file imports items from the wx package into the wxPython package for ## backwards compatibility. Some names will also have a 'wx' added on if ## that is how they used to be named in the old wxPython package. +import wx.lib.masked +import wx.lib.masked.maskededit -import wx.lib.maskededit +__doc__ = wx.lib.masked.maskededit.__doc__ -__doc__ = wx.lib.maskededit.__doc__ +from wx.lib.masked.maskededit import * -Field = wx.lib.maskededit.Field -test = wx.lib.maskededit.test -test2 = wx.lib.maskededit.test2 -wxIpAddrCtrl = wx.lib.maskededit.IpAddrCtrl -wxMaskedComboBox = wx.lib.maskededit.MaskedComboBox -wxMaskedComboBoxSelectEvent = wx.lib.maskededit.MaskedComboBoxSelectEvent -wxMaskedEditMixin = wx.lib.maskededit.MaskedEditMixin -wxMaskedTextCtrl = wx.lib.maskededit.MaskedTextCtrl +wxMaskedEditMixin = wx.lib.masked.MaskedEditMixin +wxMaskedTextCtrl = wx.lib.masked.TextCtrl +wxMaskedComboBox = wx.lib.masked.ComboBox +wxMaskedComboBoxSelectEvent = wx.lib.masked.MaskedComboBoxSelectEvent +wxIpAddrCtrl = wx.lib.masked.IpAddrCtrl diff --git a/wxPython/wxPython/lib/maskednumctrl.py b/wxPython/wxPython/lib/maskednumctrl.py index 99e09bc188..69c9032e62 100644 --- a/wxPython/wxPython/lib/maskednumctrl.py +++ b/wxPython/wxPython/lib/maskednumctrl.py @@ -2,10 +2,10 @@ ## backwards compatibility. Some names will also have a 'wx' added on if ## that is how they used to be named in the old wxPython package. -import wx.lib.maskednumctrl +import wx.lib.masked.numctrl -__doc__ = wx.lib.maskednumctrl.__doc__ +__doc__ = wx.lib.masked.numctrl.__doc__ -EVT_MASKEDNUM = wx.lib.maskednumctrl.EVT_MASKEDNUM -wxMaskedNumCtrl = wx.lib.maskednumctrl.MaskedNumCtrl -wxMaskedNumNumberUpdatedEvent = wx.lib.maskednumctrl.MaskedNumNumberUpdatedEvent +EVT_MASKEDNUM = wx.lib.masked.numctrl.EVT_NUM +wxMaskedNumCtrl = wx.lib.masked.numctrl.NumCtrl +wxMaskedNumNumberUpdatedEvent = wx.lib.masked.numctrl.NumberUpdatedEvent diff --git a/wxPython/wxPython/lib/timectrl.py b/wxPython/wxPython/lib/timectrl.py index da46bcf97a..d71c4cebd6 100644 --- a/wxPython/wxPython/lib/timectrl.py +++ b/wxPython/wxPython/lib/timectrl.py @@ -2,10 +2,10 @@ ## backwards compatibility. Some names will also have a 'wx' added on if ## that is how they used to be named in the old wxPython package. -import wx.lib.timectrl +import wx.lib.masked.timectrl -__doc__ = wx.lib.timectrl.__doc__ +__doc__ = wx.lib.masked.timectrl.__doc__ -EVT_TIMEUPDATE = wx.lib.timectrl.EVT_TIMEUPDATE -TimeUpdatedEvent = wx.lib.timectrl.TimeUpdatedEvent -wxTimeCtrl = wx.lib.timectrl.TimeCtrl +EVT_TIMEUPDATE = wx.lib.masked.timectrl.EVT_TIMEUPDATE +TimeUpdatedEvent = wx.lib.masked.timectrl.TimeUpdatedEvent +wxTimeCtrl = wx.lib.masked.timectrl.TimeCtrl