1 #---------------------------------------------------------------------------- 
   3 # Authors:      Jeff Childers, Will Sadkin 
   4 # Email:        jchilders_98@yahoo.com, wsadkin@parlancecorp.com 
   6 # Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003 
   7 # Portions:     (c) 2002 by Will Sadkin, 2002-2006 
   9 # License:      wxWindows license 
  10 #---------------------------------------------------------------------------- 
  12 #   MaskedEdit controls are based on a suggestion made on [wxPython-Users] by 
  13 #   Jason Hihn, and borrows liberally from Will Sadkin's original masked edit 
  14 #   control for time entry, TimeCtrl (which is now rewritten using this 
  17 #   MaskedEdit controls do not normally use validators, because they do 
  18 #   careful manipulation of the cursor in the text window on each keystroke, 
  19 #   and validation is cursor-position specific, so the control intercepts the 
  20 #   key codes before the validator would fire.  However, validators can be 
  21 #   provided to do data transfer to the controls. 
  23 #---------------------------------------------------------------------------- 
  25 # This file now contains the bulk of the logic behind all masked controls, 
  26 # the MaskedEditMixin class, the Field class, and the autoformat codes. 
  28 #---------------------------------------------------------------------------- 
  30 # 03/30/2004 - Will Sadkin (wsadkin@nameconnector.com) 
  32 # o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files, 
  33 # o Reorganized code into masked package 
  35 # 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  37 # o Updated for wx namespace. No guarantees. This is one huge file. 
  39 # 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  41 # o Missed wx.DateTime stuff earlier. 
  43 # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  45 # o MaskedEditMixin -> MaskedEditMixin 
  46 # o wxMaskedTextCtrl -> maskedTextCtrl 
  47 # o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent 
  48 # o wxMaskedComboBox -> MaskedComboBox 
  49 # o wxIpAddrCtrl -> IpAddrCtrl 
  50 # o wxTimeCtrl -> TimeCtrl 
  54 contains MaskedEditMixin class that drives all the other masked controls. 
  61     is a sublassed text control that can carefully control the user's input 
  62     based on a mask string you provide. 
  64     General usage example:: 
  66         control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####') 
  68     The example above will create a text control that allows only numbers to be 
  69     entered and then only in the positions indicated in the mask by the # sign. 
  72     is a similar subclass of wxComboBox that allows the same sort of masking, 
  73     but also can do auto-complete of values, and can require the value typed 
  74     to be in the list of choices to be colored appropriately. 
  77     is actually a factory function for several types of masked edit controls: 
  79     =================   ================================================== 
  80     masked.TextCtrl     standard masked edit text box 
  81     masked.ComboBox     adds combobox capabilities 
  82     masked.IpAddrCtrl   adds special semantics for IP address entry 
  83     masked.TimeCtrl     special subclass handling lots of types as values 
  84     masked.NumCtrl      special subclass handling numeric values 
  85     =================   ================================================== 
  87     It works by looking for a *controlType* parameter in the keyword 
  88     arguments of the control, to determine what kind of instance to return. 
  89     If not specified as a keyword argument, the default control type returned 
  90     will be masked.TextCtrl. 
  92     Each of the above classes has its own set of arguments, but masked.Ctrl 
  93     provides a single "unified" interface for masked controls. 
  95 What follows is a description of how to configure the generic masked.TextCtrl 
  96 and masked.ComboBox;  masked.NumCtrl and masked.TimeCtrl have their own demo  
  97 pages and interface descriptions. 
  99 ========================= 
 101 Initialization Parameters 
 102 ------------------------- 
 104     Allowed mask characters and function: 
 106     =========  ========================================================== 
 108     =========  ========================================================== 
 109         #       Allow numeric only (0-9) 
 110         N       Allow letters and numbers (0-9) 
 111         A       Allow uppercase letters only 
 112         a       Allow lowercase letters only 
 113         C       Allow any letter, upper or lower 
 114         X       Allow string.letters, string.punctuation, string.digits 
 115         &       Allow string.punctuation only (doesn't include all unicode symbols) 
 116         \*       Allow any visible character 
 117         |       explicit field boundary (takes no space in the control; allows mix 
 118                 of adjacent mask characters to be treated as separate fields, 
 119                 eg: '&|###' means "field 0 = '&', field 1 = '###'", but there's 
 120                 no fixed characters in between. 
 121     =========  ========================================================== 
 124     These controls define these sets of characters using string.letters, 
 125     string.uppercase, etc.  These sets are affected by the system locale 
 126     setting, so in order to have the masked controls accept characters 
 127     that are specific to your users' language, your application should 
 129     For example, to allow international characters to be used in the 
 130     above masks, you can place the following in your code as part of 
 131     your application's initialization code:: 
 134       locale.setlocale(locale.LC_ALL, '') 
 136     The controls now also support (by popular demand) all "visible" characters, 
 137     by use of the * mask character, including unicode characters above 
 138     the standard ANSI keycode range. 
 139     Note:  As string.punctuation doesn't typically include all unicode 
 140     symbols, you will have to use includechars to get some of these into  
 141     otherwise restricted positions in your control, such as those specified 
 144   Using these mask characters, a variety of template masks can be built. See 
 145   the demo for some other common examples include date+time, social security 
 146   number, etc.  If any of these characters are needed as template rather 
 147   than mask characters, they can be escaped with \, ie. \N means "literal N". 
 148   (use \\ for literal backslash, as in: r'CCC\\NNN'.) 
 152       Masks containing only # characters and one optional decimal point 
 153       character are handled specially, as "numeric" controls.  Such 
 154       controls have special handling for typing the '-' key, handling 
 155       the "decimal point" character as truncating the integer portion, 
 156       optionally allowing grouping characters and so forth. 
 157       There are several parameters and format codes that only make sense 
 158       when combined with such masks, eg. groupChar, decimalChar, and so 
 159       forth (see below).  These allow you to construct reasonable 
 160       numeric entry controls. 
 163       Changing the mask for a control deletes any previous field classes 
 164       (and any associated validation or formatting constraints) for them. 
 167   By default, masked edit controls use a fixed width font, so that 
 168   the mask characters are fixed within the control, regardless of 
 169   subsequent modifications to the value.  Set to False if having 
 170   the control font be the same as other controls is required. (This is 
 171   a control-level parameter.) 
 174   (Applies to unicode systems only) By default, the default unicode encoding 
 175   used is latin1, or iso-8859-1.  If necessary, you can set this control-level 
 176   parameter to govern the codec used to decode your keyboard inputs. 
 177   (This is a control-level parameter.) 
 180   These other properties can be passed to the class when instantiating it: 
 181     Formatcodes are specified as a string of single character formatting 
 182     codes that modify  behavior of the control:: 
 187             R  Right-align field(s) 
 188             r  Right-insert in field(s) (implies R) 
 189             <  Stay in field until explicit navigation out of it 
 191             >  Allow insert/delete within partially filled fields (as 
 192                opposed to the default "overwrite" mode for fixed-width 
 193                masked edit controls.)  This allows single-field controls 
 194                or each field within a multi-field control to optionally 
 195                behave more like standard text controls. 
 196                (See EMAIL or phone number autoformat examples.) 
 198                *Note: This also governs whether backspace/delete operations 
 199                shift contents of field to right of cursor, or just blank the 
 202                Also, when combined with 'r', this indicates that the field 
 203                or control allows right insert anywhere within the current 
 204                non-empty value in the field.  (Otherwise right-insert behavior 
 205                is only performed to when the entire right-insertable field is 
 206                selected or the cursor is at the right edge of the field.* 
 209             ,  Allow grouping character in integer fields of numeric controls 
 210                and auto-group/regroup digits (if the result fits) when leaving 
 211                such a field.  (If specified, .SetValue() will attempt to 
 213                ',' is also the default grouping character.  To change the 
 214                grouping character and/or decimal character, use the groupChar 
 215                and decimalChar parameters, respectively. 
 216                Note: typing the "decimal point" character in such fields will 
 217                clip the value to that left of the cursor for integer 
 218                fields of controls with "integer" or "floating point" masks. 
 219                If the ',' format code is specified, this will also cause the 
 220                resulting digits to be regrouped properly, using the current 
 222             -  Prepend and reserve leading space for sign to mask and allow 
 223                signed values (negative #s shown in red by default.) Can be 
 224                used with argument useParensForNegatives (see below.) 
 225             0  integer fields get leading zeros 
 228             F  Auto-Fit: the control calulates its size from 
 229                the length of the template mask 
 230             V  validate entered chars against validRegex before allowing them 
 231                to be entered vs. being allowed by basic mask and then having 
 232                the resulting value just colored as invalid. 
 233                (See USSTATE autoformat demo for how this can be used.) 
 234             S  select entire field when navigating to new field 
 239   These controls have two options for the initial state of the control. 
 240   If a blank control with just the non-editable characters showing 
 241   is desired, simply leave the constructor variable fillChar as its 
 242   default (' ').  If you want some other character there, simply 
 243   change the fillChar to that value.  Note: changing the control's fillChar 
 244   will implicitly reset all of the fields' fillChars to this value. 
 246   If you need different default characters in each mask position, 
 247   you can specify a defaultValue parameter in the constructor, or 
 248   set them for each field individually. 
 249   This value must satisfy the non-editable characters of the mask, 
 250   but need not conform to the replaceable characters. 
 255   These parameters govern what character is used to group numbers 
 256   and is used to indicate the decimal point for numeric format controls. 
 257   The default groupChar is ',', the default decimalChar is '.' 
 258   By changing these, you can customize the presentation of numbers 
 263         formatcodes = ',', groupChar='\''                  allows  12'345.34 
 264         formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34 
 266   (These are control-level parameters.) 
 269   The default "shiftDecimalChar" (used for "backwards-tabbing" until 
 270   shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for 
 271   other keyboards, you may want to customize this, eg '?' for shift ',' on 
 272   AZERTY keyboards, ':' or ';' for other European keyboards, etc. 
 273   (This is a control-level parameter.) 
 275 useParensForNegatives=False 
 276   This option can be used with signed numeric format controls to 
 277   indicate signs via () rather than '-'. 
 278   (This is a control-level parameter.) 
 281   This option can be used to have a field or the control try to 
 282   auto-complete on each keystroke if choices have been specified. 
 284 autoCompleteKeycodes=[] 
 285   By default, DownArrow, PageUp and PageDown will auto-complete a 
 286   partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp 
 287   and PageDown will also auto-complete, but if the field already 
 288   contains a matched value, these keys will cycle through the list 
 289   of choices forward or backward as appropriate.  Shift-Up and 
 290   Shift-Down also take you to the next/previous field after any 
 291   auto-complete action. 
 293   Additional auto-complete keys can be specified via this parameter. 
 294   Any keys so specified will act like PageDown. 
 295   (This is a control-level parameter.) 
 299 Validating User Input 
 300 ===================== 
 301 There are a variety of initialization parameters that are used to validate 
 302 user input.  These parameters can apply to the control as a whole, and/or 
 303 to individual fields: 
 305         =====================  ================================================================== 
 306         excludeChars           A string of characters to exclude even if otherwise allowed 
 307         includeChars           A string of characters to allow even if otherwise disallowed 
 308         validRegex             Use a regular expression to validate the contents of the text box 
 309         validRange             Pass a rangeas list (low,high) to limit numeric fields/values 
 310         choices                A list of strings that are allowed choices for the control. 
 311         choiceRequired         value must be member of choices list 
 312         compareNoCase          Perform case-insensitive matching when validating against list 
 313                                *Note: for masked.ComboBox, this defaults to True.* 
 314         emptyInvalid           Boolean indicating whether an empty value should be considered  
 317         validFunc              A function to call of the form: bool = func(candidate_value) 
 318                                which will return True if the candidate_value satisfies some 
 319                                external criteria for the control in addition to the the 
 320                                other validation, or False if not.  (This validation is 
 321                                applied last in the chain of validations.) 
 323         validRequired          Boolean indicating whether or not keys that are allowed by the 
 324                                mask, but result in an invalid value are allowed to be entered 
 325                                into the control.  Setting this to True implies that a valid 
 326                                default value is set for the control. 
 328         retainFieldValidation  False by default; if True, this allows individual fields to 
 329                                retain their own validation constraints independently of any 
 330                                subsequent changes to the control's overall parameters. 
 331                                (This is a control-level parameter.) 
 333         validator              Validators are not normally needed for masked controls, because 
 334                                of the nature of the validation and control of input.  However, 
 335                                you can supply one to provide data transfer routines for the 
 337         raiseOnInvalidPaste    False by default; normally a bad paste simply is ignored with a bell; 
 338                                if True, this will cause a ValueError exception to be thrown, 
 339                                with the .value attribute of the exception containing the bad value. 
 340         =====================  ================================================================== 
 345   The following parameters have been provided to allow you to change the default 
 346   coloring behavior of the control.   These can be set at construction, or via 
 347   the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow': 
 349         ========================  ======================================================================= 
 350         emptyBackgroundColour      Control Background color when identified as empty. Default=White 
 351         invalidBackgroundColour    Control Background color when identified as Not valid. Default=Yellow 
 352         validBackgroundColour      Control Background color when identified as Valid. Default=white 
 353         ========================  ======================================================================= 
 356   The following parameters control the default foreground color coloring behavior of the 
 357   control. Pass a color as string e.g. 'Yellow': 
 359         ========================  ====================================================================== 
 360         foregroundColour           Control foreground color when value is not negative.  Default=Black 
 361         signedForegroundColour     Control foreground color when value is negative. Default=Red 
 362         ========================  ====================================================================== 
 367   Each part of the mask that allows user input is considered a field.  The fields 
 368   are represented by their own class instances.  You can specify field-specific 
 369   constraints by constructing or accessing the field instances for the control 
 370   and then specifying those constraints via parameters. 
 373   This parameter allows you to specify Field instances containing 
 374   constraints for the individual fields of a control, eg: local 
 375   choice lists, validation rules, functions, regexps, etc. 
 376   It can be either an ordered list or a dictionary.  If a list, 
 377   the fields will be applied as fields 0, 1, 2, etc. 
 378   If a dictionary, it should be keyed by field index. 
 379   the values should be a instances of maskededit.Field. 
 381   Any field not represented by the list or dictionary will be 
 382   implicitly created by the control. 
 386     fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ] 
 391                1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']), 
 392                3: ( Field(choices=['01', '02', '03'], choiceRequired=True) 
 395   The following parameters are available for individual fields, with the 
 396   same semantics as for the whole control but applied to the field in question: 
 398     ==============  ============================================================================= 
 399     fillChar        if set for a field, it will override the control's fillChar for that field 
 400     groupChar       if set for a field, it will override the control's default 
 401     defaultValue    sets field-specific default value; overrides any default from control 
 402     compareNoCase   overrides control's settings 
 403     emptyInvalid    determines whether field is required to be filled at all times 
 404     validRequired   if set, requires field to contain valid value 
 405     ==============  ============================================================================= 
 407   If any of the above parameters are subsequently specified for the control as a 
 408   whole, that new value will be propagated to each field, unless the 
 409   retainFieldValidation control-level parameter is set. 
 411     ==============  ============================== 
 412     formatcodes      Augments control's settings 
 420     ==============  ============================== 
 424 Control Class Functions 
 425 ======================= 
 426 .GetPlainValue(value=None) 
 427                     Returns the value specified (or the control's text value 
 428                     not specified) without the formatting text. 
 429                     In the example above, might return phone no='3522640075', 
 430                     whereas control.GetValue() would return '(352) 264-0075' 
 432                     Returns the control's value to its default, and places the 
 433                     cursor at the beginning of the control. 
 435                     Does "smart replacement" of passed value into the control, as does 
 436                     the .Paste() method.  As with other text entry controls, the 
 437                     .SetValue() text replacement begins at left-edge of the control, 
 438                     with missing mask characters inserted as appropriate. 
 439                     .SetValue will also adjust integer, float or date mask entry values, 
 440                     adding commas, auto-completing years, etc. as appropriate. 
 441                     For "right-aligned" numeric controls, it will also now automatically 
 442                     right-adjust any value whose length is less than the width of the 
 443                     control before attempting to set the value. 
 444                     If a value does not follow the format of the control's mask, or will 
 445                     not fit into the control, a ValueError exception will be raised. 
 449                       mask = '(###) ###-####' 
 450                           .SetValue('1234567890')           => '(123) 456-7890' 
 451                           .SetValue('(123)4567890')         => '(123) 456-7890' 
 452                           .SetValue('(123)456-7890')        => '(123) 456-7890' 
 453                           .SetValue('123/4567-890')         => illegal paste; ValueError 
 455                       mask = '#{6}.#{2}', formatcodes = '_,-', 
 456                           .SetValue('111')                  => ' 111   .  ' 
 457                           .SetValue(' %9.2f' % -111.12345 ) => '   -111.12' 
 458                           .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00' 
 459                           .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError 
 461                       mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control 
 462                           .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit 
 463                           .SetValue('%.2f' % 111 )          => '    111.00' 
 464                           .SetValue('%.2f' % -111.12345 )   => '   -111.12' 
 468                     Returns True if the value specified (or the value of the control 
 469                     if not specified) passes validation tests 
 471                     Returns True if the value specified (or the value of the control 
 472                     if not specified) is equal to an "empty value," ie. all 
 473                     editable characters == the fillChar for their respective fields. 
 474 .IsDefault(value=None) 
 475                     Returns True if the value specified (or the value of the control 
 476                     if not specified) is equal to the initial value of the control. 
 479                     Recolors the control as appropriate to its current settings. 
 481 .SetCtrlParameters(\*\*kwargs) 
 482                     This function allows you to set up and/or change the control parameters 
 483                     after construction; it takes a list of key/value pairs as arguments, 
 484                     where the keys can be any of the mask-specific parameters in the constructor. 
 488                         ctl = masked.TextCtrl( self, -1 ) 
 489                         ctl.SetCtrlParameters( mask='###-####', 
 490                                                defaultValue='555-1212', 
 493 .GetCtrlParameter(parametername) 
 494                     This function allows you to retrieve the current value of a parameter 
 497   *Note:* Each of the control parameters can also be set using its 
 498       own Set and Get function.  These functions follow a regular form: 
 499       All of the parameter names start with lower case; for their 
 500       corresponding Set/Get function, the parameter name is capitalized. 
 504           ctl.SetMask('###-####') 
 505           ctl.SetDefaultValue('555-1212') 
 506           ctl.GetChoiceRequired() 
 509   *Note:* After any change in parameters, the choices for the 
 510       control are reevaluated to ensure that they are still legal.  If you 
 511       have large choice lists, it is therefore more efficient to set parameters 
 512       before setting the choices available. 
 514 .SetFieldParameters(field_index, \*\*kwargs) 
 515                     This function allows you to specify change individual field 
 516                     parameters after construction. (Indices are 0-based.) 
 518 .GetFieldParameter(field_index, parametername) 
 519                     Allows the retrieval of field parameters after construction 
 522 The control detects certain common constructions. In order to use the signed feature 
 523 (negative numbers and coloring), the mask has to be all numbers with optionally one 
 524 decimal point. Without a decimal (e.g. '######', the control will treat it as an integer 
 525 value. With a decimal (e.g. '###.##'), the control will act as a floating point control 
 526 (i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the 
 527 integer control truncates the value.  However, for a true numeric control, 
 528 masked.NumCtrl provides all this, and true numeric input/output support as well. 
 531 Check your controls by calling each control's .IsValid() function and the 
 532 .IsEmpty() function to determine which controls have been a) filled in and 
 533 b) filled in properly. 
 536 Regular expression validations can be used flexibly and creatively. 
 537 Take a look at the demo; the zip-code validation succeeds as long as the 
 538 first five numerals are entered. the last four are optional, but if 
 539 any are entered, there must be 4 to be valid. 
 541 masked.Ctrl Configuration 
 542 ========================= 
 543 masked.Ctrl works by looking for a special *controlType* 
 544 parameter in the variable arguments of the control, to determine 
 545 what kind of instance to return. 
 546 controlType can be one of:: 
 554 These constants are also available individually, ie, you can 
 555 use either of the following:: 
 557     from wxPython.wx.lib.masked import MaskedCtrl, controlTypes 
 558     from wxPython.wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR 
 560 If not specified as a keyword argument, the default controlType is 
 566 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
 571   All methods of the Mixin that are not meant to be exposed to the external 
 572   interface are prefaced with '_'.  Those functions that are primarily 
 573   intended to be internal subroutines subsequently start with a lower-case 
 574   letter; those that are primarily intended to be used and/or overridden 
 575   by derived subclasses start with a capital letter. 
 577   The following methods must be used and/or defined when deriving a control 
 578   from MaskedEditMixin.  NOTE: if deriving from a *masked edit* control 
 579   (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary, 
 580   as it's already been done for you in the base class. 
 583                         This function must be called after the associated base 
 584                         control has been initialized in the subclass __init__ 
 585                         function.  It sets the initial value of the control, 
 586                         either to the value specified if non-empty, the 
 587                         default value if specified, or the "template" for 
 588                         the empty control as necessary.  It will also set/reset 
 589                         the font if necessary and apply formatting to the 
 590                         control at this time. 
 594                         Each class derived from MaskedEditMixin must define 
 595                         the function for getting the start and end of the 
 596                         current text selection.  The reason for this is 
 597                         that not all controls have the same function name for 
 598                         doing this; eg. wx.TextCtrl uses .GetSelection(), 
 599                         whereas we had to write a .GetMark() function for 
 600                         wxComboBox, because .GetSelection() for the control 
 601                         gets the currently selected list item from the combo 
 602                         box, and the control doesn't (yet) natively provide 
 603                         a means of determining the text selection. 
 606                         Similarly to _GetSelection, each class derived from 
 607                         MaskedEditMixin must define the function for setting 
 608                         the start and end of the current text selection. 
 609                         (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for 
 612         ._GetInsertionPoint() 
 613         ._SetInsertionPoint() 
 615                         For consistency, and because the mixin shouldn't rely 
 616                         on fixed names for any manipulations it does of any of 
 617                         the base controls, we require each class derived from 
 618                         MaskedEditMixin to define these functions as well. 
 621         ._SetValue()    REQUIRED 
 622                         Each class derived from MaskedEditMixin must define 
 623                         the functions used to get and set the raw value of the 
 625                         This is necessary so that recursion doesn't take place 
 626                         when setting the value, and so that the mixin can 
 627                         call the appropriate function after doing all its 
 628                         validation and manipulation without knowing what kind 
 629                         of base control it was mixed in with.  To handle undo 
 630                         functionality, the ._SetValue() must record the current 
 631                         selection prior to setting the value. 
 637                         Each class derived from MaskedEditMixin must redefine 
 638                         these functions to call the _Cut(), _Paste(), _Undo() 
 639                         and _SetValue() methods, respectively for the control, 
 640                         so as to prevent programmatic corruption of the control's 
 641                         value.  This must be done in each derivation, as the 
 642                         mixin cannot itself override a member of a sibling class. 
 645                         Each class derived from MaskedEditMixin must define 
 646                         the function used to refresh the base control. 
 649                         Each class derived from MaskedEditMixin must redefine 
 650                         this function so that it checks the validity of the 
 651                         control (via self._CheckValid) and then refreshes 
 652                         control using the base class method. 
 654         ._IsEditable()  REQUIRED 
 655                         Each class derived from MaskedEditMixin must define 
 656                         the function used to determine if the base control is 
 657                         editable or not.  (For masked.ComboBox, this has to 
 658                         be done with code, rather than specifying the proper 
 659                         function in the base control, as there isn't one...) 
 660         ._CalcSize()    REQUIRED 
 661                         Each class derived from MaskedEditMixin must define 
 662                         the function used to determine how wide the control 
 663                         should be given the mask.  (The mixin function 
 664                         ._calcSize() provides a baseline estimate.) 
 669   Event handlers are "chained", and MaskedEditMixin usually 
 670   swallows most of the events it sees, thereby preventing any other 
 671   handlers from firing in the chain.  It is therefore required that 
 672   each class derivation using the mixin to have an option to hook up 
 673   the event handlers itself or forego this operation and let a 
 674   subclass of the masked control do so.  For this reason, each 
 675   subclass should probably include the following code: 
 677     if setupEventHandling: 
 678         ## Setup event handlers 
 679         EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection 
 680         EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator 
 681         EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick 
 682         EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu 
 683         EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab. 
 684         EVT_CHAR( self, self._OnChar )              ## handle each keypress 
 685         EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep 
 686                                                             ## track of previous value for undo 
 688   where setupEventHandling is an argument to its constructor. 
 690   These 5 handlers must be "wired up" for the masked edit 
 691   controls to provide default behavior.  (The setupEventHandling 
 692   is an argument to masked.TextCtrl and masked.ComboBox, so 
 693   that controls derived from *them* may replace one of these 
 694   handlers if they so choose.) 
 696   If your derived control wants to preprocess events before 
 697   taking action, it should then set up the event handling itself, 
 698   so it can be first in the event handler chain. 
 701   The following routines are available to facilitate changing 
 702   the default behavior of masked edit controls: 
 704         ._SetKeycodeHandler(keycode, func) 
 705         ._SetKeyHandler(char, func) 
 706                         Use to replace default handling for any given keycode. 
 707                         func should take the key event as argument and return 
 708                         False if no further action is required to handle the 
 710                             self._SetKeycodeHandler(WXK_UP, self.IncrementValue) 
 711                             self._SetKeyHandler('-', self._OnChangeSign) 
 713         (Setting a func of None removes any keyhandler for the given key.) 
 715         "Navigation" keys are assumed to change the cursor position, and 
 716         therefore don't cause automatic motion of the cursor as insertable 
 719         ._AddNavKeycode(keycode, handler=None) 
 720         ._AddNavKey(char, handler=None) 
 721                         Allows controls to specify other keys (and optional handlers) 
 722                         to be treated as navigational characters. (eg. '.' in IpAddrCtrl) 
 724         ._GetNavKeycodes()  Returns the current list of navigational keycodes. 
 726         ._SetNavKeycodes(key_func_tuples) 
 727                         Allows replacement of the current list of keycode 
 728                         processed as navigation keys, and bind associated 
 729                         optional keyhandlers. argument is a list of key/handler 
 730                         tuples.  Passing a value of None for the handler in a 
 731                         given tuple indicates that default processing for the key 
 734         ._FindField(pos) Returns the Field object associated with this position 
 737         ._FindFieldExtent(pos, getslice=False, value=None) 
 738                         Returns edit_start, edit_end of the field corresponding 
 739                         to the specified position within the control, and 
 740                         optionally also returns the current contents of that field. 
 741                         If value is specified, it will retrieve the slice the corresponding 
 742                         slice from that value, rather than the current value of the 
 746                         This is, the function that gets called for a given position 
 747                         whenever the cursor is adjusted to leave a given field. 
 748                         By default, it adjusts the year in date fields if mask is a date, 
 749                         It can be overridden by a derived class to 
 750                         adjust the value of the control at that time. 
 751                         (eg. IpAddrCtrl reformats the address in this way.) 
 753         ._Change()      Called by internal EVT_TEXT handler. Return False to force 
 754                         skip of the normal class change event. 
 755         ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force 
 756                         skip of the normal class keypress event. 
 757         ._LostFocus()   Called by internal EVT_KILL_FOCUS handler 
 760                         This is the default EVT_KEY_DOWN routine; it just checks for 
 761                         "navigation keys", and if event.ControlDown(), it fires the 
 762                         mixin's _OnChar() routine, as such events are not always seen 
 763                         by the "cooked" EVT_CHAR routine. 
 765         ._OnChar(event) This is the main EVT_CHAR handler for the 
 768     The following routines are used to handle standard actions 
 770         _OnArrow(event)         used for arrow navigation events 
 771         _OnCtrl_A(event)        'select all' 
 772         _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive) 
 773         _OnCtrl_S(event)        'save' (does nothing) 
 774         _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste 
 775         _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection 
 776         _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any) 
 778         _OnChangeField(event)   primarily used for tab events, but can be 
 779                                 used for other keys (eg. '.' in IpAddrCtrl) 
 781         _OnErase(event)         used for backspace and delete 
 785     The following routine provides a hook back to any class derivations, so that 
 786     they can react to parameter changes before any value is set/reset as a result of 
 787     those changes.  (eg. masked.ComboBox needs to detect when the choices list is 
 788     modified, either implicitly or explicitly, so it can reset the base control 
 789     to have the appropriate choice list *before* the initial value is reset to match.) 
 791         _OnCtrlParametersChanged() 
 795     For convenience, each class derived from MaskedEditMixin should 
 796     define an accessors mixin, so that it exposes only those parameters 
 797     that make sense for the derivation.  This is done with an intermediate 
 798     level of inheritance, ie: 
 800     class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): 
 802     class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): 
 803     class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): 
 804     class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): 
 805     class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): 
 806     class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): 
 810     Each accessors mixin defines Get/Set functions for the base class parameters 
 811     that are appropriate for that derivation. 
 812     This allows the base classes to be "more generic," exposing the widest 
 813     set of options, while not requiring derived classes to be so general. 
 824 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
 825 # be a good place to implement the 2.3 logger class 
 826 from wx
.tools
.dbg 
import Logger
 
 831 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 833 ## Constants for identifying control keys and classes of keys: 
 835 WXK_CTRL_A 
= (ord('A')+1) - ord('A')   ## These keys are not already defined in wx 
 836 WXK_CTRL_C 
= (ord('C')+1) - ord('A') 
 837 WXK_CTRL_S 
= (ord('S')+1) - ord('A') 
 838 WXK_CTRL_V 
= (ord('V')+1) - ord('A') 
 839 WXK_CTRL_X 
= (ord('X')+1) - ord('A') 
 840 WXK_CTRL_Z 
= (ord('Z')+1) - ord('A') 
 843     wx
.WXK_BACK
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
, wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_TAB
, 
 844     wx
.WXK_HOME
, wx
.WXK_END
, wx
.WXK_RETURN
, wx
.WXK_PRIOR
, wx
.WXK_NEXT
 
 848     wx
.WXK_BACK
, wx
.WXK_DELETE
, wx
.WXK_INSERT
, WXK_CTRL_A
, WXK_CTRL_C
, WXK_CTRL_S
, WXK_CTRL_V
, 
 849     WXK_CTRL_X
, WXK_CTRL_Z
 
 852 # Because unicode can go over the ansi character range, we need to explicitly test 
 853 # for all non-visible keystrokes, rather than just assuming a particular range for 
 854 # visible characters: 
 855 wx_control_keycodes 
= range(32) + list(nav
) + list(control
) + [ 
 856     wx
.WXK_START
, wx
.WXK_LBUTTON
, wx
.WXK_RBUTTON
, wx
.WXK_CANCEL
, wx
.WXK_MBUTTON
, 
 857     wx
.WXK_CLEAR
, wx
.WXK_SHIFT
, wx
.WXK_CONTROL
, wx
.WXK_MENU
, wx
.WXK_PAUSE
,  
 858     wx
.WXK_CAPITAL
, wx
.WXK_SELECT
, wx
.WXK_PRINT
, wx
.WXK_EXECUTE
, wx
.WXK_SNAPSHOT
, 
 859     wx
.WXK_HELP
, wx
.WXK_NUMPAD0
, wx
.WXK_NUMPAD1
, wx
.WXK_NUMPAD2
, wx
.WXK_NUMPAD3
, 
 860     wx
.WXK_NUMPAD4
, wx
.WXK_NUMPAD5
, wx
.WXK_NUMPAD6
, wx
.WXK_NUMPAD7
, wx
.WXK_NUMPAD8
, 
 861     wx
.WXK_NUMPAD9
, wx
.WXK_MULTIPLY
, wx
.WXK_ADD
, wx
.WXK_SEPARATOR
, wx
.WXK_SUBTRACT
, 
 862     wx
.WXK_DECIMAL
, wx
.WXK_DIVIDE
, wx
.WXK_F1
, wx
.WXK_F2
, wx
.WXK_F3
, wx
.WXK_F4
,  
 863     wx
.WXK_F5
, wx
.WXK_F6
, wx
.WXK_F7
, wx
.WXK_F8
, wx
.WXK_F9
, wx
.WXK_F10
, wx
.WXK_F11
, 
 864     wx
.WXK_F12
, wx
.WXK_F13
, wx
.WXK_F14
, wx
.WXK_F15
, wx
.WXK_F16
, wx
.WXK_F17
, 
 865     wx
.WXK_F18
, wx
.WXK_F19
, wx
.WXK_F20
, wx
.WXK_F21
, wx
.WXK_F22
, wx
.WXK_F23
, 
 866     wx
.WXK_F24
, wx
.WXK_NUMLOCK
, wx
.WXK_SCROLL
, wx
.WXK_PAGEUP
, wx
.WXK_PAGEDOWN
, 
 867     wx
.WXK_NUMPAD_SPACE
, wx
.WXK_NUMPAD_TAB
, wx
.WXK_NUMPAD_ENTER
, wx
.WXK_NUMPAD_F1
, 
 868     wx
.WXK_NUMPAD_F2
, wx
.WXK_NUMPAD_F3
, wx
.WXK_NUMPAD_F4
, wx
.WXK_NUMPAD_HOME
, 
 869     wx
.WXK_NUMPAD_LEFT
, wx
.WXK_NUMPAD_UP
, wx
.WXK_NUMPAD_RIGHT
, wx
.WXK_NUMPAD_DOWN
, 
 870     wx
.WXK_NUMPAD_PRIOR
, wx
.WXK_NUMPAD_PAGEUP
, wx
.WXK_NUMPAD_NEXT
, wx
.WXK_NUMPAD_PAGEDOWN
, 
 871     wx
.WXK_NUMPAD_END
, wx
.WXK_NUMPAD_BEGIN
, wx
.WXK_NUMPAD_INSERT
, wx
.WXK_NUMPAD_DELETE
, 
 872     wx
.WXK_NUMPAD_EQUAL
, wx
.WXK_NUMPAD_MULTIPLY
, wx
.WXK_NUMPAD_ADD
, wx
.WXK_NUMPAD_SEPARATOR
, 
 873     wx
.WXK_NUMPAD_SUBTRACT
, wx
.WXK_NUMPAD_DECIMAL
, wx
.WXK_NUMPAD_DIVIDE
, wx
.WXK_WINDOWS_LEFT
, 
 874     wx
.WXK_WINDOWS_RIGHT
, wx
.WXK_WINDOWS_MENU
, wx
.WXK_COMMAND
, 
 875     # Hardware-specific buttons 
 876     wx
.WXK_SPECIAL1
, wx
.WXK_SPECIAL2
, wx
.WXK_SPECIAL3
, wx
.WXK_SPECIAL4
, wx
.WXK_SPECIAL5
, 
 877     wx
.WXK_SPECIAL6
, wx
.WXK_SPECIAL7
, wx
.WXK_SPECIAL8
, wx
.WXK_SPECIAL9
, wx
.WXK_SPECIAL10
, 
 878     wx
.WXK_SPECIAL11
, wx
.WXK_SPECIAL12
, wx
.WXK_SPECIAL13
, wx
.WXK_SPECIAL14
, wx
.WXK_SPECIAL15
, 
 879     wx
.WXK_SPECIAL16
, wx
.WXK_SPECIAL17
, wx
.WXK_SPECIAL18
, wx
.WXK_SPECIAL19
, wx
.WXK_SPECIAL20
 
 883 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 885 ## Constants for masking. This is where mask characters 
 887 ##  maskchars used to identify valid mask characters from all others 
 888 ##   # - allow numeric 0-9 only 
 889 ##   A - allow uppercase only. Combine with forceupper to force lowercase to upper 
 890 ##   a - allow lowercase only. Combine with forcelower to force upper to lowercase 
 891 ##   C - allow any letter, upper or lower 
 892 ##   X - allow string.letters, string.punctuation, string.digits 
 893 ##   & - allow string.punctuation only (doesn't include all unicode symbols) 
 894 ##   * - allow any visible character 
 896 ## Note: locale settings affect what "uppercase", lowercase, etc comprise. 
 897 ## Note: '|' is not a maskchar, in that it is a mask processing directive, and so 
 898 ## does not appear here. 
 900 maskchars 
= ("#","A","a","X","C","N",'*','&') 
 902 for i 
in xrange(32, 256): 
 905 months 
= '(01|02|03|04|05|06|07|08|09|10|11|12)' 
 906 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)' 
 907 charmonths_dict 
= {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 
 908                    'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} 
 910 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)' 
 911 hours  
= '(0\d| \d|1[012])' 
 912 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)' 
 913 minutes 
= """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ 
 914 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ 
 915 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ 
 918 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' 
 920 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(',') 
 922 state_names 
= ['Alabama','Alaska','Arizona','Arkansas', 
 923                'California','Colorado','Connecticut', 
 924                'Delaware','District of Columbia', 
 925                'Florida','Georgia','Hawaii', 
 926                'Idaho','Illinois','Indiana','Iowa', 
 927                'Kansas','Kentucky','Louisiana', 
 928                'Maine','Maryland','Massachusetts','Michigan', 
 929                'Minnesota','Mississippi','Missouri','Montana', 
 930                'Nebraska','Nevada','New Hampshire','New Jersey', 
 931                'New Mexico','New York','North Carolina','North Dakokta', 
 932                'Ohio','Oklahoma','Oregon', 
 933                'Pennsylvania','Puerto Rico','Rhode Island', 
 934                'South Carolina','South Dakota', 
 935                'Tennessee','Texas','Utah', 
 936                'Vermont','Virginia', 
 937                'Washington','West Virginia', 
 938                'Wisconsin','Wyoming'] 
 940 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 942 ## The following dictionary defines the current set of autoformats: 
 946            'mask': "(###) ###-#### x:###", 
 947            'formatcodes': 'F^->', 
 948            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 949            'description': "Phone Number w/opt. ext" 
 952            'mask': "###-###-#### x:###", 
 953            'formatcodes': 'F^->', 
 954            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 955            'description': "Phone Number\n (w/hyphens and opt. ext)" 
 958            'mask': "(###) ###-####", 
 959            'formatcodes': 'F^->', 
 960            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 961            'description': "Phone Number only" 
 964            'mask': "###-###-####", 
 965            'formatcodes': 'F^->', 
 966            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 967            'description': "Phone Number\n(w/hyphens)" 
 971            'formatcodes': 'F!V', 
 972            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(states
,'|'), 
 974            'choiceRequired': True, 
 975            'description': "US State Code" 
 978            'mask': "ACCCCCCCCCCCCCCCCCCC", 
 980            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(state_names
,'|'), 
 981            'choices': state_names
, 
 982            'choiceRequired': True, 
 983            'description': "US State Name" 
 986        "USDATETIMEMMDDYYYY/HHMMSS": { 
 987            'mask': "##/##/#### ##:##:## AM", 
 988            'excludeChars': am_pm_exclude
, 
 989            'formatcodes': 'DF!', 
 990            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 991            'description': "US Date + Time" 
 993        "USDATETIMEMMDDYYYY-HHMMSS": { 
 994            'mask': "##-##-#### ##:##:## AM", 
 995            'excludeChars': am_pm_exclude
, 
 996            'formatcodes': 'DF!', 
 997            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 998            'description': "US Date + Time\n(w/hypens)" 
1000        "USDATE24HRTIMEMMDDYYYY/HHMMSS": { 
1001            'mask': "##/##/#### ##:##:##", 
1002            'formatcodes': 'DF', 
1003            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1004            'description': "US Date + 24Hr (Military) Time" 
1006        "USDATE24HRTIMEMMDDYYYY-HHMMSS": { 
1007            'mask': "##-##-#### ##:##:##", 
1008            'formatcodes': 'DF', 
1009            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1010            'description': "US Date + 24Hr Time\n(w/hypens)" 
1012        "USDATETIMEMMDDYYYY/HHMM": { 
1013            'mask': "##/##/#### ##:## AM", 
1014            'excludeChars': am_pm_exclude
, 
1015            'formatcodes': 'DF!', 
1016            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1017            'description': "US Date + Time\n(without seconds)" 
1019        "USDATE24HRTIMEMMDDYYYY/HHMM": { 
1020            'mask': "##/##/#### ##:##", 
1021            'formatcodes': 'DF', 
1022            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1023            'description': "US Date + 24Hr Time\n(without seconds)" 
1025        "USDATETIMEMMDDYYYY-HHMM": { 
1026            'mask': "##-##-#### ##:## AM", 
1027            'excludeChars': am_pm_exclude
, 
1028            'formatcodes': 'DF!', 
1029            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1030            'description': "US Date + Time\n(w/hypens and w/o secs)" 
1032        "USDATE24HRTIMEMMDDYYYY-HHMM": { 
1033            'mask': "##-##-#### ##:##", 
1034            'formatcodes': 'DF', 
1035            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1036            'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" 
1038        "USDATEMMDDYYYY/": { 
1039            'mask': "##/##/####", 
1040            'formatcodes': 'DF', 
1041            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4}', 
1042            'description': "US Date\n(MMDDYYYY)" 
1046            'formatcodes': 'DF', 
1047            'validRegex': '^' + months 
+ '/' + days 
+ '/\d\d', 
1048            'description': "US Date\n(MMDDYY)" 
1050        "USDATEMMDDYYYY-": { 
1051            'mask': "##-##-####", 
1052            'formatcodes': 'DF', 
1053            'validRegex': '^' + months 
+ '-' + days 
+ '-' +'\d{4}', 
1054            'description': "MM-DD-YYYY" 
1057        "EUDATEYYYYMMDD/": { 
1058            'mask': "####/##/##", 
1059            'formatcodes': 'DF', 
1060            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days
, 
1061            'description': "YYYY/MM/DD" 
1063        "EUDATEYYYYMMDD.": { 
1064            'mask': "####.##.##", 
1065            'formatcodes': 'DF', 
1066            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days
, 
1067            'description': "YYYY.MM.DD" 
1069        "EUDATEDDMMYYYY/": { 
1070            'mask': "##/##/####", 
1071            'formatcodes': 'DF', 
1072            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4}', 
1073            'description': "DD/MM/YYYY" 
1075        "EUDATEDDMMYYYY.": { 
1076            'mask': "##.##.####", 
1077            'formatcodes': 'DF', 
1078            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4}', 
1079            'description': "DD.MM.YYYY" 
1081        "EUDATEDDMMMYYYY.": { 
1082            'mask': "##.CCC.####", 
1083            'formatcodes': 'DF', 
1084            'validRegex': '^' + days 
+ '.' + charmonths 
+ '.' + '\d{4}', 
1085            'description': "DD.Month.YYYY" 
1087        "EUDATEDDMMMYYYY/": { 
1088            'mask': "##/CCC/####", 
1089            'formatcodes': 'DF', 
1090            'validRegex': '^' + days 
+ '/' + charmonths 
+ '/' + '\d{4}', 
1091            'description': "DD/Month/YYYY" 
1094        "EUDATETIMEYYYYMMDD/HHMMSS": { 
1095            'mask': "####/##/## ##:##:## AM", 
1096            'excludeChars': am_pm_exclude
, 
1097            'formatcodes': 'DF!', 
1098            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1099            'description': "YYYY/MM/DD HH:MM:SS" 
1101        "EUDATETIMEYYYYMMDD.HHMMSS": { 
1102            'mask': "####.##.## ##:##:## AM", 
1103            'excludeChars': am_pm_exclude
, 
1104            'formatcodes': 'DF!', 
1105            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1106            'description': "YYYY.MM.DD HH:MM:SS" 
1108        "EUDATETIMEDDMMYYYY/HHMMSS": { 
1109            'mask': "##/##/#### ##:##:## AM", 
1110            'excludeChars': am_pm_exclude
, 
1111            'formatcodes': 'DF!', 
1112            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1113            'description': "DD/MM/YYYY HH:MM:SS" 
1115        "EUDATETIMEDDMMYYYY.HHMMSS": { 
1116            'mask': "##.##.#### ##:##:## AM", 
1117            'excludeChars': am_pm_exclude
, 
1118            'formatcodes': 'DF!', 
1119            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1120            'description': "DD.MM.YYYY HH:MM:SS" 
1123        "EUDATETIMEYYYYMMDD/HHMM": { 
1124            'mask': "####/##/## ##:## AM", 
1125            'excludeChars': am_pm_exclude
, 
1126            'formatcodes': 'DF!', 
1127            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1128            'description': "YYYY/MM/DD HH:MM" 
1130        "EUDATETIMEYYYYMMDD.HHMM": { 
1131            'mask': "####.##.## ##:## AM", 
1132            'excludeChars': am_pm_exclude
, 
1133            'formatcodes': 'DF!', 
1134            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1135            'description': "YYYY.MM.DD HH:MM" 
1137        "EUDATETIMEDDMMYYYY/HHMM": { 
1138            'mask': "##/##/#### ##:## AM", 
1139            'excludeChars': am_pm_exclude
, 
1140            'formatcodes': 'DF!', 
1141            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1142            'description': "DD/MM/YYYY HH:MM" 
1144        "EUDATETIMEDDMMYYYY.HHMM": { 
1145            'mask': "##.##.#### ##:## AM", 
1146            'excludeChars': am_pm_exclude
, 
1147            'formatcodes': 'DF!', 
1148            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1149            'description': "DD.MM.YYYY HH:MM" 
1152        "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { 
1153            'mask': "####/##/## ##:##:##", 
1154            'formatcodes': 'DF', 
1155            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1156            'description': "YYYY/MM/DD 24Hr Time" 
1158        "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { 
1159            'mask': "####.##.## ##:##:##", 
1160            'formatcodes': 'DF', 
1161            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1162            'description': "YYYY.MM.DD 24Hr Time" 
1164        "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { 
1165            'mask': "##/##/#### ##:##:##", 
1166            'formatcodes': 'DF', 
1167            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1168            'description': "DD/MM/YYYY 24Hr Time" 
1170        "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { 
1171            'mask': "##.##.#### ##:##:##", 
1172            'formatcodes': 'DF', 
1173            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1174            'description': "DD.MM.YYYY 24Hr Time" 
1176        "EUDATE24HRTIMEYYYYMMDD/HHMM": { 
1177            'mask': "####/##/## ##:##", 
1178            'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1179            'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" 
1181        "EUDATE24HRTIMEYYYYMMDD.HHMM": { 
1182            'mask': "####.##.## ##:##", 
1183            'formatcodes': 'DF', 
1184            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1185            'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" 
1187        "EUDATE24HRTIMEDDMMYYYY/HHMM": { 
1188            'mask': "##/##/#### ##:##", 
1189            'formatcodes': 'DF', 
1190            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1191            'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" 
1193        "EUDATE24HRTIMEDDMMYYYY.HHMM": { 
1194            'mask': "##.##.#### ##:##", 
1195            'formatcodes': 'DF', 
1196            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1197            'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" 
1201            'mask': "##:##:## AM", 
1202            'excludeChars': am_pm_exclude
, 
1203            'formatcodes': 'TF!', 
1204            'validRegex': '^' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1205            'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" 
1209            'excludeChars': am_pm_exclude
, 
1210            'formatcodes': 'TF!', 
1211            'validRegex': '^' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1212            'description': "HH:MM (A|P)M\n(see TimeCtrl)" 
1216            'formatcodes': 'TF', 
1217            'validRegex': '^' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1218            'description': "24Hr HH:MM:SS\n(see TimeCtrl)" 
1222            'formatcodes': 'TF', 
1223            'validRegex': '^' + milhours 
+ ':' + minutes
, 
1224            'description': "24Hr HH:MM\n(see TimeCtrl)" 
1227            'mask': "###-##-####", 
1229            'validRegex': "\d{3}-\d{2}-\d{4}", 
1230            'description': "Social Sec#" 
1233            'mask': "####-####-####-####", 
1235            'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", 
1236            'description': "Credit Card" 
1241            'validRegex': "^" + months 
+ "/\d\d", 
1242            'description': "Expiration MM/YY" 
1247            'validRegex': "^\d{5}", 
1248            'description': "US 5-digit zip code" 
1251            'mask': "#####-####", 
1253            'validRegex': "\d{5}-(\s{4}|\d{4})", 
1254            'description': "US zip+4 code" 
1259            'validRegex': "^0.\d\d", 
1260            'description': "Percentage" 
1265            'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]", 
1266            'description': "Age" 
1269            'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 
1270            'excludeChars': " \\/*&%$#!+='\"", 
1271            'formatcodes': "F>", 
1272            '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}\]) *$", 
1273            'description': "Email address" 
1276            'mask': "###.###.###.###", 
1277            'formatcodes': 'F_Sr', 
1278            '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}", 
1279            'description': "IP Address\n(see IpAddrCtrl)" 
1283 # build demo-friendly dictionary of descriptions of autoformats 
1285 for key
, value 
in masktags
.items(): 
1286     autoformats
.append((key
, value
['description'])) 
1289 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1293     This class manages the individual fields in a masked edit control. 
1294     Each field has a zero-based index, indicating its position in the 
1295     control, an extent, an associated mask, and a plethora of optional 
1296     parameters.  Fields can be instantiated and then associated with 
1297     parent masked controls, in order to provide field-specific configuration. 
1298     Alternatively, fields will be implicitly created by the parent control 
1299     if not provided at construction, at which point, the fields can then 
1300     manipulated by the controls .SetFieldParameters() method. 
1303               'index': None,                    ## which field of mask; set by parent control. 
1304               'mask': "",                       ## mask chars for this field 
1305               'extent': (),                     ## (edit start, edit_end) of field; set by parent control. 
1306               'formatcodes':  "",               ## codes indicating formatting options for the control 
1307               'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given 
1308               'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits 
1309               'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction 
1310               'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard 
1311               'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. 
1312               'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar 
1313               'excludeChars': "",               ## optional string of chars to exclude even if main mask type does 
1314               'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't 
1315               'validRegex':   "",               ## optional regular expression to use to validate the control 
1316               'validRange':   (),               ## Optional hi-low range for numerics 
1317               'choices':    [],                 ## Optional list for character expressions 
1318               'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list 
1319               'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search 
1320               'autoSelect': False,              ## Set to True to try auto-completion on each keystroke: 
1321               'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl 
1322               'validRequired': False,           ## Set to True to disallow input that results in an invalid value 
1323               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID 
1324               'description': "",                ## primarily for autoformats, but could be useful elsewhere 
1325               'raiseOnInvalidPaste': False,     ## if True, paste into field will cause ValueError 
1328     # This list contains all parameters that when set at the control level should 
1329     # propagate down to each field: 
1330     propagating_params 
= ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', 
1331                           'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste') 
1333     def __init__(self
, **kwargs
): 
1335         This is the "constructor" for setting up parameters for fields. 
1336         a field_index of -1 is used to indicate "the entire control." 
1338 ####        dbg('Field::Field', indent=1) 
1339         # Validate legitimate set of parameters: 
1340         for key 
in kwargs
.keys(): 
1341             if key 
not in Field
.valid_params
.keys(): 
1343                 ae 
= AttributeError('invalid parameter "%s"' % (key
)) 
1347         # Set defaults for each parameter for this instance, and fully 
1348         # populate initial parameter list for configuration: 
1349         for key
, value 
in Field
.valid_params
.items(): 
1350             setattr(self
, '_' + key
, copy
.copy(value
)) 
1351             if not kwargs
.has_key(key
): 
1352                 kwargs
[key
] = copy
.copy(value
) 
1354         self
._autoCompleteIndex 
= -1 
1355         self
._SetParameters
(**kwargs
) 
1356         self
._ValidateParameters
(**kwargs
) 
1361     def _SetParameters(self
, **kwargs
): 
1363         This function can be used to set individual or multiple parameters for 
1364         a masked edit field parameter after construction. 
1367 ##        dbg('maskededit.Field::_SetParameters', indent=1) 
1368         # Validate keyword arguments: 
1369         for key 
in kwargs
.keys(): 
1370             if key 
not in Field
.valid_params
.keys(): 
1371 ##                dbg(indent=0, suspend=0) 
1372                 ae 
= AttributeError('invalid keyword argument "%s"' % key
) 
1376 ##        if self._index is not None: dbg('field index:', self._index) 
1377 ##        dbg('parameters:', indent=1) 
1378         for key
, value 
in kwargs
.items(): 
1379 ##            dbg('%s:' % key, value) 
1384         old_fillChar 
= self
._fillChar   
# store so we can change choice lists accordingly if it changes 
1386         # First, Assign all parameters specified: 
1387         for key 
in Field
.valid_params
.keys(): 
1388             if kwargs
.has_key(key
): 
1389                 setattr(self
, '_' + key
, kwargs
[key
] ) 
1391         if kwargs
.has_key('formatcodes'):   # (set/changed) 
1392             self
._forceupper  
= '!' in self
._formatcodes
 
1393             self
._forcelower  
= '^' in self
._formatcodes
 
1394             self
._groupdigits 
= ',' in self
._formatcodes
 
1395             self
._okSpaces    
= '_' in self
._formatcodes
 
1396             self
._padZero     
= '0' in self
._formatcodes
 
1397             self
._autofit     
= 'F' in self
._formatcodes
 
1398             self
._insertRight 
= 'r' in self
._formatcodes
 
1399             self
._allowInsert 
= '>' in self
._formatcodes
 
1400             self
._alignRight  
= 'R' in self
._formatcodes 
or 'r' in self
._formatcodes
 
1401             self
._moveOnFieldFull 
= not '<' in self
._formatcodes
 
1402             self
._selectOnFieldEntry 
= 'S' in self
._formatcodes
 
1404             if kwargs
.has_key('groupChar'): 
1405                 self
._groupChar 
= kwargs
['groupChar'] 
1406             if kwargs
.has_key('decimalChar'): 
1407                 self
._decimalChar 
= kwargs
['decimalChar'] 
1408             if kwargs
.has_key('shiftDecimalChar'): 
1409                 self
._shiftDecimalChar 
= kwargs
['shiftDecimalChar'] 
1411         if kwargs
.has_key('formatcodes') or kwargs
.has_key('validRegex'): 
1412             self
._regexMask   
= 'V' in self
._formatcodes 
and self
._validRegex
 
1414         if kwargs
.has_key('fillChar'): 
1415             self
._old
_fillChar 
= old_fillChar
 
1416 ####            dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1418         if kwargs
.has_key('mask') or kwargs
.has_key('validRegex'):  # (set/changed) 
1419             self
._isInt 
= _isInteger(self
._mask
) 
1420 ##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) 
1422 ##        dbg(indent=0, suspend=0) 
1425     def _ValidateParameters(self
, **kwargs
): 
1427         This function can be used to validate individual or multiple parameters for 
1428         a masked edit field parameter after construction. 
1431 ##        dbg('maskededit.Field::_ValidateParameters', indent=1) 
1432 ##        if self._index is not None: dbg('field index:', self._index) 
1433 ####        dbg('parameters:', indent=1) 
1434 ##        for key, value in kwargs.items(): 
1435 ####            dbg('%s:' % key, value) 
1437 ####        dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1439         # Verify proper numeric format params: 
1440         if self
._groupdigits 
and self
._groupChar 
== self
._decimalChar
: 
1441 ##            dbg(indent=0, suspend=0) 
1442             ae 
= AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self
._groupChar
, self
._decimalChar
)) 
1443             ae
.attribute 
= self
._groupChar
 
1447         # Now go do validation, semantic and inter-dependency parameter processing: 
1448         if kwargs
.has_key('choices') or kwargs
.has_key('compareNoCase') or kwargs
.has_key('choiceRequired'): # (set/changed) 
1450             self
._compareChoices 
= [choice
.strip() for choice 
in self
._choices
] 
1452             if self
._compareNoCase 
and self
._choices
: 
1453                 self
._compareChoices 
= [item
.lower() for item 
in self
._compareChoices
] 
1455             if kwargs
.has_key('choices'): 
1456                 self
._autoCompleteIndex 
= -1 
1459         if kwargs
.has_key('validRegex'):    # (set/changed) 
1460             if self
._validRegex
: 
1462                     if self
._compareNoCase
: 
1463                         self
._filter 
= re
.compile(self
._validRegex
, re
.IGNORECASE
) 
1465                         self
._filter 
= re
.compile(self
._validRegex
) 
1467 ##                    dbg(indent=0, suspend=0) 
1468                     raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self
._index
), self
._validRegex
)) 
1472         if kwargs
.has_key('validRange'):    # (set/changed) 
1473             self
._hasRange  
= False 
1476             if self
._validRange
: 
1477                 if type(self
._validRange
) != types
.TupleType 
or len( self
._validRange 
)!= 2 or self
._validRange
[0] > self
._validRange
[1]: 
1478 ##                    dbg(indent=0, suspend=0) 
1479                     raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' 
1480                                     % (str(self
._index
), repr(self
._validRange
)) ) 
1482                 self
._hasRange  
= True 
1483                 self
._rangeLow  
= self
._validRange
[0] 
1484                 self
._rangeHigh 
= self
._validRange
[1] 
1486         if kwargs
.has_key('choices') or (len(self
._choices
) and len(self
._choices
[0]) != len(self
._mask
)):       # (set/changed) 
1487             self
._hasList   
= False 
1488             if self
._choices 
and type(self
._choices
) not in (types
.TupleType
, types
.ListType
): 
1489 ##                dbg(indent=0, suspend=0) 
1490                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1491             elif len( self
._choices
) > 0: 
1492                 for choice 
in self
._choices
: 
1493                     if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
1494 ##                        dbg(indent=0, suspend=0) 
1495                         raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1497                 length 
= len(self
._mask
) 
1498 ##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) 
1499                 if len(self
._choices
) and length
: 
1500                     if len(self
._choices
[0]) > length
: 
1501                         # changed mask without respecifying choices; readjust the width as appropriate: 
1502                         self
._choices 
= [choice
.strip() for choice 
in self
._choices
] 
1503                     if self
._alignRight
: 
1504                         self
._choices 
= [choice
.rjust( length 
) for choice 
in self
._choices
] 
1506                         self
._choices 
= [choice
.ljust( length 
) for choice 
in self
._choices
] 
1507 ##                    dbg('aligned choices:', self._choices) 
1509                 if hasattr(self
, '_template'): 
1510                     # Verify each choice specified is valid: 
1511                     for choice 
in self
._choices
: 
1512                         if self
.IsEmpty(choice
) and not self
._validRequired
: 
1513                             # allow empty values even if invalid, (just colored differently) 
1515                         if not self
.IsValid(choice
): 
1516 ##                            dbg(indent=0, suspend=0) 
1517                             ve 
= ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
1520                 self
._hasList 
= True 
1522 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) 
1523 ####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) 
1524         if kwargs
.has_key('fillChar') and len(self
._choices
) > 0: 
1525             if kwargs
['fillChar'] != ' ': 
1526                 self
._choices 
= [choice
.replace(' ', self
._fillChar
) for choice 
in self
._choices
] 
1528                 self
._choices 
= [choice
.replace(self
._old
_fillChar
, self
._fillChar
) for choice 
in self
._choices
] 
1529 ##            dbg('updated choices:', self._choices) 
1532         if kwargs
.has_key('autoSelect') and kwargs
['autoSelect']: 
1533             if not self
._hasList
: 
1534 ##                dbg('no list to auto complete; ignoring "autoSelect=True"') 
1535                 self
._autoSelect 
= False 
1537         # reset field validity assumption: 
1539 ##        dbg(indent=0, suspend=0) 
1542     def _GetParameter(self
, paramname
): 
1544         Routine for retrieving the value of any given parameter 
1546         if Field
.valid_params
.has_key(paramname
): 
1547             return getattr(self
, '_' + paramname
) 
1549             TypeError('Field._GetParameter: invalid parameter "%s"' % key
) 
1552     def IsEmpty(self
, slice): 
1554         Indicates whether the specified slice is considered empty for the 
1557 ##        dbg('Field::IsEmpty("%s")' % slice, indent=1) 
1558         if not hasattr(self
, '_template'): 
1560             raise AttributeError('_template') 
1562 ##        dbg('self._template: "%s"' % self._template) 
1563 ##        dbg('self._defaultValue: "%s"' % str(self._defaultValue)) 
1564         if slice == self
._template 
and not self
._defaultValue
: 
1568         elif slice == self
._template
: 
1570             for pos 
in range(len(self
._template
)): 
1571 ####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) 
1572                 if slice[pos
] not in (' ', self
._fillChar
): 
1575 ##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) 
1578 ##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) 
1582     def IsValid(self
, slice): 
1584         Indicates whether the specified slice is considered a valid value for the 
1588 ##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) 
1589         valid 
= True    # assume true to start 
1591         if self
.IsEmpty(slice): 
1592 ##            dbg(indent=0, suspend=0) 
1593             if self
._emptyInvalid
: 
1598         elif self
._hasList 
and self
._choiceRequired
: 
1599 ##            dbg("(member of list required)") 
1600             # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): 
1601             if self
._fillChar 
!= ' ': 
1602                 slice = slice.replace(self
._fillChar
, ' ') 
1603 ##                dbg('updated slice:"%s"' % slice) 
1604             compareStr 
= slice.strip() 
1606             if self
._compareNoCase
: 
1607                 compareStr 
= compareStr
.lower() 
1608             valid 
= compareStr 
in self
._compareChoices
 
1610         elif self
._hasRange 
and not self
.IsEmpty(slice): 
1611 ##            dbg('validating against range') 
1613                 # allow float as well as int ranges (int comparisons for free.) 
1614                 valid 
= self
._rangeLow 
<= float(slice) <= self
._rangeHigh
 
1618         elif self
._validRegex 
and self
._filter
: 
1619 ##            dbg('validating against regex') 
1620             valid 
= (re
.match( self
._filter
, slice) is not None) 
1622         if valid 
and self
._validFunc
: 
1623 ##            dbg('validating against supplied function') 
1624             valid 
= self
._validFunc
(slice) 
1625 ##        dbg('valid?', valid, indent=0, suspend=0) 
1629     def _AdjustField(self
, slice): 
1630         """ 'Fixes' an integer field. Right or left-justifies, as required.""" 
1631 ##        dbg('Field::_AdjustField("%s")' % slice, indent=1) 
1632         length 
= len(self
._mask
) 
1633 ####        dbg('length(self._mask):', length) 
1634 ####        dbg('self._useParensForNegatives?', self._useParensForNegatives) 
1636             if self
._useParensForNegatives
: 
1637                 signpos 
= slice.find('(') 
1638                 right_signpos 
= slice.find(')') 
1639                 intStr 
= slice.replace('(', '').replace(')', '')    # drop sign, if any 
1641                 signpos 
= slice.find('-') 
1642                 intStr 
= slice.replace( '-', '' )                   # drop sign, if any 
1645             intStr 
= intStr
.replace(' ', '')                        # drop extra spaces 
1646             intStr 
= string
.replace(intStr
,self
._fillChar
,"")       # drop extra fillchars 
1647             intStr 
= string
.replace(intStr
,"-","")                  # drop sign, if any 
1648             intStr 
= string
.replace(intStr
, self
._groupChar
, "")    # lose commas/dots 
1649 ####            dbg('intStr:"%s"' % intStr) 
1650             start
, end 
= self
._extent
 
1651             field_len 
= end 
- start
 
1652             if not self
._padZero 
and len(intStr
) != field_len 
and intStr
.strip(): 
1653                 intStr 
= str(long(intStr
)) 
1654 ####            dbg('raw int str: "%s"' % intStr) 
1655 ####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) 
1656             if self
._groupdigits
: 
1659                 for i 
in range(len(intStr
)-1, -1, -1): 
1660                     new 
= intStr
[i
] + new
 
1662                         new 
= self
._groupChar 
+ new
 
1664                 if new 
and new
[0] == self
._groupChar
: 
1666                 if len(new
) <= length
: 
1667                     # expanded string will still fit and leave room for sign: 
1669                 # else... leave it without the commas... 
1671 ##            dbg('padzero?', self._padZero) 
1672 ##            dbg('len(intStr):', len(intStr), 'field length:', length) 
1673             if self
._padZero 
and len(intStr
) < length
: 
1674                 intStr 
= '0' * (length 
- len(intStr
)) + intStr
 
1675                 if signpos 
!= -1:   # we had a sign before; restore it 
1676                     if self
._useParensForNegatives
: 
1677                         intStr 
= '(' + intStr
[1:] 
1678                         if right_signpos 
!= -1: 
1681                         intStr 
= '-' + intStr
[1:] 
1682             elif signpos 
!= -1 and slice[0:signpos
].strip() == '':    # - was before digits 
1683                 if self
._useParensForNegatives
: 
1684                     intStr 
= '(' + intStr
 
1685                     if right_signpos 
!= -1: 
1688                     intStr 
= '-' + intStr
 
1689             elif right_signpos 
!= -1: 
1690                 # must have had ')' but '(' was before field; re-add ')' 
1694         slice = slice.strip() # drop extra spaces 
1696         if self
._alignRight
:     ## Only if right-alignment is enabled 
1697             slice = slice.rjust( length 
) 
1699             slice = slice.ljust( length 
) 
1700         if self
._fillChar 
!= ' ': 
1701             slice = slice.replace(' ', self
._fillChar
) 
1702 ##        dbg('adjusted slice: "%s"' % slice, indent=0) 
1706 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1708 class MaskedEditMixin
: 
1710     This class allows us to abstract the masked edit functionality that could 
1711     be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.) 
1712     It forms the basis for all of the lib.masked controls. 
1714     valid_ctrl_params 
= { 
1715               'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control 
1716               'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary 
1717               'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask 
1718               'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year 
1719               'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete 
1720               'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control 
1721               'defaultEncoding': 'latin1',      ## optional argument to indicate unicode codec to use (unicode ctrls only) 
1722               'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently, 
1723                                                 ## from field validation constraints 
1724               'emptyBackgroundColour': "White", 
1725               'validBackgroundColour': "White", 
1726               'invalidBackgroundColour': "Yellow", 
1727               'foregroundColour': "Black", 
1728               'signedForegroundColour': "Red", 
1732     def __init__(self
, name 
= 'MaskedEdit', **kwargs
): 
1734         This is the "constructor" for setting up the mixin variable parameters for the composite class. 
1739         # set up flag for doing optional things to base control if possible 
1740         if not hasattr(self
, 'controlInitialized'): 
1741             self
.controlInitialized 
= False 
1743         # Set internal state var for keeping track of whether or not a character 
1744         # action results in a modification of the control, since .SetValue() 
1745         # doesn't modify the base control's internal state: 
1746         self
.modified 
= False 
1747         self
._previous
_mask 
= None 
1749         # Validate legitimate set of parameters: 
1750         for key 
in kwargs
.keys(): 
1751             if key
.replace('Color', 'Colour') not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1752                 raise TypeError('%s: invalid parameter "%s"' % (name
, key
)) 
1754         ## Set up dictionary that can be used by subclasses to override or add to default 
1755         ## behavior for individual characters.  Derived subclasses needing to change 
1756         ## default behavior for keys can either redefine the default functions for the 
1757         ## common keys or add functions for specific keys to this list.  Each function 
1758         ## added should take the key event as argument, and return False if the key 
1759         ## requires no further processing. 
1761         ## Initially populated with navigation and function control keys: 
1762         self
._keyhandlers 
= { 
1763             # default navigation keys and handlers: 
1764             wx
.WXK_BACK
:   self
._OnErase
, 
1765             wx
.WXK_LEFT
:   self
._OnArrow
, 
1766             wx
.WXK_RIGHT
:  self
._OnArrow
, 
1767             wx
.WXK_UP
:     self
._OnAutoCompleteField
, 
1768             wx
.WXK_DOWN
:   self
._OnAutoCompleteField
, 
1769             wx
.WXK_TAB
:    self
._OnChangeField
, 
1770             wx
.WXK_HOME
:   self
._OnHome
, 
1771             wx
.WXK_END
:    self
._OnEnd
, 
1772             wx
.WXK_RETURN
: self
._OnReturn
, 
1773             wx
.WXK_PRIOR
:  self
._OnAutoCompleteField
, 
1774             wx
.WXK_NEXT
:   self
._OnAutoCompleteField
, 
1776             # default function control keys and handlers: 
1777             wx
.WXK_DELETE
: self
._OnDelete
, 
1778             wx
.WXK_INSERT
: self
._OnInsert
, 
1779             WXK_CTRL_A
: self
._OnCtrl
_A
, 
1780             WXK_CTRL_C
: self
._OnCtrl
_C
, 
1781             WXK_CTRL_S
: self
._OnCtrl
_S
, 
1782             WXK_CTRL_V
: self
._OnCtrl
_V
, 
1783             WXK_CTRL_X
: self
._OnCtrl
_X
, 
1784             WXK_CTRL_Z
: self
._OnCtrl
_Z
, 
1787         ## bind standard navigational and control keycodes to this instance, 
1788         ## so that they can be augmented and/or changed in derived classes: 
1789         self
._nav 
= list(nav
) 
1790         self
._control 
= list(control
) 
1792         ## Dynamically evaluate and store string constants for mask chars 
1793         ## so that locale settings can be made after this module is imported 
1794         ## and the controls created after that is done can allow the 
1795         ## appropriate characters: 
1796         self
.maskchardict  
= { 
1798             'A': string
.uppercase
, 
1799             'a': string
.lowercase
, 
1800             'X': string
.letters 
+ string
.punctuation 
+ string
.digits
, 
1801             'C': string
.letters
, 
1802             'N': string
.letters 
+ string
.digits
, 
1803             '&': string
.punctuation
, 
1804             '*': ansichars  
# to give it a value, but now allows any non-wxcontrol character 
1807         ## self._ignoreChange is used by MaskedComboBox, because 
1808         ## of the hack necessary to determine the selection; it causes 
1809         ## EVT_TEXT messages from the combobox to be ignored if set. 
1810         self
._ignoreChange 
= False 
1812         # These are used to keep track of previous value, for undo functionality: 
1813         self
._curValue  
= None 
1814         self
._prevValue 
= None 
1818         # Set defaults for each parameter for this instance, and fully 
1819         # populate initial parameter list for configuration: 
1820         for key
, value 
in MaskedEditMixin
.valid_ctrl_params
.items(): 
1821             setattr(self
, '_' + key
, copy
.copy(value
)) 
1822             if not kwargs
.has_key(key
): 
1823 ####                dbg('%s: "%s"' % (key, repr(value))) 
1824                 kwargs
[key
] = copy
.copy(value
) 
1826         # Create a "field" that holds global parameters for control constraints 
1827         self
._ctrl
_constraints 
= self
._fields
[-1] = Field(index
=-1) 
1828         self
.SetCtrlParameters(**kwargs
) 
1832     def SetCtrlParameters(self
, **kwargs
): 
1834         This public function can be used to set individual or multiple masked edit 
1835         parameters after construction.  (See maskededit module overview for the list 
1836         of valid parameters.) 
1839 ##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1) 
1840 ####        dbg('kwargs:', indent=1) 
1841 ##        for key, value in kwargs.items(): 
1842 ####            dbg(key, '=', value) 
1845         # Validate keyword arguments: 
1846         constraint_kwargs 
= {} 
1848         for key
, value 
in kwargs
.items(): 
1849             key 
= key
.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling 
1850             if key 
not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1851 ##                dbg(indent=0, suspend=0) 
1852                 ae 
= AttributeError('Invalid keyword argument "%s" for control "%s"' % (key
, self
.name
)) 
1855             elif key 
in Field
.valid_params
.keys(): 
1856                 constraint_kwargs
[key
] = value
 
1858                 ctrl_kwargs
[key
] = value
 
1863         if ctrl_kwargs
.has_key('autoformat'): 
1864             autoformat 
= ctrl_kwargs
['autoformat'] 
1868         # handle "parochial name" backward compatibility: 
1869         if autoformat 
and autoformat
.find('MILTIME') != -1 and autoformat 
not in masktags
.keys(): 
1870             autoformat 
= autoformat
.replace('MILTIME', '24HRTIME') 
1872         if autoformat 
!= self
._autoformat 
and autoformat 
in masktags
.keys(): 
1873 ##            dbg('autoformat:', autoformat) 
1874             self
._autoformat                  
= autoformat
 
1875             mask                              
= masktags
[self
._autoformat
]['mask'] 
1876             # gather rest of any autoformat parameters: 
1877             for param
, value 
in masktags
[self
._autoformat
].items(): 
1878                 if param 
== 'mask': continue    # (must be present; already accounted for) 
1879                 constraint_kwargs
[param
] = value
 
1881         elif autoformat 
and not autoformat 
in masktags
.keys(): 
1882             ae 
= AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat
)) 
1883             ae
.attribute 
= autoformat
 
1886 ##            dbg('autoformat not selected') 
1887             if kwargs
.has_key('mask'): 
1888                 mask 
= kwargs
['mask'] 
1889 ##                dbg('mask:', mask) 
1891         ## Assign style flags 
1893 ##            dbg('preserving previous mask') 
1894             mask 
= self
._previous
_mask   
# preserve previous mask 
1896 ##            dbg('mask (re)set') 
1897             reset_args
['reset_mask'] = mask
 
1898             constraint_kwargs
['mask'] = mask
 
1900             # wipe out previous fields; preserve new control-level constraints 
1901             self
._fields 
= {-1: self._ctrl_constraints}
 
1904         if ctrl_kwargs
.has_key('fields'): 
1905             # do field parameter type validation, and conversion to internal dictionary 
1907             fields 
= ctrl_kwargs
['fields'] 
1908             if type(fields
) in (types
.ListType
, types
.TupleType
): 
1909                 for i 
in range(len(fields
)): 
1911                     if not isinstance(field
, Field
): 
1912 ##                        dbg(indent=0, suspend=0) 
1913                         raise TypeError('invalid type for field parameter: %s' % repr(field
)) 
1914                     self
._fields
[i
] = field
 
1916             elif type(fields
) == types
.DictionaryType
: 
1917                 for index
, field 
in fields
.items(): 
1918                     if not isinstance(field
, Field
): 
1919 ##                        dbg(indent=0, suspend=0) 
1920                         raise TypeError('invalid type for field parameter: %s' % repr(field
)) 
1921                     self
._fields
[index
] = field
 
1923 ##                dbg(indent=0, suspend=0) 
1924                 raise TypeError('fields parameter must be a list or dictionary; not %s' % repr(fields
)) 
1926         # Assign constraint parameters for entire control: 
1927 ####        dbg('control constraints:', indent=1) 
1928 ##        for key, value in constraint_kwargs.items(): 
1929 ####            dbg('%s:' % key, value) 
1932         # determine if changing parameters that should affect the entire control: 
1933         for key 
in MaskedEditMixin
.valid_ctrl_params
.keys(): 
1934             if key 
in ( 'mask', 'fields' ): continue    # (processed separately) 
1935             if ctrl_kwargs
.has_key(key
): 
1936                 setattr(self
, '_' + key
, ctrl_kwargs
[key
]) 
1938         # Validate color parameters, converting strings to named colors and validating 
1939         # result if appropriate: 
1940         for key 
in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', 
1941                     'foregroundColour', 'signedForegroundColour'): 
1942             if ctrl_kwargs
.has_key(key
): 
1943                 if type(ctrl_kwargs
[key
]) in (types
.StringType
, types
.UnicodeType
): 
1944                     c 
= wx
.NamedColour(ctrl_kwargs
[key
]) 
1945                     if c
.Get() == (-1, -1, -1): 
1946                         raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1948                         # replace attribute with wxColour object: 
1949                         setattr(self
, '_' + key
, c
) 
1950                         # attach a python dynamic attribute to wxColour for debug printouts 
1951                         c
._name 
= ctrl_kwargs
[key
] 
1953                 elif type(ctrl_kwargs
[key
]) != type(wx
.BLACK
): 
1954                     raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1957 ##        dbg('self._retainFieldValidation:', self._retainFieldValidation) 
1958         if not self
._retainFieldValidation
: 
1959             # Build dictionary of any changing parameters which should be propagated to the 
1961             for arg 
in Field
.propagating_params
: 
1962 ####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) 
1963 ####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) 
1964                 reset_args
[arg
] = kwargs
.has_key(arg
) and kwargs
[arg
] != getattr(self
._ctrl
_constraints
, '_'+arg
) 
1965 ####                dbg('reset_args[%s]?' % arg, reset_args[arg]) 
1967         # Set the control-level constraints: 
1968         self
._ctrl
_constraints
._SetParameters
(**constraint_kwargs
) 
1970         # This routine does the bulk of the interdependent parameter processing, determining 
1971         # the field extents of the mask if changed, resetting parameters as appropriate, 
1972         # determining the overall template value for the control, etc. 
1973         self
._configure
(mask
, **reset_args
) 
1975         # now that we've propagated the field constraints and mask portions to the 
1976         # various fields, validate the constraints 
1977         self
._ctrl
_constraints
._ValidateParameters
(**constraint_kwargs
) 
1979         # Validate that all choices for given fields are at least of the 
1980         # necessary length, and that they all would be valid pastes if pasted 
1981         # into their respective fields: 
1982 ####        dbg('validating choices') 
1983         self
._validateChoices
() 
1986         self
._autofit 
= self
._ctrl
_constraints
._autofit
 
1989         self
._isDate     
= 'D' in self
._ctrl
_constraints
._formatcodes 
and _isDateType(mask
) 
1990         self
._isTime     
= 'T' in self
._ctrl
_constraints
._formatcodes 
and _isTimeType(mask
) 
1992             # Set _dateExtent, used in date validation to locate date in string; 
1993             # always set as though year will be 4 digits, even if mask only has 
1994             # 2 digits, so we can always properly process the intended year for 
1995             # date validation (leap years, etc.) 
1996             if self
._mask
.find('CCC') != -1: self
._dateExtent 
= 11 
1997             else:                            self
._dateExtent 
= 10 
1999             self
._4digityear 
= len(self
._mask
) > 8 and self
._mask
[9] == '#' 
2001         if self
._isDate 
and self
._autoformat
: 
2002             # Auto-decide datestyle: 
2003             if self
._autoformat
.find('MDDY')    != -1: self
._datestyle 
= 'MDY' 
2004             elif self
._autoformat
.find('YMMD')  != -1: self
._datestyle 
= 'YMD' 
2005             elif self
._autoformat
.find('YMMMD') != -1: self
._datestyle 
= 'YMD' 
2006             elif self
._autoformat
.find('DMMY')  != -1: self
._datestyle 
= 'DMY' 
2007             elif self
._autoformat
.find('DMMMY') != -1: self
._datestyle 
= 'DMY' 
2009         # Give derived controls a chance to react to parameter changes before 
2010         # potentially changing current value of the control. 
2011         self
._OnCtrlParametersChanged
() 
2013         if self
.controlInitialized
: 
2014             # Then the base control is available for configuration; 
2015             # take action on base control based on new settings, as appropriate. 
2016             if kwargs
.has_key('useFixedWidthFont'): 
2017                 # Set control font - fixed width by default 
2020             if reset_args
.has_key('reset_mask'): 
2021 ##                dbg('reset mask') 
2022                 curvalue 
= self
._GetValue
() 
2023                 if curvalue
.strip(): 
2025 ##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) 
2026                         self
._SetInitialValue
(self
._GetValue
()) 
2027                     except Exception, e
: 
2028 ##                        dbg('exception caught:', e) 
2029 ##                        dbg("current value doesn't work; attempting to reset to template") 
2030                         self
._SetInitialValue
() 
2032 ##                    dbg('attempting to _SetInitialValue() with template') 
2033                     self
._SetInitialValue
() 
2035             elif kwargs
.has_key('useParensForNegatives'): 
2036                 newvalue 
= self
._getSignedValue
()[0] 
2038                 if newvalue 
is not None: 
2039                     # Adjust for new mask: 
2040                     if len(newvalue
) < len(self
._mask
): 
2042                     elif len(newvalue
) > len(self
._mask
): 
2043                         if newvalue
[-1] in (' ', ')'): 
2044                             newvalue 
= newvalue
[:-1] 
2046 ##                    dbg('reconfiguring value for parens:"%s"' % newvalue) 
2047                     self
._SetValue
(newvalue
) 
2049                     if self
._prevValue 
!= newvalue
: 
2050                         self
._prevValue 
= newvalue  
# disallow undo of sign type 
2053 ##                dbg('calculated size:', self._CalcSize()) 
2054                 self
.SetClientSize(self
._CalcSize
()) 
2055                 width 
= self
.GetSize().width
 
2056                 height 
= self
.GetBestSize().height
 
2057 ##                dbg('setting client size to:', (width, height)) 
2058                 self
.SetInitialSize((width
, height
)) 
2060             # Set value/type-specific formatting 
2061             self
._applyFormatting
() 
2062 ##        dbg(indent=0, suspend=0) 
2064     def SetMaskParameters(self
, **kwargs
): 
2065         """ old name for the SetCtrlParameters function  (DEPRECATED)""" 
2066         return self
.SetCtrlParameters(**kwargs
) 
2069     def GetCtrlParameter(self
, paramname
): 
2071         Routine for retrieving the value of any given parameter 
2073         if MaskedEditMixin
.valid_ctrl_params
.has_key(paramname
.replace('Color','Colour')): 
2074             return getattr(self
, '_' + paramname
.replace('Color', 'Colour')) 
2075         elif Field
.valid_params
.has_key(paramname
): 
2076             return self
._ctrl
_constraints
._GetParameter
(paramname
) 
2078             TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2080     def GetMaskParameter(self
, paramname
): 
2081         """ old name for the GetCtrlParameters function  (DEPRECATED)""" 
2082         return self
.GetCtrlParameter(paramname
) 
2085 ## This idea worked, but Boa was unable to use this solution... 
2086 ##    def _attachMethod(self, func): 
2088 ##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) 
2091 ##    def _DefinePropertyFunctions(exposed_params): 
2092 ##        for param in exposed_params: 
2093 ##            propname = param[0].upper() + param[1:] 
2095 ##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2096 ##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2097 ##            self._attachMethod(locals()['Set%s' % propname]) 
2098 ##            self._attachMethod(locals()['Get%s' % propname]) 
2100 ##            if param.find('Colour') != -1: 
2101 ##                # add non-british spellings, for backward-compatibility 
2102 ##                propname.replace('Colour', 'Color') 
2104 ##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2105 ##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2106 ##                self._attachMethod(locals()['Set%s' % propname]) 
2107 ##                self._attachMethod(locals()['Get%s' % propname]) 
2111     def SetFieldParameters(self
, field_index
, **kwargs
): 
2113         Routine provided to modify the parameters of a given field. 
2114         Because changes to fields can affect the overall control, 
2115         direct access to the fields is prevented, and the control 
2116         is always "reconfigured" after setting a field parameter. 
2117         (See maskededit module overview for the list of valid field-level 
2120         if field_index 
not in self
._field
_indices
: 
2121             ie 
= IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2122             ie
.index 
= field_index
 
2124         # set parameters as requested: 
2125         self
._fields
[field_index
]._SetParameters
(**kwargs
) 
2127         # Possibly reprogram control template due to resulting changes, and ensure 
2128         # control-level params are still propagated to fields: 
2129         self
._configure
(self
._previous
_mask
) 
2130         self
._fields
[field_index
]._ValidateParameters
(**kwargs
) 
2132         if self
.controlInitialized
: 
2133             if kwargs
.has_key('fillChar') or kwargs
.has_key('defaultValue'): 
2134                 self
._SetInitialValue
() 
2137                     # this is tricky, because, as Robin explains: 
2138                     # "Basically there are two sizes to deal with, that are potentially  
2139                     #  different.  The client size is the inside size and may, depending 
2140                     #  on platform, exclude the borders and such.  The normal size is 
2141                     #  the outside size that does include the borders.  What you are 
2142                     #  calculating (in _CalcSize) is the client size, but the sizers 
2143                     #  deal with the full size and so that is the minimum size that 
2144                     #  we need to set with SetInitialSize.  The root of the problem is 
2145                     #  that in _calcSize the current client size height is returned, 
2146                     #  instead of a height based on the current font.  So I suggest using 
2147                     #  _calcSize to just get the width, and then use GetBestSize to 
2149                     self
.SetClientSize(self
._CalcSize
()) 
2150                     width 
= self
.GetSize().width
 
2151                     height 
= self
.GetBestSize().height
 
2152                     self
.SetInitialSize((width
, height
)) 
2155             # Set value/type-specific formatting 
2156             self
._applyFormatting
() 
2159     def GetFieldParameter(self
, field_index
, paramname
): 
2161         Routine provided for getting a parameter of an individual field. 
2163         if field_index 
not in self
._field
_indices
: 
2164             ie 
= IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2165             ie
.index 
= field_index
 
2167         elif Field
.valid_params
.has_key(paramname
): 
2168             return self
._fields
[field_index
]._GetParameter
(paramname
) 
2170             ae 
= AttributeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2171             ae
.attribute 
= paramname
 
2175     def _SetKeycodeHandler(self
, keycode
, func
): 
2177         This function adds and/or replaces key event handling functions 
2178         used by the control.  <func> should take the event as argument 
2179         and return False if no further action on the key is necessary. 
2182             self
._keyhandlers
[keycode
] = func
 
2183         elif self
._keyhandlers
.has_key(keycode
): 
2184             del self
._keyhandlers
[keycode
] 
2187     def _SetKeyHandler(self
, char
, func
): 
2189         This function adds and/or replaces key event handling functions 
2190         for ascii characters.  <func> should take the event as argument 
2191         and return False if no further action on the key is necessary. 
2193         self
._SetKeycodeHandler
(ord(char
), func
) 
2196     def _AddNavKeycode(self
, keycode
, handler
=None): 
2198         This function allows a derived subclass to augment the list of 
2199         keycodes that are considered "navigational" keys. 
2201         self
._nav
.append(keycode
) 
2203             self
._keyhandlers
[keycode
] = handler
 
2204         elif self
.keyhandlers
.has_key(keycode
): 
2205             del self
._keyhandlers
[keycode
] 
2209     def _AddNavKey(self
, char
, handler
=None): 
2211         This function is a convenience function so you don't have to 
2212         remember to call ord() for ascii chars to be used for navigation. 
2214         self
._AddNavKeycode
(ord(char
), handler
) 
2217     def _GetNavKeycodes(self
): 
2219         This function retrieves the current list of navigational keycodes for 
2225     def _SetNavKeycodes(self
, keycode_func_tuples
): 
2227         This function allows you to replace the current list of keycode processed 
2228         as navigation keys, and bind associated optional keyhandlers. 
2231         for keycode
, func 
in keycode_func_tuples
: 
2232             self
._nav
.append(keycode
) 
2234                 self
._keyhandlers
[keycode
] = func
 
2235             elif self
.keyhandlers
.has_key(keycode
): 
2236                 del self
._keyhandlers
[keycode
] 
2239     def _processMask(self
, mask
): 
2241         This subroutine expands {n} syntax in mask strings, and looks for escaped 
2242         special characters and returns the expanded mask, and an dictionary 
2243         of booleans indicating whether or not a given position in the mask is 
2244         a mask character or not. 
2246 ##        dbg('_processMask: mask', mask, indent=1) 
2247         # regular expression for parsing c{n} syntax: 
2248         rex 
= re
.compile('([' +string
.join(maskchars
,"") + '])\{(\d+)\}') 
2250         match 
= rex
.search(s
) 
2251         while match
:    # found an(other) occurrence 
2252             maskchr 
= s
[match
.start(1):match
.end(1)]            # char to be repeated 
2253             repcount 
= int(s
[match
.start(2):match
.end(2)])      # the number of times 
2254             replacement 
= string
.join( maskchr 
* repcount
, "")  # the resulting substr 
2255             s 
= s
[:match
.start(1)] + replacement 
+ s
[match
.end(2)+1:]   #account for trailing '}' 
2256             match 
= rex
.search(s
)                               # look for another such entry in mask 
2258         self
._decimalChar 
= self
._ctrl
_constraints
._decimalChar
 
2259         self
._shiftDecimalChar 
= self
._ctrl
_constraints
._shiftDecimalChar
 
2261         self
._isFloat      
= _isFloatingPoint(s
) and not self
._ctrl
_constraints
._validRegex
 
2262         self
._isInt      
= _isInteger(s
) and not self
._ctrl
_constraints
._validRegex
 
2263         self
._signOk     
= '-' in self
._ctrl
_constraints
._formatcodes 
and (self
._isFloat 
or self
._isInt
) 
2264         self
._useParens  
= self
._ctrl
_constraints
._useParensForNegatives
 
2266 ####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) 
2267 ####        dbg('isFloatingPoint(%s)?' % (s), _isFloatingPoint(s), 
2268 ##            'ctrl regex:', self._ctrl_constraints._validRegex) 
2270         if self
._signOk 
and s
[0] != ' ': 
2272             if self
._ctrl
_constraints
._defaultValue 
and self
._ctrl
_constraints
._defaultValue
[0] != ' ': 
2273                 self
._ctrl
_constraints
._defaultValue 
= ' ' + self
._ctrl
_constraints
._defaultValue
 
2278                 self
._ctrl
_constraints
._defaultValue 
+= ' ' 
2281         # Now, go build up a dictionary of booleans, indexed by position, 
2282         # indicating whether or not a given position is masked or not. 
2283         # Also, strip out any '|' chars, adjusting the mask as necessary, 
2284         # marking the appropriate positions for field boundaries: 
2286         explicit_field_boundaries 
= [] 
2289             if s
[i
] == '\\':            # if escaped character: 
2290                 ismasked
[i
] = False     #     mark position as not a mask char 
2291                 if i
+1 < len(s
):        #     if another char follows... 
2292                     s 
= s
[:i
] + s
[i
+1:] #         elide the '\' 
2293                     if i
+2 < len(s
) and s
[i
+1] == '\\': 
2294                         # if next char also a '\', char is a literal '\' 
2295                         s 
= s
[:i
] + s
[i
+1:]     # elide the 2nd '\' as well 
2296                 i 
+= 1                      # increment to next char 
2298                 s 
= s
[:i
] + s
[i
+1:] #         elide the '|' 
2299                 explicit_field_boundaries
.append(i
) 
2300                 # keep index where it is: 
2301             else:                       # else if special char, mark position accordingly 
2302                 ismasked
[i
] = s
[i
] in maskchars
 
2303 ####                dbg('ismasked[%d]:' % i, ismasked[i], s) 
2304                 i 
+= 1                      # increment to next char 
2305 ####        dbg('ismasked:', ismasked) 
2306 ##        dbg('new mask: "%s"' % s, indent=0) 
2308         return s
, ismasked
, explicit_field_boundaries
 
2311     def _calcFieldExtents(self
): 
2313         Subroutine responsible for establishing/configuring field instances with 
2314         indices and editable extents appropriate to the specified mask, and building 
2315         the lookup table mapping each position to the corresponding field. 
2317         self
._lookupField 
= {} 
2320             ## Create dictionary of positions,characters in mask 
2322             for charnum 
in range( len( self
._mask
)): 
2323                 self
.maskdict
[charnum
] = self
._mask
[charnum
:charnum
+1] 
2325             # For the current mask, create an ordered list of field extents 
2326             # and a dictionary of positions that map to field indices: 
2328             if self
._signOk
: start 
= 1 
2332                 # Skip field "discovery", and just construct a 2-field control with appropriate 
2333                 # constraints for a floating-point entry. 
2335                 # .setdefault always constructs 2nd argument even if not needed, so we do this 
2336                 # the old-fashioned way... 
2337                 if not self
._fields
.has_key(0): 
2338                     self
._fields
[0] = Field() 
2339                 if not self
._fields
.has_key(1): 
2340                     self
._fields
[1] = Field() 
2342                 self
._decimalpos 
= string
.find( self
._mask
, '.') 
2343 ##                dbg('decimal pos =', self._decimalpos) 
2345                 formatcodes 
= self
._fields
[0]._GetParameter
('formatcodes') 
2346                 if 'R' not in formatcodes
: formatcodes 
+= 'R' 
2347                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, self
._decimalpos
), 
2348                                                mask
=self
._mask
[start
:self
._decimalpos
], formatcodes
=formatcodes
) 
2349                 end 
= len(self
._mask
) 
2350                 if self
._signOk 
and self
._useParens
: 
2352                 self
._fields
[1]._SetParameters
(index
=1, extent
=(self
._decimalpos
+1, end
), 
2353                                                mask
=self
._mask
[self
._decimalpos
+1:end
]) 
2355                 for i 
in range(self
._decimalpos
+1): 
2356                     self
._lookupField
[i
] = 0 
2358                 for i 
in range(self
._decimalpos
+1, len(self
._mask
)+1): 
2359                     self
._lookupField
[i
] = 1 
2362                 # Skip field "discovery", and just construct a 1-field control with appropriate 
2363                 # constraints for a integer entry. 
2364                 if not self
._fields
.has_key(0): 
2365                     self
._fields
[0] = Field(index
=0) 
2366                 end 
= len(self
._mask
) 
2367                 if self
._signOk 
and self
._useParens
: 
2369                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, end
), 
2370                                                mask
=self
._mask
[start
:end
]) 
2371                 for i 
in range(len(self
._mask
)+1): 
2372                     self
._lookupField
[i
] = 0 
2374                 # generic control; parse mask to figure out where the fields are: 
2377                 i 
= self
._findNextEntry
(pos
,adjustInsert
=False)  # go to 1st entry point: 
2378                 if i 
< len(self
._mask
):   # no editable chars! 
2379                     for j 
in range(pos
, i
+1): 
2380                         self
._lookupField
[j
] = field_index
 
2381                     pos 
= i       
# figure out field for 1st editable space: 
2383                 while i 
<= len(self
._mask
): 
2384 ####                    dbg('searching: outer field loop: i = ', i) 
2385                     if self
._isMaskChar
(i
): 
2386 ####                        dbg('1st char is mask char; recording edit_start=', i) 
2388                         # Skip to end of editable part of current field: 
2389                         while i 
< len(self
._mask
) and self
._isMaskChar
(i
): 
2390                             self
._lookupField
[i
] = field_index
 
2392                             if i 
in self
._explicit
_field
_boundaries
: 
2394 ####                        dbg('edit_end =', i) 
2396                         self
._lookupField
[i
] = field_index
 
2397 ####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) 
2398                         if not self
._fields
.has_key(field_index
): 
2399                             kwargs 
= Field
.valid_params
.copy() 
2400                             kwargs
['index'] = field_index
 
2401                             kwargs
['extent'] = (edit_start
, edit_end
) 
2402                             kwargs
['mask'] = self
._mask
[edit_start
:edit_end
] 
2403                             self
._fields
[field_index
] = Field(**kwargs
) 
2405                             self
._fields
[field_index
]._SetParameters
( 
2407                                                                 extent
=(edit_start
, edit_end
), 
2408                                                                 mask
=self
._mask
[edit_start
:edit_end
]) 
2410                     i 
= self
._findNextEntry
(pos
, adjustInsert
=False)  # go to next field: 
2411 ####                    dbg('next entry:', i) 
2413                         for j 
in range(pos
, i
+1): 
2414                             self
._lookupField
[j
] = field_index
 
2415                     if i 
>= len(self
._mask
): 
2416                         break           # if past end, we're done 
2419 ####                        dbg('next field:', field_index) 
2421         indices 
= self
._fields
.keys() 
2423         self
._field
_indices 
= indices
[1:] 
2424 ####        dbg('lookupField map:', indent=1) 
2425 ##        for i in range(len(self._mask)): 
2426 ####            dbg('pos %d:' % i, self._lookupField[i]) 
2429         # Verify that all field indices specified are valid for mask: 
2430         for index 
in self
._fields
.keys(): 
2431             if index 
not in [-1] + self
._lookupField
.values(): 
2432                 ie 
= IndexError('field %d is not a valid field for mask "%s"' % (index
, self
._mask
)) 
2438     def _calcTemplate(self
, reset_fillchar
, reset_default
): 
2440         Subroutine for processing current fillchars and default values for 
2441         whole control and individual fields, constructing the resulting 
2442         overall template, and adjusting the current value as necessary. 
2445         if self
._ctrl
_constraints
._defaultValue
: 
2448             for field 
in self
._fields
.values(): 
2449                 if field
._defaultValue 
and not reset_default
: 
2451 ##        dbg('default set?', default_set) 
2453         # Determine overall new template for control, and keep track of previous 
2454         # values, so that current control value can be modified as appropriate: 
2455         if self
.controlInitialized
: curvalue 
= list(self
._GetValue
()) 
2456         else:                       curvalue 
= None 
2458         if hasattr(self
, '_fillChar'): old_fillchars 
= self
._fillChar
 
2459         else:                          old_fillchars 
= None 
2461         if hasattr(self
, '_template'): old_template 
= self
._template
 
2462         else:                          old_template 
= None 
2469         for field 
in self
._fields
.values(): 
2470             field
._template 
= "" 
2472         for pos 
in range(len(self
._mask
)): 
2473 ####            dbg('pos:', pos) 
2474             field 
= self
._FindField
(pos
) 
2475 ####            dbg('field:', field._index) 
2476             start
, end 
= field
._extent
 
2478             if pos 
== 0 and self
._signOk
: 
2479                 self
._template 
= ' ' # always make 1st 1st position blank, regardless of fillchar 
2480             elif self
._isFloat 
and pos 
== self
._decimalpos
: 
2481                 self
._template 
+= self
._decimalChar
 
2482             elif self
._isMaskChar
(pos
): 
2483                 if field
._fillChar 
!= self
._ctrl
_constraints
._fillChar 
and not reset_fillchar
: 
2484                     fillChar 
= field
._fillChar
 
2486                     fillChar 
= self
._ctrl
_constraints
._fillChar
 
2487                 self
._fillChar
[pos
] = fillChar
 
2489                 # Replace any current old fillchar with new one in current value; 
2490                 # if action required, set reset_value flag so we can take that action 
2491                 # after we're all done 
2492                 if self
.controlInitialized 
and old_fillchars 
and old_fillchars
.has_key(pos
) and curvalue
: 
2493                     if curvalue
[pos
] == old_fillchars
[pos
] and old_fillchars
[pos
] != fillChar
: 
2495                         curvalue
[pos
] = fillChar
 
2497                 if not field
._defaultValue 
and not self
._ctrl
_constraints
._defaultValue
: 
2498 ####                    dbg('no default value') 
2499                     self
._template 
+= fillChar
 
2500                     field
._template 
+= fillChar
 
2502                 elif field
._defaultValue 
and not reset_default
: 
2503 ####                    dbg('len(field._defaultValue):', len(field._defaultValue)) 
2504 ####                    dbg('pos-start:', pos-start) 
2505                     if len(field
._defaultValue
) > pos
-start
: 
2506 ####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) 
2507                         self
._template 
+= field
._defaultValue
[pos
-start
] 
2508                         field
._template 
+= field
._defaultValue
[pos
-start
] 
2510 ####                        dbg('field default not long enough; using fillChar') 
2511                         self
._template 
+= fillChar
 
2512                         field
._template 
+= fillChar
 
2514                     if len(self
._ctrl
_constraints
._defaultValue
) > pos
: 
2515 ####                        dbg('using control default') 
2516                         self
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2517                         field
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2519 ####                        dbg('ctrl default not long enough; using fillChar') 
2520                         self
._template 
+= fillChar
 
2521                         field
._template 
+= fillChar
 
2522 ####                dbg('field[%d]._template now "%s"' % (field._index, field._template)) 
2523 ####                dbg('self._template now "%s"' % self._template) 
2525                 self
._template 
+= self
._mask
[pos
] 
2527         self
._fields
[-1]._template 
= self
._template     
# (for consistency) 
2529         if curvalue
:    # had an old value, put new one back together 
2530             newvalue 
= string
.join(curvalue
, "") 
2535             self
._defaultValue 
= self
._template
 
2536 ##            dbg('self._defaultValue:', self._defaultValue) 
2537             if not self
.IsEmpty(self
._defaultValue
) and not self
.IsValid(self
._defaultValue
): 
2539                 ve 
= ValueError('Default value of "%s" is not a valid value for control "%s"' % (self
._defaultValue
, self
.name
)) 
2540                 ve
.value 
= self
._defaultValue
 
2543             # if no fillchar change, but old value == old template, replace it: 
2544             if newvalue 
== old_template
: 
2545                 newvalue 
= self
._template
 
2548             self
._defaultValue 
= None 
2551 ##            dbg('resetting value to: "%s"' % newvalue) 
2552             pos 
= self
._GetInsertionPoint
() 
2553             sel_start
, sel_to 
= self
._GetSelection
() 
2554             self
._SetValue
(newvalue
) 
2555             self
._SetInsertionPoint
(pos
) 
2556             self
._SetSelection
(sel_start
, sel_to
) 
2559     def _propagateConstraints(self
, **reset_args
): 
2561         Subroutine for propagating changes to control-level constraints and 
2562         formatting to the individual fields as appropriate. 
2564         parent_codes 
= self
._ctrl
_constraints
._formatcodes
 
2565         parent_includes 
= self
._ctrl
_constraints
._includeChars
 
2566         parent_excludes 
= self
._ctrl
_constraints
._excludeChars
 
2567         for i 
in self
._field
_indices
: 
2568             field 
= self
._fields
[i
] 
2570             if len(self
._field
_indices
) == 1: 
2571                 inherit_args
['formatcodes'] = parent_codes
 
2572                 inherit_args
['includeChars'] = parent_includes
 
2573                 inherit_args
['excludeChars'] = parent_excludes
 
2575                 field_codes 
= current_codes 
= field
._GetParameter
('formatcodes') 
2576                 for c 
in parent_codes
: 
2577                     if c 
not in field_codes
: field_codes 
+= c
 
2578                 if field_codes 
!= current_codes
: 
2579                     inherit_args
['formatcodes'] = field_codes
 
2581                 include_chars 
= current_includes 
= field
._GetParameter
('includeChars') 
2582                 for c 
in parent_includes
: 
2583                     if not c 
in include_chars
: include_chars 
+= c
 
2584                 if include_chars 
!= current_includes
: 
2585                     inherit_args
['includeChars'] = include_chars
 
2587                 exclude_chars 
= current_excludes 
= field
._GetParameter
('excludeChars') 
2588                 for c 
in parent_excludes
: 
2589                     if not c 
in exclude_chars
: exclude_chars 
+= c
 
2590                 if exclude_chars 
!= current_excludes
: 
2591                     inherit_args
['excludeChars'] = exclude_chars
 
2593             if reset_args
.has_key('defaultValue') and reset_args
['defaultValue']: 
2594                 inherit_args
['defaultValue'] = ""   # (reset for field) 
2596             for param 
in Field
.propagating_params
: 
2597 ####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) 
2598 ####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) 
2599                 if reset_args
.has_key(param
): 
2600                     inherit_args
[param
] = self
.GetCtrlParameter(param
) 
2601 ####                    dbg('inherit_args[%s]' % param, inherit_args[param]) 
2604                 field
._SetParameters
(**inherit_args
) 
2605                 field
._ValidateParameters
(**inherit_args
) 
2608     def _validateChoices(self
): 
2610         Subroutine that validates that all choices for given fields are at 
2611         least of the necessary length, and that they all would be valid pastes 
2612         if pasted into their respective fields. 
2614         for field 
in self
._fields
.values(): 
2616                 index 
= field
._index
 
2617                 if len(self
._field
_indices
) == 1 and index 
== 0 and field
._choices 
== self
._ctrl
_constraints
._choices
: 
2618 ##                    dbg('skipping (duplicate) choice validation of field 0') 
2620 ####                dbg('checking for choices for field', field._index) 
2621                 start
, end 
= field
._extent
 
2622                 field_length 
= end 
- start
 
2623 ####                dbg('start, end, length:', start, end, field_length) 
2624                 for choice 
in field
._choices
: 
2625 ####                    dbg('testing "%s"' % choice) 
2626                     valid_paste
, ignore
, replace_to 
= self
._validatePaste
(choice
, start
, end
) 
2629                         ve 
= ValueError('"%s" could not be entered into field %d of control "%s"' % (choice
, index
, self
.name
)) 
2633                     elif replace_to 
> end
: 
2635                         ve 
= ValueError('"%s" will not fit into field %d of control "%s"' (choice
, index
, self
.name
)) 
2640 ####                    dbg(choice, 'valid in field', index) 
2643     def _configure(self
, mask
, **reset_args
): 
2645         This function sets flags for automatic styling options.  It is 
2646         called whenever a control or field-level parameter is set/changed. 
2648         This routine does the bulk of the interdependent parameter processing, determining 
2649         the field extents of the mask if changed, resetting parameters as appropriate, 
2650         determining the overall template value for the control, etc. 
2652         reset_args is supplied if called from control's .SetCtrlParameters() 
2653         routine, and indicates which if any parameters which can be 
2654         overridden by individual fields have been reset by request for the 
2659 ##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) 
2661         # Preprocess specified mask to expand {n} syntax, handle escaped 
2662         # mask characters, etc and build the resulting positionally keyed 
2663         # dictionary for which positions are mask vs. template characters: 
2664         self
._mask
, self
._ismasked
, self
._explicit
_field
_boundaries 
= self
._processMask
(mask
) 
2665         self
._masklength 
= len(self
._mask
) 
2666 ####        dbg('processed mask:', self._mask) 
2668         # Preserve original mask specified, for subsequent reprocessing 
2669         # if parameters change. 
2670 ##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) 
2671         self
._previous
_mask 
= mask    
# save unexpanded mask for next time 
2672             # Set expanded mask and extent of field -1 to width of entire control: 
2673         self
._ctrl
_constraints
._SetParameters
(mask 
= self
._mask
, extent
=(0,self
._masklength
)) 
2675         # Go parse mask to determine where each field is, construct field 
2676         # instances as necessary, configure them with those extents, and 
2677         # build lookup table mapping each position for control to its corresponding 
2679 ####        dbg('calculating field extents') 
2681         self
._calcFieldExtents
() 
2684         # Go process defaultValues and fillchars to construct the overall 
2685         # template, and adjust the current value as necessary: 
2686         reset_fillchar 
= reset_args
.has_key('fillChar') and reset_args
['fillChar'] 
2687         reset_default 
= reset_args
.has_key('defaultValue') and reset_args
['defaultValue'] 
2689 ####        dbg('calculating template') 
2690         self
._calcTemplate
(reset_fillchar
, reset_default
) 
2692         # Propagate control-level formatting and character constraints to each 
2693         # field if they don't already have them; if only one field, propagate 
2694         # control-level validation constraints to field as well: 
2695 ####        dbg('propagating constraints') 
2696         self
._propagateConstraints
(**reset_args
) 
2699         if self
._isFloat 
and self
._fields
[0]._groupChar 
== self
._decimalChar
: 
2700             raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % 
2701                                  (self
._fields
[0]._groupChar
, self
._decimalChar
) ) 
2703 ####        dbg('fields:', indent=1) 
2704 ##        for i in [-1] + self._field_indices: 
2705 ####            dbg('field %d:' % i, self._fields[i].__dict__) 
2708         # Set up special parameters for numeric control, if appropriate: 
2710             self
._signpos 
= 0   # assume it starts here, but it will move around on floats 
2711             signkeys 
= ['-', '+', ' '] 
2713                 signkeys 
+= ['(', ')'] 
2714             for key 
in signkeys
: 
2716                 if not self
._keyhandlers
.has_key(keycode
): 
2717                     self
._SetKeyHandler
(key
, self
._OnChangeSign
) 
2718         elif self
._isInt 
or self
._isFloat
: 
2719             signkeys 
= ['-', '+', ' ', '(', ')'] 
2720             for key 
in signkeys
: 
2722                 if self
._keyhandlers
.has_key(keycode
) and self
._keyhandlers
[keycode
] == self
._OnChangeSign
: 
2723                     self
._SetKeyHandler
(key
, None) 
2727         if self
._isFloat 
or self
._isInt
: 
2728             if self
.controlInitialized
: 
2729                 value 
= self
._GetValue
() 
2730 ####                dbg('value: "%s"' % value, 'len(value):', len(value), 
2731 ##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) 
2732                 if len(value
) < len(self
._ctrl
_constraints
._mask
): 
2734                     if self
._useParens 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find('(') == -1: 
2736                     if self
._signOk 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find(')') == -1: 
2737                         newvalue 
= ' ' + newvalue
 
2738                     if len(newvalue
) < len(self
._ctrl
_constraints
._mask
): 
2739                         if self
._ctrl
_constraints
._alignRight
: 
2740                             newvalue 
= newvalue
.rjust(len(self
._ctrl
_constraints
._mask
)) 
2742                             newvalue 
= newvalue
.ljust(len(self
._ctrl
_constraints
._mask
)) 
2743 ##                    dbg('old value: "%s"' % value) 
2744 ##                    dbg('new value: "%s"' % newvalue) 
2746                         self
._SetValue
(newvalue
) 
2747                     except Exception, e
: 
2748 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2749                         self
._SetInitialValue
() 
2751                 elif len(value
) > len(self
._ctrl
_constraints
._mask
): 
2753                     if not self
._useParens 
and newvalue
[-1] == ' ': 
2754                         newvalue 
= newvalue
[:-1] 
2755                     if not self
._signOk 
and len(newvalue
) > len(self
._ctrl
_constraints
._mask
): 
2756                         newvalue 
= newvalue
[1:] 
2757                     if not self
._signOk
: 
2758                         newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(newvalue
) 
2760 ##                    dbg('old value: "%s"' % value) 
2761 ##                    dbg('new value: "%s"' % newvalue) 
2763                         self
._SetValue
(newvalue
) 
2764                     except Exception, e
: 
2765 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2766                         self
._SetInitialValue
() 
2767                 elif not self
._signOk 
and ('(' in value 
or '-' in value
): 
2768                     newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(value
) 
2769 ##                    dbg('old value: "%s"' % value) 
2770 ##                    dbg('new value: "%s"' % newvalue) 
2772                         self
._SetValue
(newvalue
) 
2774 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2775                         self
._SetInitialValue
() 
2777             # Replace up/down arrow default handling: 
2778             # make down act like tab, up act like shift-tab: 
2780 ####            dbg('Registering numeric navigation and control handlers (if not already set)') 
2781             if not self
._keyhandlers
.has_key(wx
.WXK_DOWN
): 
2782                 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnChangeField
) 
2783             if not self
._keyhandlers
.has_key(wx
.WXK_UP
): 
2784                 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnUpNumeric
)  # (adds "shift" to up arrow, and calls _OnChangeField) 
2786             # On ., truncate contents right of cursor to decimal point (if any) 
2787             # leaves cursor after decimal point if floating point, otherwise at 0. 
2788             if not self
._keyhandlers
.has_key(ord(self
._decimalChar
)) or self
._keyhandlers
[ord(self
._decimalChar
)] != self
._OnDecimalPoint
: 
2789                 self
._SetKeyHandler
(self
._decimalChar
, self
._OnDecimalPoint
) 
2791             if not self
._keyhandlers
.has_key(ord(self
._shiftDecimalChar
)) or self
._keyhandlers
[ord(self
._shiftDecimalChar
)] != self
._OnChangeField
: 
2792                 self
._SetKeyHandler
(self
._shiftDecimalChar
, self
._OnChangeField
)   # (Shift-'.' == '>' on US keyboards) 
2794             # Allow selective insert of groupchar in numbers: 
2795             if not self
._keyhandlers
.has_key(ord(self
._fields
[0]._groupChar
)) or self
._keyhandlers
[ord(self
._fields
[0]._groupChar
)] != self
._OnGroupChar
: 
2796                 self
._SetKeyHandler
(self
._fields
[0]._groupChar
, self
._OnGroupChar
) 
2798 ##        dbg(indent=0, suspend=0) 
2801     def _SetInitialValue(self
, value
=""): 
2803         fills the control with the generated or supplied default value. 
2804         It will also set/reset the font if necessary and apply 
2805         formatting to the control at this time. 
2807 ##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) 
2809             self
._prevValue 
= self
._curValue 
= self
._template
 
2810             # don't apply external validation rules in this case, as template may 
2811             # not coincide with "legal" value... 
2813                 self
._SetValue
(self
._curValue
)  # note the use of "raw" ._SetValue()... 
2814             except Exception, e
: 
2815 ##                dbg('exception thrown:', e, indent=0) 
2818             # Otherwise apply validation as appropriate to passed value: 
2819 ####            dbg('value = "%s", length:' % value, len(value)) 
2820             self
._prevValue 
= self
._curValue 
= value
 
2822                 self
.SetValue(value
)            # use public (validating) .SetValue() 
2823             except Exception, e
: 
2824 ##                dbg('exception thrown:', e, indent=0) 
2828         # Set value/type-specific formatting 
2829         self
._applyFormatting
() 
2833     def _calcSize(self
, size
=None): 
2834         """ Calculate automatic size if allowed; must be called after the base control is instantiated""" 
2835 ####        dbg('MaskedEditMixin::_calcSize', indent=1) 
2836         cont 
= (size 
is None or size 
== wx
.DefaultSize
) 
2838         if cont 
and self
._autofit
: 
2839             sizing_text 
= 'M' * self
._masklength
 
2840             if wx
.Platform 
!= "__WXMSW__":   # give it a little extra space 
2842             if wx
.Platform 
== "__WXMAC__":   # give it even a little more... 
2844 ####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) 
2845             w
, h 
= self
.GetTextExtent(sizing_text
) 
2846             size 
= (w
+4, self
.GetSize().height
) 
2847 ####            dbg('size:', size, indent=0) 
2852         """ Set the control's font typeface -- pass the font name as str.""" 
2853 ####        dbg('MaskedEditMixin::_setFont', indent=1) 
2854         if not self
._useFixedWidthFont
: 
2855             self
._font 
= wx
.SystemSettings_GetFont(wx
.SYS_DEFAULT_GUI_FONT
) 
2857             font 
= self
.GetFont()   # get size, weight, etc from current font 
2859             # Set to teletype font (guaranteed to be mappable to all wxWindows 
2861             self
._font 
= wx
.Font( font
.GetPointSize(), wx
.TELETYPE
, font
.GetStyle(), 
2862                                  font
.GetWeight(), font
.GetUnderlined()) 
2863 ####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) 
2865         self
.SetFont(self
._font
) 
2869     def _OnTextChange(self
, event
): 
2871         Handler for EVT_TEXT event. 
2872         self._Change() is provided for subclasses, and may return False to 
2873         skip this method logic.  This function returns True if the event 
2874         detected was a legitimate event, or False if it was a "bogus" 
2875         EVT_TEXT event.  (NOTE: There is currently an issue with calling 
2876         .SetValue from within the EVT_CHAR handler that causes duplicate 
2877         EVT_TEXT events for the same change.) 
2879         newvalue 
= self
._GetValue
() 
2880 ##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) 
2882         if self
._ignoreChange
:      # ie. if an "intermediate text change event" 
2886         ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue 
2887         ## call is generating two (2) EVT_TEXT events.  On certain platforms, 
2888         ## (eg. linux/GTK) the 1st is an empty string value. 
2889         ## This is the only mechanism I can find to mask this problem: 
2890         if newvalue 
== self
._curValue 
or len(newvalue
) == 0: 
2891 ##            dbg('ignoring bogus text change event', indent=0) 
2894 ##            dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue))) 
2896                 if self
._signOk 
and self
._isNeg 
and newvalue
.find('-') == -1 and newvalue
.find('(') == -1: 
2897 ##                    dbg('clearing self._isNeg') 
2899                     text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
() 
2900                 self
._CheckValid
()  # Recolor control as appropriate 
2901 ##            dbg('calling event.Skip()') 
2904         self
._prevValue 
= self
._curValue    
# save for undo 
2905         self
._curValue 
= newvalue           
# Save last seen value for next iteration 
2910     def _OnKeyDown(self
, event
): 
2912         This function allows the control to capture Ctrl-events like Ctrl-tab, 
2913         that are not normally seen by the "cooked" EVT_CHAR routine. 
2915         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2916         key    
= event
.GetKeyCode() 
2917         if key 
in self
._nav 
and event
.ControlDown(): 
2918             # then this is the only place we will likely see these events; 
2920 ##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') 
2923         # else allow regular EVT_CHAR key processing 
2927     def _OnChar(self
, event
): 
2929         This is the engine of MaskedEdit controls.  It examines each keystroke, 
2930         decides if it's allowed, where it should go or what action to take. 
2932 ##        dbg('MaskedEditMixin::_OnChar', indent=1) 
2934         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2935         key 
= event
.GetKeyCode() 
2936         orig_pos 
= self
._GetInsertionPoint
() 
2937         orig_value 
= self
._GetValue
() 
2938 ##        dbg('keycode = ', key) 
2939 ##        dbg('current pos = ', orig_pos) 
2940 ##        dbg('current selection = ', self._GetSelection()) 
2942         if not self
._Keypress
(key
): 
2946         # If no format string for this control, or the control is marked as "read-only", 
2947         # skip the rest of the special processing, and just "do the standard thing:" 
2948         if not self
._mask 
or not self
._IsEditable
(): 
2953         # Process navigation and control keys first, with 
2954         # position/selection unadulterated: 
2955         if key 
in self
._nav 
+ self
._control
: 
2956             if self
._keyhandlers
.has_key(key
): 
2957                 keep_processing 
= self
._keyhandlers
[key
](event
) 
2958                 if self
._GetValue
() != orig_value
: 
2959                     self
.modified 
= True 
2960                 if not keep_processing
: 
2963                 self
._applyFormatting
() 
2967         # Else... adjust the position as necessary for next input key, 
2968         # and determine resulting selection: 
2969         pos 
= self
._adjustPos
( orig_pos
, key 
)    ## get insertion position, adjusted as needed 
2970         sel_start
, sel_to 
= self
._GetSelection
()                ## check for a range of selected text 
2971 ##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) 
2973         keep_processing 
= True 
2974         # Capture user past end of format field 
2975         if pos 
> len(self
.maskdict
): 
2976 ##            dbg("field length exceeded:",pos) 
2977             keep_processing 
= False 
2979         key 
= self
._adjustKey
(pos
, key
)     # apply formatting constraints to key: 
2981         if self
._keyhandlers
.has_key(key
): 
2982             # there's an override for default behavior; use override function instead 
2983 ##            dbg('using supplied key handler:', self._keyhandlers[key]) 
2984             keep_processing 
= self
._keyhandlers
[key
](event
) 
2985             if self
._GetValue
() != orig_value
: 
2986                 self
.modified 
= True 
2987             if not keep_processing
: 
2990             # else skip default processing, but do final formatting 
2991         if key 
in wx_control_keycodes
: 
2992 ##            dbg('key in wx_control_keycodes') 
2993             event
.Skip()                # non-printable; let base control handle it 
2994             keep_processing 
= False 
2996             field 
= self
._FindField
(pos
) 
2998             if 'unicode' in wx
.PlatformInfo
: 
3000                     char 
= chr(key
) # (must work if we got this far) 
3001                     char 
= char
.decode(self
._defaultEncoding
) 
3003                     char 
= unichr(event
.GetUnicodeKey()) 
3004                     dbg('unicode char:', char
) 
3006                 if type(field
._excludeChars
) != types
.UnicodeType
: 
3007                     excludes 
+= field
._excludeChars
.decode(self
._defaultEncoding
) 
3008                 if type(self
._ctrl
_constraints
) != types
.UnicodeType
: 
3009                     excludes 
+= self
._ctrl
_constraints
._excludeChars
.decode(self
._defaultEncoding
) 
3011                 char 
= chr(key
) # (must work if we got this far) 
3012                 excludes 
= field
._excludeChars 
+ self
._ctrl
_constraints
._excludeChars
 
3014 ##                dbg("key ='%s'" % chr(key)) 
3016 ##                dbg('okSpaces?', field._okSpaces) 
3020             if char 
in excludes
: 
3021                 keep_processing 
= False 
3023             if keep_processing 
and self
._isCharAllowed
( char
, pos
, checkRegex 
= True ): 
3024 ##                dbg("key allowed by mask") 
3025                 # insert key into candidate new value, but don't change control yet: 
3026                 oldstr 
= self
._GetValue
() 
3027                 newstr
, newpos
, new_select_to
, match_field
, match_index 
= self
._insertKey
( 
3028                                 char
, pos
, sel_start
, sel_to
, self
._GetValue
(), allowAutoSelect 
= True) 
3029 ##                dbg("str with '%s' inserted:" % char, '"%s"' % newstr) 
3030                 if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3031 ##                    dbg('not valid; checking to see if adjusted string is:') 
3032                     keep_processing 
= False 
3033                     if self
._isFloat 
and newstr 
!= self
._template
: 
3034                         newstr 
= self
._adjustFloat
(newstr
) 
3035 ##                        dbg('adjusted str:', newstr) 
3036                         if self
.IsValid(newstr
): 
3038                             keep_processing 
= True 
3039                             wx
.CallAfter(self
._SetInsertionPoint
, self
._decimalpos
) 
3040                     if not keep_processing
: 
3041 ##                        dbg("key disallowed by validation") 
3042                         if not wx
.Validator_IsSilent() and orig_pos 
== pos
: 
3048                     # special case: adjust date value as necessary: 
3049                     if self
._isDate 
and newstr 
!= self
._template
: 
3050                         newstr 
= self
._adjustDate
(newstr
) 
3051 ##                    dbg('adjusted newstr:', newstr) 
3053                     if newstr 
!= orig_value
: 
3054                         self
.modified 
= True 
3056                     wx
.CallAfter(self
._SetValue
, newstr
) 
3058                     # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: 
3059                     if not self
.IsDefault() and self
._isDate 
and self
._4digityear
: 
3060                         year2dig 
= self
._dateExtent 
- 2 
3061                         if pos 
== year2dig 
and unadjusted
[year2dig
] != newstr
[year2dig
]: 
3064 ##                    dbg('queuing insertion point: (%d)' % newpos) 
3065                     wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3067                     if match_field 
is not None: 
3068 ##                        dbg('matched field') 
3069                         self
._OnAutoSelect
(match_field
, match_index
) 
3071                     if new_select_to 
!= newpos
: 
3072 ##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) 
3073                         wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
3075                         newfield 
= self
._FindField
(newpos
) 
3076                         if newfield 
!= field 
and newfield
._selectOnFieldEntry
: 
3077 ##                            dbg('queuing insertion point: (%d)' % newfield._extent[0]) 
3078                             wx
.CallAfter(self
._SetInsertionPoint
, newfield
._extent
[0]) 
3079 ##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) 
3080                             wx
.CallAfter(self
._SetSelection
, newfield
._extent
[0], newfield
._extent
[1]) 
3082                             wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
3083                     keep_processing 
= False 
3085             elif keep_processing
: 
3086 ##                dbg('char not allowed') 
3087                 keep_processing 
= False 
3088                 if (not wx
.Validator_IsSilent()) and orig_pos 
== pos
: 
3091         self
._applyFormatting
() 
3093         # Move to next insertion point 
3094         if keep_processing 
and key 
not in self
._nav
: 
3095             pos 
= self
._GetInsertionPoint
() 
3096             next_entry 
= self
._findNextEntry
( pos 
) 
3097             if pos 
!= next_entry
: 
3098 ##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) 
3099                 wx
.CallAfter(self
._SetInsertionPoint
, next_entry 
) 
3101             if self
._isTemplateChar
(pos
): 
3102                 self
._AdjustField
(pos
) 
3106     def _FindFieldExtent(self
, pos
=None, getslice
=False, value
=None): 
3107         """ returns editable extent of field corresponding to 
3108         position pos, and, optionally, the contents of that field 
3109         in the control or the value specified. 
3110         Template chars are bound to the preceding field. 
3111         For masks beginning with template chars, these chars are ignored 
3112         when calculating the current field. 
3114         Eg: with template (###) ###-####, 
3115         >>> self._FindFieldExtent(pos=0) 
3117         >>> self._FindFieldExtent(pos=1) 
3119         >>> self._FindFieldExtent(pos=5) 
3121         >>> self._FindFieldExtent(pos=6) 
3123         >>> self._FindFieldExtent(pos=10) 
3127 ##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) 
3129         field 
= self
._FindField
(pos
) 
3132                 return None, None, "" 
3135         edit_start
, edit_end 
= field
._extent
 
3137             if value 
is None: value 
= self
._GetValue
() 
3138             slice = value
[edit_start
:edit_end
] 
3139 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) 
3141             return edit_start
, edit_end
, slice 
3143 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end) 
3145             return edit_start
, edit_end
 
3148     def _FindField(self
, pos
=None): 
3150         Returns the field instance in which pos resides. 
3151         Template chars are bound to the preceding field. 
3152         For masks beginning with template chars, these chars are ignored 
3153         when calculating the current field. 
3156 ####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) 
3157         if pos 
is None: pos 
= self
._GetInsertionPoint
() 
3158         elif pos 
< 0 or pos 
> self
._masklength
: 
3159             raise IndexError('position %s out of range of control' % str(pos
)) 
3161         if len(self
._fields
) == 0: 
3167         return self
._fields
[self
._lookupField
[pos
]] 
3170     def ClearValue(self
): 
3171         """ Blanks the current control value by replacing it with the default value.""" 
3172 ##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") 
3173         self
._SetValue
( self
._template 
) 
3174         self
._SetInsertionPoint
(0) 
3178     def _baseCtrlEventHandler(self
, event
): 
3180         This function is used whenever a key should be handled by the base control. 
3186     def _OnUpNumeric(self
, event
): 
3188         Makes up-arrow act like shift-tab should; ie. take you to start of 
3191 ##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1) 
3192         event
.m_shiftDown 
= 1 
3193 ##        dbg('event.ShiftDown()?', event.ShiftDown()) 
3194         self
._OnChangeField
(event
) 
3198     def _OnArrow(self
, event
): 
3200         Used in response to left/right navigation keys; makes these actions skip 
3201         over mask template chars. 
3203 ##        dbg("MaskedEditMixin::_OnArrow", indent=1) 
3204         pos 
= self
._GetInsertionPoint
() 
3205         keycode 
= event
.GetKeyCode() 
3206         sel_start
, sel_to 
= self
._GetSelection
() 
3207         entry_end 
= self
._goEnd
(getPosOnly
=True) 
3208         if keycode 
in (wx
.WXK_RIGHT
, wx
.WXK_DOWN
): 
3209             if( ( not self
._isTemplateChar
(pos
) and pos
+1 > entry_end
) 
3210                 or ( self
._isTemplateChar
(pos
) and pos 
>= entry_end
) ): 
3211 ##                dbg("can't advance", indent=0) 
3213             elif self
._isTemplateChar
(pos
): 
3214                 self
._AdjustField
(pos
) 
3215         elif keycode 
in (wx
.WXK_LEFT
,wx
.WXK_UP
) and sel_start 
== sel_to 
and pos 
> 0 and self
._isTemplateChar
(pos
-1): 
3216 ##            dbg('adjusting field') 
3217             self
._AdjustField
(pos
) 
3219         # treat as shifted up/down arrows as tab/reverse tab: 
3220         if event
.ShiftDown() and keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
): 
3221             # remove "shifting" and treat as (forward) tab: 
3222             event
.m_shiftDown 
= False 
3223             keep_processing 
= self
._OnChangeField
(event
) 
3225         elif self
._FindField
(pos
)._selectOnFieldEntry
: 
3226             if( keycode 
in (wx
.WXK_UP
, wx
.WXK_LEFT
) 
3228                 and self
._isTemplateChar
(sel_start
-1) 
3229                 and sel_start 
!= self
._masklength
 
3230                 and not self
._signOk 
and not self
._useParens
): 
3232                 # call _OnChangeField to handle "ctrl-shifted event" 
3233                 # (which moves to previous field and selects it.) 
3234                 event
.m_shiftDown 
= True 
3235                 event
.m_ControlDown 
= True 
3236                 keep_processing 
= self
._OnChangeField
(event
) 
3237             elif( keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
) 
3238                   and sel_to 
!= self
._masklength
 
3239                   and self
._isTemplateChar
(sel_to
)): 
3241                 # when changing field to the right, ensure don't accidentally go left instead 
3242                 event
.m_shiftDown 
= False 
3243                 keep_processing 
= self
._OnChangeField
(event
) 
3245                 # treat arrows as normal, allowing selection 
3247 ##                dbg('using base ctrl event processing') 
3250             if( (sel_to 
== self
._fields
[0]._extent
[0] and keycode 
== wx
.WXK_LEFT
) 
3251                 or (sel_to 
== self
._masklength 
and keycode 
== wx
.WXK_RIGHT
) ): 
3252                 if not wx
.Validator_IsSilent(): 
3255                 # treat arrows as normal, allowing selection 
3257 ##                dbg('using base event processing') 
3260         keep_processing 
= False 
3262         return keep_processing
 
3265     def _OnCtrl_S(self
, event
): 
3266         """ Default Ctrl-S handler; prints value information if demo enabled. """ 
3267 ##        dbg("MaskedEditMixin::_OnCtrl_S") 
3269             print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self
.GetValue(), self
.GetPlainValue()) 
3270             print "Valid? => " + str(self
.IsValid()) 
3271             print "Current field, start, end, value =", str( self
._FindFieldExtent
(getslice
=True)) 
3275     def _OnCtrl_X(self
, event
=None): 
3276         """ Handles ctrl-x keypress in control and Cut operation on context menu. 
3277             Should return False to skip other processing. """ 
3278 ##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1) 
3283     def _OnCtrl_C(self
, event
=None): 
3284         """ Handles ctrl-C keypress in control and Copy operation on context menu. 
3285             Uses base control handling. Should return False to skip other processing.""" 
3289     def _OnCtrl_V(self
, event
=None): 
3290         """ Handles ctrl-V keypress in control and Paste operation on context menu. 
3291             Should return False to skip other processing. """ 
3292 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1) 
3297     def _OnInsert(self
, event
=None): 
3298         """ Handles shift-insert and control-insert operations (paste and copy, respectively)""" 
3299 ##        dbg("MaskedEditMixin::_OnInsert", indent=1) 
3300         if event 
and isinstance(event
, wx
.KeyEvent
): 
3301             if event
.ShiftDown(): 
3303             elif event
.ControlDown(): 
3310     def _OnDelete(self
, event
=None): 
3311         """ Handles shift-delete and delete operations (cut and erase, respectively)""" 
3312 ##        dbg("MaskedEditMixin::_OnDelete", indent=1) 
3313         if event 
and isinstance(event
, wx
.KeyEvent
): 
3314             if event
.ShiftDown(): 
3317                 self
._OnErase
(event
) 
3319             self
._OnErase
(event
) 
3323     def _OnCtrl_Z(self
, event
=None): 
3324         """ Handles ctrl-Z keypress in control and Undo operation on context menu. 
3325             Should return False to skip other processing. """ 
3326 ##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) 
3331     def _OnCtrl_A(self
,event
=None): 
3332         """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ 
3333         end 
= self
._goEnd
(getPosOnly
=True) 
3334         if not event 
or (isinstance(event
, wx
.KeyEvent
) and event
.ShiftDown()): 
3335             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3336             wx
.CallAfter(self
._SetSelection
, 0, self
._masklength
) 
3338             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3339             wx
.CallAfter(self
._SetSelection
, 0, end
) 
3343     def _OnErase(self
, event
=None, just_return_value
=False): 
3344         """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" 
3345 ##        dbg("MaskedEditMixin::_OnErase", indent=1) 
3346         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
3348         if event 
is None:   # called as action routine from Cut() operation. 
3351             key 
= event
.GetKeyCode() 
3353         field 
= self
._FindField
(sel_to
) 
3354         start
, end 
= field
._extent
 
3355         value 
= self
._GetValue
() 
3356         oldstart 
= sel_start
 
3358         # If trying to erase beyond "legal" bounds, disallow operation: 
3359         if( (sel_to 
== 0 and key 
== wx
.WXK_BACK
) 
3360             or (self
._signOk 
and sel_to 
== 1 and value
[0] == ' ' and key 
== wx
.WXK_BACK
) 
3361             or (sel_to 
== self
._masklength 
and sel_start 
== sel_to 
and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) 
3362             or (self
._signOk 
and self
._useParens
 
3363                 and sel_start 
== sel_to
 
3364                 and sel_to 
== self
._masklength 
- 1 
3365                 and value
[sel_to
] == ' ' and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) ): 
3366             if not wx
.Validator_IsSilent(): 
3372         if( field
._insertRight                                  
# an insert-right field 
3373             and value
[start
:end
] != self
._template
[start
:end
]   # and field not empty 
3374             and sel_start 
>= start                              
# and selection starts in field 
3375             and ((sel_to 
== sel_start                           
# and no selection 
3376                   and sel_to 
== end                             
# and cursor at right edge 
3377                   and key 
in (wx
.WXK_BACK
, wx
.WXK_DELETE
))            # and either delete or backspace key 
3379                  (key 
== wx
.WXK_BACK                               
# backspacing 
3380                     and (sel_to 
== end                          
# and selection ends at right edge 
3381                          or sel_to 
< end 
and field
._allowInsert
)) ) ):  # or allow right insert at any point in field 
3383 ##            dbg('delete left') 
3384             # if backspace but left of cursor is empty, adjust cursor right before deleting 
3385             while( key 
== wx
.WXK_BACK
 
3386                    and sel_start 
== sel_to
 
3388                    and value
[start
:sel_start
] == self
._template
[start
:sel_start
]): 
3392 ##            dbg('sel_start, start:', sel_start, start) 
3394             if sel_start 
== sel_to
: 
3398             newfield 
= value
[start
:keep
] + value
[sel_to
:end
] 
3400             # handle sign char moving from outside field into the field: 
3401             move_sign_into_field 
= False 
3402             if not field
._padZero 
and self
._signOk 
and self
._isNeg 
and value
[0] in ('-', '('): 
3404                 newfield 
= signchar 
+ newfield
 
3405                 move_sign_into_field 
= True 
3406 ##            dbg('cut newfield: "%s"' % newfield) 
3408             # handle what should fill in from the left: 
3410             for i 
in range(start
, end 
- len(newfield
)): 
3413                 elif( self
._signOk 
and self
._isNeg 
and i 
== 1 
3414                       and ((self
._useParens 
and newfield
.find('(') == -1) 
3415                            or (not self
._useParens 
and newfield
.find('-') == -1)) ): 
3418                     left 
+= self
._template
[i
]   # this can produce strange results in combination with default values... 
3419             newfield 
= left 
+ newfield
 
3420 ##            dbg('filled newfield: "%s"' % newfield) 
3422             newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3424             # (handle sign located in "mask position" in front of field prior to delete) 
3425             if move_sign_into_field
: 
3426                 newstr 
= ' ' + newstr
[1:] 
3429             # handle erasure of (left) sign, moving selection accordingly... 
3430             if self
._signOk 
and sel_start 
== 0: 
3431                 newstr 
= value 
= ' ' + value
[1:] 
3434             if field
._allowInsert 
and sel_start 
>= start
: 
3435                 # selection (if any) falls within current insert-capable field: 
3436                 select_len 
= sel_to 
- sel_start
 
3437                 # determine where cursor should end up: 
3438                 if key 
== wx
.WXK_BACK
: 
3440                         newpos 
= sel_start 
-1 
3446                     if sel_to 
== sel_start
: 
3447                         erase_to 
= sel_to 
+ 1 
3451                 if self
._isTemplateChar
(newpos
) and select_len 
== 0: 
3453                         if value
[newpos
] in ('(', '-'): 
3454                             newpos 
+= 1     # don't move cusor 
3455                             newstr 
= ' ' + value
[newpos
:] 
3456                         elif value
[newpos
] == ')': 
3457                             # erase right sign, but don't move cursor; (matching left sign handled later) 
3458                             newstr 
= value
[:newpos
] + ' ' 
3460                             # no deletion; just move cursor 
3463                         # no deletion; just move cursor 
3466                     if erase_to 
> end
: erase_to 
= end
 
3467                     erase_len 
= erase_to 
- newpos
 
3469                     left 
= value
[start
:newpos
] 
3470 ##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) 
3471                     right 
= value
[erase_to
:end
] + self
._template
[end
-erase_len
:end
] 
3473                     if field
._alignRight
: 
3474                         rstripped 
= right
.rstrip() 
3475                         if rstripped 
!= right
: 
3476                             pos_adjust 
= len(right
) - len(rstripped
) 
3479                     if not field
._insertRight 
and value
[-1] == ')' and end 
== self
._masklength 
- 1: 
3480                         # need to shift ) into the field: 
3481                         right 
= right
[:-1] + ')' 
3482                         value 
= value
[:-1] + ' ' 
3484                     newfield 
= left
+right
 
3486                         newfield 
= newfield
.rjust(end
-start
) 
3487                         newpos 
+= pos_adjust
 
3488 ##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) 
3489                     newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3494                 if sel_start 
== sel_to
: 
3495 ##                    dbg("current sel_start, sel_to:", sel_start, sel_to) 
3496                     if key 
== wx
.WXK_BACK
: 
3497                         sel_start
, sel_to 
= sel_to
-1, sel_to
-1 
3498 ##                        dbg("new sel_start, sel_to:", sel_start, sel_to) 
3500                     if field
._padZero 
and not value
[start
:sel_to
].replace('0', '').replace(' ','').replace(field
._fillChar
, ''): 
3501                         # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: 
3504                         newchar 
= self
._template
[sel_to
] ## get an original template character to "clear" the current char 
3505 ##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) 
3507                     if self
._isTemplateChar
(sel_to
): 
3508                         if sel_to 
== 0 and self
._signOk 
and value
[sel_to
] == '-':   # erasing "template" sign char 
3509                             newstr 
= ' ' + value
[1:] 
3511                         elif self
._signOk 
and self
._useParens 
and (value
[sel_to
] == ')' or value
[sel_to
] == '('): 
3512                             # allow "change sign" by removing both parens: 
3513                             newstr 
= value
[:self
._signpos
] + ' ' + value
[self
._signpos
+1:-1] + ' ' 
3518                         if field
._insertRight 
and sel_start 
== sel_to
: 
3519                             # force non-insert-right behavior, by selecting char to be replaced: 
3521                         newstr
, ignore 
= self
._insertKey
(newchar
, sel_start
, sel_start
, sel_to
, value
) 
3525                     newstr 
= self
._eraseSelection
(value
, sel_start
, sel_to
) 
3527                 pos 
= sel_start  
# put cursor back at beginning of selection 
3529         if self
._signOk 
and self
._useParens
: 
3530             # account for resultant unbalanced parentheses: 
3531             left_signpos 
= newstr
.find('(') 
3532             right_signpos 
= newstr
.find(')') 
3534             if left_signpos 
== -1 and right_signpos 
!= -1: 
3535                 # erased left-sign marker; get rid of right sign marker: 
3536                 newstr 
= newstr
[:right_signpos
] + ' ' + newstr
[right_signpos
+1:] 
3538             elif left_signpos 
!= -1 and right_signpos 
== -1: 
3539                 # erased right-sign marker; get rid of left-sign marker: 
3540                 newstr 
= newstr
[:left_signpos
] + ' ' + newstr
[left_signpos
+1:] 
3542 ##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) 
3543 ##        dbg("newstr:'%s'" % newstr, 'pos:', pos) 
3545         # if erasure results in an invalid field, disallow it: 
3546 ##        dbg('field._validRequired?', field._validRequired) 
3547 ##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) 
3548         if field
._validRequired 
and not field
.IsValid(newstr
[start
:end
]): 
3549             if not wx
.Validator_IsSilent(): 
3554         # if erasure results in an invalid value, disallow it: 
3555         if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3556             if not wx
.Validator_IsSilent(): 
3561         if just_return_value
: 
3566 ##        dbg('setting value (later) to', newstr) 
3567         wx
.CallAfter(self
._SetValue
, newstr
) 
3568 ##        dbg('setting insertion point (later) to', pos) 
3569         wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3572             self
.modified 
= True 
3576     def _OnEnd(self
,event
): 
3577         """ Handles End keypress in control. Should return False to skip other processing. """ 
3578 ##        dbg("MaskedEditMixin::_OnEnd", indent=1) 
3579         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3580         if not event
.ControlDown(): 
3581             end 
= self
._masklength  
# go to end of control 
3582             if self
._signOk 
and self
._useParens
: 
3583                 end 
= end 
- 1       # account for reserved char at end 
3585             end_of_input 
= self
._goEnd
(getPosOnly
=True) 
3586             sel_start
, sel_to 
= self
._GetSelection
() 
3587             if sel_to 
< pos
: sel_to 
= pos
 
3588             field 
= self
._FindField
(sel_to
) 
3589             field_end 
= self
._FindField
(end_of_input
) 
3591             # pick different end point if either: 
3592             # - cursor not in same field 
3593             # - or at or past last input already 
3594             # - or current selection = end of current field: 
3595 ####            dbg('field != field_end?', field != field_end) 
3596 ####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input) 
3597             if field 
!= field_end 
or sel_to 
>= end_of_input
: 
3598                 edit_start
, edit_end 
= field
._extent
 
3599 ####                dbg('edit_end:', edit_end) 
3600 ####                dbg('sel_to:', sel_to) 
3601 ####                dbg('sel_to == edit_end?', sel_to == edit_end) 
3602 ####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) 
3604                 if sel_to 
== edit_end 
and field
._index 
< self
._field
_indices
[-1]: 
3605                     edit_start
, edit_end 
= self
._FindFieldExtent
(self
._findNextEntry
(edit_end
))  # go to end of next field: 
3607 ##                    dbg('end moved to', end) 
3609                 elif sel_to 
== edit_end 
and field
._index 
== self
._field
_indices
[-1]: 
3610                     # already at edit end of last field; select to end of control: 
3611                     end 
= self
._masklength
 
3612 ##                    dbg('end moved to', end) 
3614                     end 
= edit_end  
# select to end of current field 
3615 ##                    dbg('end moved to ', end) 
3617                 # select to current end of input 
3621 ####        dbg('pos:', pos, 'end:', end) 
3623         if event
.ShiftDown(): 
3624             if not event
.ControlDown(): 
3625 ##                dbg("shift-end; select to end of control") 
3628 ##                dbg("shift-ctrl-end; select to end of non-whitespace") 
3630             wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3631             wx
.CallAfter(self
._SetSelection
, pos
, end
) 
3633             if not event
.ControlDown(): 
3634 ##                dbg('go to end of control:') 
3636             wx
.CallAfter(self
._SetInsertionPoint
, end
) 
3637             wx
.CallAfter(self
._SetSelection
, end
, end
) 
3643     def _OnReturn(self
, event
): 
3645          Swallows the return, issues a Navigate event instead, since 
3646          masked controls are "single line" by defn. 
3648 ##         dbg('MaskedEditMixin::OnReturn') 
3653     def _OnHome(self
,event
): 
3654         """ Handles Home keypress in control. Should return False to skip other processing.""" 
3655 ##        dbg("MaskedEditMixin::_OnHome", indent=1) 
3656         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3657         sel_start
, sel_to 
= self
._GetSelection
() 
3659         # There are 5 cases here: 
3661         # 1) shift: select from start of control to end of current 
3663         if event
.ShiftDown() and not event
.ControlDown(): 
3664 ##            dbg("shift-home; select to start of control") 
3668         # 2) no shift, no control: move cursor to beginning of control. 
3669         elif not event
.ControlDown(): 
3670 ##            dbg("home; move to start of control") 
3674         # 3) No shift, control: move cursor back to beginning of field; if 
3675         #    there already, go to beginning of previous field. 
3676         # 4) shift, control, start of selection not at beginning of control: 
3677         #    move sel_start back to start of field; if already there, go to 
3678         #    start of previous field. 
3679         elif( event
.ControlDown() 
3680               and (not event
.ShiftDown() 
3681                    or (event
.ShiftDown() and sel_start 
> 0) ) ): 
3682             if len(self
._field
_indices
) > 1: 
3683                 field 
= self
._FindField
(sel_start
) 
3684                 start
, ignore 
= field
._extent
 
3685                 if sel_start 
== start 
and field
._index 
!= self
._field
_indices
[0]:  # go to start of previous field: 
3686                     start
, ignore 
= self
._FindFieldExtent
(sel_start
-1) 
3687                 elif sel_start 
== start
: 
3688                     start 
= 0   # go to literal beginning if edit start 
3695             if not event
.ShiftDown(): 
3696 ##                dbg("ctrl-home; move to beginning of field") 
3699 ##                dbg("shift-ctrl-home; select to beginning of field") 
3703         # 5) shift, control, start of selection at beginning of control: 
3704         #    unselect by moving sel_to backward to beginning of current field; 
3705         #    if already there, move to start of previous field. 
3707             if len(self
._field
_indices
) > 1: 
3708                 # find end of previous field: 
3709                 field 
= self
._FindField
(sel_to
) 
3710                 if sel_to 
> start 
and field
._index 
!= self
._field
_indices
[0]: 
3711                     ignore
, end 
= self
._FindFieldExtent
(field
._extent
[0]-1) 
3717                 end_of_field 
= False 
3718 ##            dbg("shift-ctrl-home; unselect to beginning of field") 
3720 ##        dbg('queuing new sel_start, sel_to:', (start, end)) 
3721         wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3722         wx
.CallAfter(self
._SetSelection
, start
, end
) 
3727     def _OnChangeField(self
, event
): 
3729         Primarily handles TAB events, but can be used for any key that 
3730         designer wants to change fields within a masked edit control. 
3732 ##        dbg('MaskedEditMixin::_OnChangeField', indent = 1) 
3733         # determine end of current field: 
3734         pos 
= self
._GetInsertionPoint
() 
3735 ##        dbg('current pos:', pos) 
3736         sel_start
, sel_to 
= self
._GetSelection
() 
3738         if self
._masklength 
< 0:   # no fields; process tab normally 
3739             self
._AdjustField
(pos
) 
3740             if event
.GetKeyCode() == wx
.WXK_TAB
: 
3741 ##                dbg('tab to next ctrl') 
3742                 # As of 2.5.2, you don't call event.Skip() to do 
3743                 # this, but instead force explicit navigation, if 
3744                 # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3751         if event
.ShiftDown(): 
3755             # NOTE: doesn't yet work with SHIFT-tab under wx; the control 
3756             # never sees this event! (But I've coded for it should it ever work, 
3757             # and it *does* work for '.' in IpAddrCtrl.) 
3758             field 
= self
._FindField
(pos
) 
3759             index 
= field
._index
 
3760             field_start 
= field
._extent
[0] 
3761             if pos 
< field_start
: 
3762 ##                dbg('cursor before 1st field; cannot change to a previous field') 
3763                 if not wx
.Validator_IsSilent(): 
3767             if event
.ControlDown(): 
3768 ##                dbg('queuing select to beginning of field:', field_start, pos) 
3769                 wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3770                 wx
.CallAfter(self
._SetSelection
, field_start
, pos
) 
3775                   # We're already in the 1st field; process shift-tab normally: 
3776                 self
._AdjustField
(pos
) 
3777                 if event
.GetKeyCode() == wx
.WXK_TAB
: 
3778 ##                    dbg('tab to previous ctrl') 
3779                     # As of 2.5.2, you don't call event.Skip() to do 
3780                     # this, but instead force explicit navigation, if 
3781                     # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3782                     self
.Navigate(False) 
3784 ##                    dbg('position at beginning') 
3785                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3789                 # find beginning of previous field: 
3790                 begin_prev 
= self
._FindField
(field_start
-1)._extent
[0] 
3791                 self
._AdjustField
(pos
) 
3792 ##                dbg('repositioning to', begin_prev) 
3793                 wx
.CallAfter(self
._SetInsertionPoint
, begin_prev
) 
3794                 if self
._FindField
(begin_prev
)._selectOnFieldEntry
: 
3795                     edit_start
, edit_end 
= self
._FindFieldExtent
(begin_prev
) 
3796 ##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) 
3797                     wx
.CallAfter(self
._SetInsertionPoint
, edit_start
) 
3798                     wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3804             field 
= self
._FindField
(sel_to
) 
3805             field_start
, field_end 
= field
._extent
 
3806             if event
.ControlDown(): 
3807 ##                dbg('queuing select to end of field:', pos, field_end) 
3808                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3809                 wx
.CallAfter(self
._SetSelection
, pos
, field_end
) 
3813                 if pos 
< field_start
: 
3814 ##                    dbg('cursor before 1st field; go to start of field') 
3815                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3816                     if field
._selectOnFieldEntry
: 
3817                         wx
.CallAfter(self
._SetSelection
, field_start
, field_end
) 
3819                         wx
.CallAfter(self
._SetSelection
, field_start
, field_start
) 
3822 ##                dbg('end of current field:', field_end) 
3823 ##                dbg('go to next field') 
3824                 if field_end 
== self
._fields
[self
._field
_indices
[-1]]._extent
[1]: 
3825                     self
._AdjustField
(pos
) 
3826                     if event
.GetKeyCode() == wx
.WXK_TAB
: 
3827 ##                        dbg('tab to next ctrl') 
3828                         # As of 2.5.2, you don't call event.Skip() to do 
3829                         # this, but instead force explicit navigation, if 
3830                         # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3833 ##                        dbg('position at end') 
3834                         wx
.CallAfter(self
._SetInsertionPoint
, field_end
) 
3838                     # we have to find the start of the next field 
3839                     next_pos 
= self
._findNextEntry
(field_end
) 
3840                     if next_pos 
== field_end
: 
3841 ##                        dbg('already in last field') 
3842                         self
._AdjustField
(pos
) 
3843                         if event
.GetKeyCode() == wx
.WXK_TAB
: 
3844 ##                            dbg('tab to next ctrl') 
3845                             # As of 2.5.2, you don't call event.Skip() to do 
3846                             # this, but instead force explicit navigation, if 
3847                             # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3853                         self
._AdjustField
( pos 
) 
3855                         # move cursor to appropriate point in the next field and select as necessary: 
3856                         field 
= self
._FindField
(next_pos
) 
3857                         edit_start
, edit_end 
= field
._extent
 
3858                         if field
._selectOnFieldEntry
: 
3859 ##                            dbg('move to ', next_pos) 
3860                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3861                             edit_start
, edit_end 
= self
._FindFieldExtent
(next_pos
) 
3862 ##                            dbg('queuing select', edit_start, edit_end) 
3863                             wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3865                             if field
._insertRight
: 
3866                                 next_pos 
= field
._extent
[1] 
3867 ##                            dbg('move to ', next_pos) 
3868                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3873     def _OnDecimalPoint(self
, event
): 
3874 ##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) 
3876         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3878         if self
._isFloat
:       ## handle float value, move to decimal place 
3879 ##            dbg('key == Decimal tab; decimal pos:', self._decimalpos) 
3880             value 
= self
._GetValue
() 
3881             if pos 
< self
._decimalpos
: 
3882                 clipped_text 
= value
[0:pos
] + self
._decimalChar 
+ value
[self
._decimalpos
+1:] 
3883 ##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3884                 newstr 
= self
._adjustFloat
(clipped_text
) 
3886                 newstr 
= self
._adjustFloat
(value
) 
3887             wx
.CallAfter(self
._SetValue
, newstr
) 
3888             fraction 
= self
._fields
[1] 
3889             start
, end 
= fraction
._extent
 
3890             wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3891             if fraction
._selectOnFieldEntry
: 
3892 ##                dbg('queuing selection after decimal point to:', (start, end)) 
3893                 wx
.CallAfter(self
._SetSelection
, start
, end
) 
3895                 wx
.CallAfter(self
._SetSelection
, start
, start
) 
3896             keep_processing 
= False 
3898         if self
._isInt
:      ## handle integer value, truncate from current position 
3899 ##            dbg('key == Integer decimal event') 
3900             value 
= self
._GetValue
() 
3901             clipped_text 
= value
[0:pos
] 
3902 ##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3903             newstr 
= self
._adjustInt
(clipped_text
) 
3904 ##            dbg('newstr: "%s"' % newstr) 
3905             wx
.CallAfter(self
._SetValue
, newstr
) 
3906             newpos 
= len(newstr
.rstrip()) 
3907             if newstr
.find(')') != -1: 
3908                 newpos 
-= 1     # (don't move past right paren) 
3909             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3910             wx
.CallAfter(self
._SetSelection
, newpos
, newpos
) 
3911             keep_processing 
= False 
3915     def _OnChangeSign(self
, event
): 
3916 ##        dbg('MaskedEditMixin::_OnChangeSign', indent=1) 
3917         key 
= event
.GetKeyCode() 
3918         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), key
) 
3919         value 
= self
._eraseSelection
() 
3920         integer 
= self
._fields
[0] 
3921         start
, end 
= integer
._extent
 
3922         sel_start
, sel_to 
= self
._GetSelection
() 
3924 ####        dbg('adjusted pos:', pos) 
3925         if chr(key
) in ('-','+','(', ')') or (chr(key
) == " " and pos 
== self
._signpos
): 
3926             cursign 
= self
._isNeg
 
3927 ##            dbg('cursign:', cursign) 
3928             if chr(key
) in ('-','(', ')'): 
3929                 if sel_start 
<= self
._signpos
: 
3932                     self
._isNeg 
= (not self
._isNeg
)   ## flip value 
3935 ##            dbg('isNeg?', self._isNeg) 
3937             text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
(candidate
=value
) 
3938 ##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) 
3942             if self
._isNeg 
and self
._signpos 
is not None and self
._signpos 
!= -1: 
3943                 if self
._useParens 
and self
._right
_signpos 
is not None: 
3944                     text 
= text
[:self
._signpos
] + '(' + text
[self
._signpos
+1:self
._right
_signpos
] + ')' + text
[self
._right
_signpos
+1:] 
3946                     text 
= text
[:self
._signpos
] + '-' + text
[self
._signpos
+1:] 
3948 ####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) 
3950                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:self
._right
_signpos
] + ' ' + text
[self
._right
_signpos
+1:] 
3952                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:] 
3953 ##                dbg('clearing self._isNeg') 
3956             wx
.CallAfter(self
._SetValue
, text
) 
3957             wx
.CallAfter(self
._applyFormatting
) 
3958 ##            dbg('pos:', pos, 'signpos:', self._signpos) 
3959             if pos 
== self
._signpos 
or integer
.IsEmpty(text
[start
:end
]): 
3960                 wx
.CallAfter(self
._SetInsertionPoint
, self
._signpos
+1) 
3962                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3964             keep_processing 
= False 
3966             keep_processing 
= True 
3968         return keep_processing
 
3971     def _OnGroupChar(self
, event
): 
3973         This handler is only registered if the mask is a numeric mask. 
3974         It allows the insertion of ',' or '.' if appropriate. 
3976 ##        dbg('MaskedEditMixin::_OnGroupChar', indent=1) 
3977         keep_processing 
= True 
3978         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3979         sel_start
, sel_to 
= self
._GetSelection
() 
3980         groupchar 
= self
._fields
[0]._groupChar
 
3981         if not self
._isCharAllowed
(groupchar
, pos
, checkRegex
=True): 
3982             keep_processing 
= False 
3983             if not wx
.Validator_IsSilent(): 
3987             newstr
, newpos 
= self
._insertKey
(groupchar
, pos
, sel_start
, sel_to
, self
._GetValue
() ) 
3988 ##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) 
3989             if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3990                 keep_processing 
= False 
3991                 if not wx
.Validator_IsSilent(): 
3995             wx
.CallAfter(self
._SetValue
, newstr
) 
3996             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3997         keep_processing 
= False 
3999         return keep_processing
 
4002     def _findNextEntry(self
,pos
, adjustInsert
=True): 
4003         """ Find the insertion point for the next valid entry character position.""" 
4004 ##        dbg('MaskedEditMixin::_findNextEntry', indent=1)         
4005         if self
._isTemplateChar
(pos
) or pos 
in self
._explicit
_field
_boundaries
:   # if changing fields, pay attn to flag 
4006             adjustInsert 
= adjustInsert
 
4007         else:                           # else within a field; flag not relevant 
4008             adjustInsert 
= False 
4010         while self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
4013         # if changing fields, and we've been told to adjust insert point, 
4014         # look at new field; if empty and right-insert field, 
4015         # adjust to right edge: 
4016         if adjustInsert 
and pos 
< self
._masklength
: 
4017             field 
= self
._FindField
(pos
) 
4018             start
, end 
= field
._extent
 
4019             slice = self
._GetValue
()[start
:end
] 
4020             if field
._insertRight 
and field
.IsEmpty(slice): 
4022 ##        dbg('final pos:', pos, indent=0) 
4026     def _findNextTemplateChar(self
, pos
): 
4027         """ Find the position of the next non-editable character in the mask.""" 
4028         while not self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
4033     def _OnAutoCompleteField(self
, event
): 
4034 ##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) 
4035         pos 
= self
._GetInsertionPoint
() 
4036         field 
= self
._FindField
(pos
) 
4037         edit_start
, edit_end
, slice = self
._FindFieldExtent
(pos
, getslice
=True) 
4040         keycode 
= event
.GetKeyCode() 
4042         if field
._fillChar 
!= ' ': 
4043             text 
= slice.replace(field
._fillChar
, '') 
4047         keep_processing 
= True  # (assume True to start) 
4048 ##        dbg('field._hasList?', field._hasList) 
4050 ##            dbg('choices:', field._choices) 
4051 ##            dbg('compareChoices:', field._compareChoices) 
4052             choices
, choice_required 
= field
._compareChoices
, field
._choiceRequired
 
4053             if keycode 
in (wx
.WXK_PRIOR
, wx
.WXK_UP
): 
4057             match_index
, partial_match 
= self
._autoComplete
(direction
, choices
, text
, compareNoCase
=field
._compareNoCase
, current_index 
= field
._autoCompleteIndex
) 
4058             if( match_index 
is None 
4059                 and (keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
4060                      or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown() ) ) ): 
4061                 # Select the 1st thing from the list: 
4064             if( match_index 
is not None 
4065                 and ( keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
4066                       or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown()) 
4067                       or (keycode 
== wx
.WXK_DOWN 
and partial_match
) ) ): 
4069                 # We're allowed to auto-complete: 
4070 ##                dbg('match found') 
4071                 value 
= self
._GetValue
() 
4072                 newvalue 
= value
[:edit_start
] + field
._choices
[match_index
] + value
[edit_end
:] 
4073 ##                dbg('setting value to "%s"' % newvalue) 
4074                 self
._SetValue
(newvalue
) 
4075                 self
._SetInsertionPoint
(min(edit_end
, len(newvalue
.rstrip()))) 
4076                 self
._OnAutoSelect
(field
, match_index
) 
4077                 self
._CheckValid
()  # recolor as appopriate 
4080         if keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
): 
4081             # treat as left right arrow if unshifted, tab/shift tab if shifted. 
4082             if event
.ShiftDown(): 
4083                 if keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
): 
4084                     # remove "shifting" and treat as (forward) tab: 
4085                     event
.m_shiftDown 
= False 
4086                 keep_processing 
= self
._OnChangeField
(event
) 
4088                 keep_processing 
= self
._OnArrow
(event
) 
4089         # else some other key; keep processing the key 
4091 ##        dbg('keep processing?', keep_processing, indent=0) 
4092         return keep_processing
 
4095     def _OnAutoSelect(self
, field
, match_index 
= None): 
4097         Function called if autoselect feature is enabled and entire control 
4100 ##        dbg('MaskedEditMixin::OnAutoSelect', field._index) 
4101         if match_index 
is not None: 
4102             field
._autoCompleteIndex 
= match_index
 
4105     def _autoComplete(self
, direction
, choices
, value
, compareNoCase
, current_index
): 
4107         This function gets called in response to Auto-complete events. 
4108         It attempts to find a match to the specified value against the 
4109         list of choices; if exact match, the index of then next 
4110         appropriate value in the list, based on the given direction. 
4111         If not an exact match, it will return the index of the 1st value from 
4112         the choice list for which the partial value can be extended to match. 
4113         If no match found, it will return None. 
4114         The function returns a 2-tuple, with the 2nd element being a boolean 
4115         that indicates if partial match was necessary. 
4117 ##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) 
4119 ##            dbg('nothing to match against', indent=0) 
4120             return (None, False) 
4122         partial_match 
= False 
4125             value 
= value
.lower() 
4127         last_index 
= len(choices
) - 1 
4128         if value 
in choices
: 
4129 ##            dbg('"%s" in', choices) 
4130             if current_index 
is not None and choices
[current_index
] == value
: 
4131                 index 
= current_index
 
4133                 index 
= choices
.index(value
) 
4135 ##            dbg('matched "%s" (%d)' % (choices[index], index)) 
4137 ##                dbg('going to previous') 
4138                 if index 
== 0: index 
= len(choices
) - 1 
4141                 if index 
== len(choices
) - 1: index 
= 0 
4143 ##            dbg('change value to "%s" (%d)' % (choices[index], index)) 
4146             partial_match 
= True 
4147             value 
= value
.strip() 
4148 ##            dbg('no match; try to auto-complete:') 
4150 ##            dbg('searching for "%s"' % value) 
4151             if current_index 
is None: 
4152                 indices 
= range(len(choices
)) 
4157                     indices 
= range(current_index 
+1, len(choices
)) + range(current_index
+1) 
4158 ##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) 
4160                     indices 
= range(current_index
-1, -1, -1) + range(len(choices
)-1, current_index
-1, -1) 
4161 ##                    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) 
4162 ####            dbg('indices:', indices) 
4163             for index 
in indices
: 
4164                 choice 
= choices
[index
] 
4165                 if choice
.find(value
, 0) == 0: 
4166 ##                    dbg('match found:', choice) 
4170 ##                    dbg('choice: "%s" - no match' % choice) 
4172             if match 
is not None: 
4173 ##                dbg('matched', match) 
4176 ##                dbg('no match found') 
4179         return (match
, partial_match
) 
4182     def _AdjustField(self
, pos
): 
4184         This function gets called by default whenever the cursor leaves a field. 
4185         The pos argument given is the char position before leaving that field. 
4186         By default, floating point, integer and date values are adjusted to be 
4187         legal in this function.  Derived classes may override this function 
4188         to modify the value of the control in a different way when changing fields. 
4190         NOTE: these change the value immediately, and restore the cursor to 
4191         the passed location, so that any subsequent code can then move it 
4192         based on the operation being performed. 
4194         newvalue 
= value 
= self
._GetValue
() 
4195         field 
= self
._FindField
(pos
) 
4196         start
, end
, slice = self
._FindFieldExtent
(getslice
=True) 
4197         newfield 
= field
._AdjustField
(slice) 
4198         newvalue 
= value
[:start
] + newfield 
+ value
[end
:] 
4200         if self
._isFloat 
and newvalue 
!= self
._template
: 
4201             newvalue 
= self
._adjustFloat
(newvalue
) 
4203         if self
._ctrl
_constraints
._isInt 
and value 
!= self
._template
: 
4204             newvalue 
= self
._adjustInt
(value
) 
4206         if self
._isDate 
and value 
!= self
._template
: 
4207             newvalue 
= self
._adjustDate
(value
, fixcentury
=True) 
4208             if self
._4digityear
: 
4209                 year2dig 
= self
._dateExtent 
- 2 
4210                 if pos 
== year2dig 
and value
[year2dig
] != newvalue
[year2dig
]: 
4213         if newvalue 
!= value
: 
4214 ##            dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue)) 
4215             self
._SetValue
(newvalue
) 
4216             self
._SetInsertionPoint
(pos
) 
4219     def _adjustKey(self
, pos
, key
): 
4220         """ Apply control formatting to the key (e.g. convert to upper etc). """ 
4221         field 
= self
._FindField
(pos
) 
4222         if field
._forceupper 
and key 
in range(97,123): 
4223             key 
= ord( chr(key
).upper()) 
4225         if field
._forcelower 
and key 
in range(97,123): 
4226             key 
= ord( chr(key
).lower()) 
4231     def _adjustPos(self
, pos
, key
): 
4233         Checks the current insertion point position and adjusts it if 
4234         necessary to skip over non-editable characters. 
4236 ##        dbg('_adjustPos', pos, key, indent=1) 
4237         sel_start
, sel_to 
= self
._GetSelection
() 
4238         # If a numeric or decimal mask, and negatives allowed, reserve the 
4239         # first space for sign, and last one if using parens. 
4241             and ((pos 
== self
._signpos 
and key 
in (ord('-'), ord('+'), ord(' ')) ) 
4242                  or (self
._useParens 
and pos 
== self
._masklength 
-1))): 
4243 ##            dbg('adjusted pos:', pos, indent=0) 
4246         if key 
not in self
._nav
: 
4247             field 
= self
._FindField
(pos
) 
4249 ##            dbg('field._insertRight?', field._insertRight) 
4250 ##            if self._signOk: dbg('self._signpos:', self._signpos) 
4251             if field
._insertRight
:              # if allow right-insert 
4252                 start
, end 
= field
._extent
 
4253                 slice = self
._GetValue
()[start
:end
].strip() 
4254                 field_len 
= end 
- start
 
4255                 if pos 
== end
:                      # if cursor at right edge of field 
4256                     # if not filled or supposed to stay in field, keep current position 
4257 ####                    dbg('pos==end') 
4258 ####                    dbg('len (slice):', len(slice)) 
4259 ####                    dbg('field_len?', field_len) 
4260 ####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) 
4261 ####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) 
4262                     if len(slice) == field_len 
and field
._moveOnFieldFull
: 
4263                         # move cursor to next field: 
4264                         pos 
= self
._findNextEntry
(pos
) 
4265                         self
._SetInsertionPoint
(pos
) 
4267                             self
._SetSelection
(pos
, sel_to
)     # restore selection 
4269                             self
._SetSelection
(pos
, pos
)        # remove selection 
4270                     else: # leave cursor alone 
4273                     # if at start of control, move to right edge 
4274                     if (sel_to 
== sel_start
 
4275                         and (self
._isTemplateChar
(pos
) or (pos 
== start 
and len(slice)+ 1 < field_len
)) 
4277                         pos 
= end                   
# move to right edge 
4278 ##                    elif sel_start <= start and sel_to == end: 
4279 ##                        # select to right edge of field - 1 (to replace char) 
4281 ##                        self._SetInsertionPoint(pos) 
4282 ##                        # restore selection 
4283 ##                        self._SetSelection(sel_start, pos) 
4285                     # if selected to beginning and signed, and not changing sign explicitly: 
4286                     elif self
._signOk 
and sel_start 
== 0 and key 
not in (ord('-'), ord('+'), ord(' ')): 
4287                         # adjust to past reserved sign position: 
4288                         pos 
= self
._fields
[0]._extent
[0] 
4289 ##                        dbg('adjusting field to ', pos) 
4290                         self
._SetInsertionPoint
(pos
) 
4291                         # but keep original selection, to allow replacement of any sign:  
4292                         self
._SetSelection
(0, sel_to
) 
4294                         pass    # leave position/selection alone 
4296             # else make sure the user is not trying to type over a template character 
4297             # If they are, move them to the next valid entry position 
4298             elif self
._isTemplateChar
(pos
): 
4299                 if( not field
._moveOnFieldFull
 
4300                       and (not self
._signOk
 
4302                                and field
._index 
== 0 
4303                                and pos 
> 0) ) ):      # don't move to next field without explicit cursor movement 
4306                     # find next valid position 
4307                     pos 
= self
._findNextEntry
(pos
) 
4308                     self
._SetInsertionPoint
(pos
) 
4309                     if pos 
< sel_to
:    # restore selection 
4310                         self
._SetSelection
(pos
, sel_to
) 
4312                         self
._SetSelection
(pos
, pos
) 
4313 ##        dbg('adjusted pos:', pos, indent=0) 
4317     def _adjustFloat(self
, candidate
=None): 
4319         'Fixes' an floating point control. Collapses spaces, right-justifies, etc. 
4321 ##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) 
4322         lenInt
,lenFraction  
= [len(s
) for s 
in self
._mask
.split('.')]  ## Get integer, fraction lengths 
4324         if candidate 
is None: value 
= self
._GetValue
() 
4325         else: value 
= candidate
 
4326 ##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) 
4327         intStr
, fracStr 
= value
.split(self
._decimalChar
) 
4329         intStr 
= self
._fields
[0]._AdjustField
(intStr
) 
4330 ##        dbg('adjusted intStr: "%s"' % intStr) 
4331         lenInt 
= len(intStr
) 
4332         fracStr 
= fracStr 
+ ('0'*(lenFraction
-len(fracStr
)))  # add trailing spaces to decimal 
4334 ##        dbg('intStr "%(intStr)s"' % locals()) 
4335 ##        dbg('lenInt:', lenInt) 
4337         intStr 
= string
.rjust( intStr
[-lenInt
:], lenInt
) 
4338 ##        dbg('right-justifed intStr = "%(intStr)s"' % locals()) 
4339         newvalue 
= intStr 
+ self
._decimalChar 
+ fracStr
 
4342             if len(newvalue
) < self
._masklength
: 
4343                 newvalue 
= ' ' + newvalue
 
4344             signedvalue 
= self
._getSignedValue
(newvalue
)[0] 
4345             if signedvalue 
is not None: newvalue 
= signedvalue
 
4347         # Finally, align string with decimal position, left-padding with 
4349         newdecpos 
= newvalue
.find(self
._decimalChar
) 
4350         if newdecpos 
< self
._decimalpos
: 
4351             padlen 
= self
._decimalpos 
- newdecpos
 
4352             newvalue 
= string
.join([' ' * padlen
] + [newvalue
] ,'') 
4354         if self
._signOk 
and self
._useParens
: 
4355             if newvalue
.find('(') != -1: 
4356                 newvalue 
= newvalue
[:-1] + ')' 
4358                 newvalue 
= newvalue
[:-1] + ' ' 
4360 ##        dbg('newvalue = "%s"' % newvalue) 
4361         if candidate 
is None: 
4362             wx
.CallAfter(self
._SetValue
, newvalue
) 
4367     def _adjustInt(self
, candidate
=None): 
4368         """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" 
4369 ##        dbg("MaskedEditMixin::_adjustInt", candidate) 
4370         lenInt 
= self
._masklength
 
4371         if candidate 
is None: value 
= self
._GetValue
() 
4372         else: value 
= candidate
 
4374         intStr 
= self
._fields
[0]._AdjustField
(value
) 
4375         intStr 
= intStr
.strip() # drop extra spaces 
4376 ##        dbg('adjusted field: "%s"' % intStr) 
4378         if self
._isNeg 
and intStr
.find('-') == -1 and intStr
.find('(') == -1: 
4380                 intStr 
= '(' + intStr 
+ ')' 
4382                 intStr 
= '-' + intStr
 
4383         elif self
._isNeg 
and intStr
.find('-') != -1 and self
._useParens
: 
4384             intStr 
= intStr
.replace('-', '(') 
4386         if( self
._signOk 
and ((self
._useParens 
and intStr
.find('(') == -1) 
4387                                 or (not self
._useParens 
and intStr
.find('-') == -1))): 
4388             intStr 
= ' ' + intStr
 
4390                 intStr 
+= ' '   # space for right paren position 
4392         elif self
._signOk 
and self
._useParens 
and intStr
.find('(') != -1 and intStr
.find(')') == -1: 
4393             # ensure closing right paren: 
4396         if self
._fields
[0]._alignRight
:     ## Only if right-alignment is enabled 
4397             intStr 
= intStr
.rjust( lenInt 
) 
4399             intStr 
= intStr
.ljust( lenInt 
) 
4401         if candidate 
is None: 
4402             wx
.CallAfter(self
._SetValue
, intStr 
) 
4406     def _adjustDate(self
, candidate
=None, fixcentury
=False, force4digit_year
=False): 
4408         'Fixes' a date control, expanding the year if it can. 
4409         Applies various self-formatting options. 
4411 ##        dbg("MaskedEditMixin::_adjustDate", indent=1) 
4412         if candidate 
is None: text    
= self
._GetValue
() 
4413         else: text 
= candidate
 
4414 ##        dbg('text=', text) 
4415         if self
._datestyle 
== "YMD": 
4420 ##        dbg('getYear: "%s"' % _getYear(text, self._datestyle)) 
4421         year    
= string
.replace( _getYear( text
, self
._datestyle
),self
._fields
[year_field
]._fillChar
,"")  # drop extra fillChars 
4422         month   
= _getMonth( text
, self
._datestyle
) 
4423         day     
= _getDay( text
, self
._datestyle
) 
4424 ##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) 
4427         yearstart 
= self
._dateExtent 
- 4 
4431                  or (self
._GetInsertionPoint
() > yearstart
+1 and text
[yearstart
+2] == ' ') 
4432                  or (self
._GetInsertionPoint
() > yearstart
+2 and text
[yearstart
+3] == ' ') ) ): 
4433             ## user entered less than four digits and changing fields or past point where we could 
4434             ## enter another digit: 
4438 ##                dbg('bad year=', year) 
4439                 year 
= text
[yearstart
:self
._dateExtent
] 
4441         if len(year
) < 4 and yearVal
: 
4443                 # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the 
4445                 now 
= wx
.DateTime_Now() 
4446                 century 
= (now
.GetYear() /100) * 100        # "this century" 
4447                 twodig_year 
= now
.GetYear() - century       
# "this year" (2 digits) 
4448                 # if separation between today's 2-digit year and typed value > 50, 
4449                 #      assume last century, 
4450                 # else assume this century. 
4452                 # Eg: if 2003 and yearVal == 30, => 2030 
4453                 #     if 2055 and yearVal == 80, => 2080 
4454                 #     if 2010 and yearVal == 96, => 1996 
4456                 if abs(yearVal 
- twodig_year
) > 50: 
4457                     yearVal 
= (century 
- 100) + yearVal
 
4459                     yearVal 
= century 
+ yearVal
 
4460                 year 
= str( yearVal 
) 
4461             else:   # pad with 0's to make a 4-digit year 
4462                 year 
= "%04d" % yearVal
 
4463             if self
._4digityear 
or force4digit_year
: 
4464                 text 
= _makeDate(year
, month
, day
, self
._datestyle
, text
) + text
[self
._dateExtent
:] 
4465 ##        dbg('newdate: "%s"' % text, indent=0) 
4469     def _goEnd(self
, getPosOnly
=False): 
4470         """ Moves the insertion point to the end of user-entry """ 
4471 ##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) 
4472         text 
= self
._GetValue
() 
4473 ####        dbg('text: "%s"' % text) 
4475         if len(text
.rstrip()): 
4476             for i 
in range( min( self
._masklength
-1, len(text
.rstrip())), -1, -1): 
4477 ####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) 
4478                 if self
._isMaskChar
(i
): 
4480 ####                    dbg("text[%d]: '%s'" % (i, char)) 
4486             pos 
= self
._goHome
(getPosOnly
=True) 
4488             pos 
= min(i
,self
._masklength
) 
4490         field 
= self
._FindField
(pos
) 
4491         start
, end 
= field
._extent
 
4492         if field
._insertRight 
and pos 
< end
: 
4494 ##        dbg('next pos:', pos) 
4499             self
._SetInsertionPoint
(pos
) 
4502     def _goHome(self
, getPosOnly
=False): 
4503         """ Moves the insertion point to the beginning of user-entry """ 
4504 ##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) 
4505         text 
= self
._GetValue
() 
4506         for i 
in range(self
._masklength
): 
4507             if self
._isMaskChar
(i
): 
4514             self
._SetInsertionPoint
(max(i
,0)) 
4518     def _getAllowedChars(self
, pos
): 
4519         """ Returns a string of all allowed user input characters for the provided 
4520             mask character plus control options 
4522         maskChar 
= self
.maskdict
[pos
] 
4523         okchars 
= self
.maskchardict
[maskChar
]    ## entry, get mask approved characters 
4525         # convert okchars to unicode if required; will force subsequent appendings to 
4526         # result in unicode strings 
4527         if 'unicode' in wx
.PlatformInfo 
and type(okchars
) != types
.UnicodeType
: 
4528             okchars 
= okchars
.decode(self
._defaultEncoding
) 
4530         field 
= self
._FindField
(pos
) 
4531         if okchars 
and field
._okSpaces
:          ## Allow spaces? 
4533         if okchars 
and field
._includeChars
:      ## any additional included characters? 
4534             okchars 
+= field
._includeChars
 
4535 ####        dbg('okchars[%d]:' % pos, okchars) 
4539     def _isMaskChar(self
, pos
): 
4540         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) 
4542         if pos 
< self
._masklength
: 
4543             return self
._ismasked
[pos
] 
4548     def _isTemplateChar(self
,Pos
): 
4549         """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) 
4551         if Pos 
< self
._masklength
: 
4552             return not self
._isMaskChar
(Pos
) 
4557     def _isCharAllowed(self
, char
, pos
, checkRegex
=False, allowAutoSelect
=True, ignoreInsertRight
=False): 
4558         """ Returns True if character is allowed at the specific position, otherwise False.""" 
4559 ##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1) 
4560         field 
= self
._FindField
(pos
) 
4561         right_insert 
= False 
4563         if self
.controlInitialized
: 
4564             sel_start
, sel_to 
= self
._GetSelection
() 
4566             sel_start
, sel_to 
= pos
, pos
 
4568         if (field
._insertRight 
or self
._ctrl
_constraints
._insertRight
) and not ignoreInsertRight
: 
4569             start
, end 
= field
._extent
 
4570             field_len 
= end 
- start
 
4571             if self
.controlInitialized
: 
4572                 value 
= self
._GetValue
() 
4573                 fstr 
= value
[start
:end
].strip() 
4575                     while fstr 
and fstr
[0] == '0': 
4577                 input_len 
= len(fstr
) 
4578                 if self
._signOk 
and '-' in fstr 
or '(' in fstr
: 
4579                     input_len 
-= 1  # sign can move out of field, so don't consider it in length 
4581                 value 
= self
._template
 
4582                 input_len 
= 0   # can't get the current "value", so use 0 
4585             # if entire field is selected or position is at end and field is not full, 
4586             # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar 
4587             # or the field is a singleton integer field and is currently 0 and we're at the end: 
4588             if( (sel_start
, sel_to
) == field
._extent
 
4589                 or (pos 
== end 
and ((input_len 
< field_len
) 
4591                                          and input_len 
== field_len
 
4593                                          and value
[end
-1] == '0' 
4597 ##                dbg('pos = end - 1 = ', pos, 'right_insert? 1') 
4599             elif( field
._allowInsert 
and sel_start 
== sel_to
 
4600                   and (sel_to 
== end 
or (sel_to 
< self
._masklength 
and value
[sel_start
] != field
._fillChar
)) 
4601                   and input_len 
< field_len 
): 
4602                 pos 
= sel_to 
- 1    # where character will go 
4603 ##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') 
4605             # else leave pos alone... 
4607 ##                dbg('pos stays ', pos, 'right_insert? 0') 
4610         if self
._isTemplateChar
( pos 
):  ## if a template character, return empty 
4611 ##            dbg('%d is a template character; returning False' % pos, indent=0) 
4614         if self
._isMaskChar
( pos 
): 
4615             okChars  
= self
._getAllowedChars
(pos
) 
4617             if self
._fields
[0]._groupdigits 
and (self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
)): 
4618                 okChars 
+= self
._fields
[0]._groupChar
 
4621                 if self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
): 
4625                 elif self
._useParens 
and (self
._isInt 
or (self
._isFloat 
and pos 
> self
._decimalpos
)): 
4628 ####            dbg('%s in %s?' % (char, okChars), char in okChars) 
4629             approved 
= (self
.maskdict
[pos
] == '*' or char 
in okChars
) 
4631             if approved 
and checkRegex
: 
4632 ##                dbg("checking appropriate regex's") 
4633                 value 
= self
._eraseSelection
(self
._GetValue
()) 
4635                     # move the position to the right side of the insertion: 
4640                     newvalue
, ignore
, ignore
, ignore
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
, allowAutoSelect
=True) 
4642                     newvalue
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
) 
4643 ##                dbg('newvalue: "%s"' % newvalue) 
4645                 fields 
= [self
._FindField
(pos
)] + [self
._ctrl
_constraints
] 
4646                 for field 
in fields
:    # includes fields[-1] == "ctrl_constraints" 
4647                     if field
._regexMask 
and field
._filter
: 
4648 ##                        dbg('checking vs. regex') 
4649                         start
, end 
= field
._extent
 
4650                         slice = newvalue
[start
:end
] 
4651                         approved 
= (re
.match( field
._filter
, slice) is not None) 
4652 ##                        dbg('approved?', approved) 
4653                     if not approved
: break 
4657 ##            dbg('%d is a !???! character; returning False', indent=0) 
4661     def _applyFormatting(self
): 
4662         """ Apply formatting depending on the control's state. 
4663             Need to find a way to call this whenever the value changes, in case the control's 
4664             value has been changed or set programatically. 
4667 ##        dbg('MaskedEditMixin::_applyFormatting', indent=1) 
4669         # Handle negative numbers 
4671             text
, signpos
, right_signpos 
= self
._getSignedValue
() 
4672 ##            dbg('text: "%s", signpos:' % text, signpos) 
4673             if text 
and signpos 
!= self
._signpos
: 
4674                 self
._signpos 
= signpos
 
4675             if not text 
or text
[signpos
] not in ('-','('): 
4677 ##                dbg('no valid sign found; new sign:', self._isNeg) 
4678             elif text 
and self
._valid 
and not self
._isNeg 
and text
[signpos
] in ('-', '('): 
4679 ##                dbg('setting _isNeg to True') 
4681 ##            dbg('self._isNeg:', self._isNeg) 
4683         if self
._signOk 
and self
._isNeg
: 
4684             fc 
= self
._signedForegroundColour
 
4686             fc 
= self
._foregroundColour
 
4688         if hasattr(fc
, '_name'): 
4692 ##        dbg('setting foreground to', c) 
4693         self
.SetForegroundColour(fc
) 
4698                 bc 
= self
._emptyBackgroundColour
 
4700                 bc 
= self
._validBackgroundColour
 
4703             bc 
= self
._invalidBackgroundColour
 
4704         if hasattr(bc
, '_name'): 
4708 ##        dbg('setting background to', c) 
4709         self
.SetBackgroundColour(bc
) 
4711 ##        dbg(indent=0, suspend=0) 
4714     def _getAbsValue(self
, candidate
=None): 
4715         """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). 
4717 ##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) 
4718         if candidate 
is None: text 
= self
._GetValue
() 
4719         else: text 
= candidate
 
4720         right_signpos 
= text
.find(')') 
4723             if self
._ctrl
_constraints
._alignRight 
and self
._fields
[0]._fillChar 
== ' ': 
4724                 signpos 
= text
.find('-') 
4726 ##                    dbg('no - found; searching for (') 
4727                     signpos 
= text
.find('(') 
4729 ##                    dbg('- found at', signpos) 
4733 ##                    dbg('signpos still -1') 
4734 ##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) 
4735                     if len(text
) < self
._masklength
: 
4737                     if len(text
) < self
._masklength
: 
4739                     if len(text
) > self
._masklength 
and text
[-1] in (')', ' '): 
4742 ##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) 
4743 ##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) 
4744                         signpos 
= len(text
) - (len(text
.lstrip()) + 1) 
4746                         if self
._useParens 
and not text
.strip(): 
4747                             signpos 
-= 1    # empty value; use penultimate space 
4748 ##                dbg('signpos:', signpos) 
4750                     text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4755                     text 
= self
._template
[0] + text
[1:] 
4759             if right_signpos 
!= -1: 
4761                     text 
= text
[:right_signpos
] + ' ' + text
[right_signpos
+1:] 
4762                 elif len(text
) > self
._masklength
: 
4763                     text 
= text
[:right_signpos
] + text
[right_signpos
+1:] 
4767             elif self
._useParens 
and self
._signOk
: 
4768                 # figure out where it ought to go: 
4769                 right_signpos 
= self
._masklength 
- 1     # initial guess 
4770                 if not self
._ctrl
_constraints
._alignRight
: 
4771 ##                    dbg('not right-aligned') 
4772                     if len(text
.strip()) == 0: 
4773                         right_signpos 
= signpos 
+ 1 
4774                     elif len(text
.strip()) < self
._masklength
: 
4775                         right_signpos 
= len(text
.rstrip()) 
4776 ##                dbg('right_signpos:', right_signpos) 
4778             groupchar 
= self
._fields
[0]._groupChar
 
4780                 value 
= long(text
.replace(groupchar
,'').replace('(','-').replace(')','').replace(' ', '')) 
4782 ##                dbg('invalid number', indent=0) 
4783                 return None, signpos
, right_signpos
 
4787                 groupchar 
= self
._fields
[0]._groupChar
 
4788                 value 
= float(text
.replace(groupchar
,'').replace(self
._decimalChar
, '.').replace('(', '-').replace(')','').replace(' ', '')) 
4789 ##                dbg('value:', value) 
4793             if value 
< 0 and value 
is not None: 
4794                 signpos 
= text
.find('-') 
4796                     signpos 
= text
.find('(') 
4798                 text 
= text
[:signpos
] + self
._template
[signpos
] + text
[signpos
+1:] 
4800                 # look forwards up to the decimal point for the 1st non-digit 
4801 ##                dbg('decimal pos:', self._decimalpos) 
4802 ##                dbg('text: "%s"' % text) 
4804                     signpos 
= self
._decimalpos 
- (len(text
[:self
._decimalpos
].lstrip()) + 1) 
4805                     # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET 
4806                     if len(text
) >= signpos
+1 and  text
[signpos
+1] in ('-','('): 
4810 ##                dbg('signpos:', signpos) 
4814                     right_signpos 
= self
._masklength 
- 1 
4815                     text 
= text
[:right_signpos
] + ' ' 
4816                     if text
[signpos
] == '(': 
4817                         text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4819                     right_signpos 
= text
.find(')') 
4820                     if right_signpos 
!= -1: 
4825 ##                dbg('invalid number') 
4828 ##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) 
4830         return text
, signpos
, right_signpos
 
4833     def _getSignedValue(self
, candidate
=None): 
4834         """ Return a signed value by adding a "-" prefix if the value 
4835             is set to negative, or a space if positive. 
4837 ##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) 
4838         if candidate 
is None: text 
= self
._GetValue
() 
4839         else: text 
= candidate
 
4842         abstext
, signpos
, right_signpos 
= self
._getAbsValue
(text
) 
4846                 return abstext
, signpos
, right_signpos
 
4848             if self
._isNeg 
or text
[signpos
] in ('-', '('): 
4855             if abstext
[signpos
] not in string
.digits
: 
4856                 text 
= abstext
[:signpos
] + sign 
+ abstext
[signpos
+1:] 
4858                 # this can happen if value passed is too big; sign assumed to be 
4859                 # in position 0, but if already filled with a digit, prepend sign... 
4860                 text 
= sign 
+ abstext
 
4861             if self
._useParens 
and text
.find('(') != -1: 
4862                 text 
= text
[:right_signpos
] + ')' + text
[right_signpos
+1:] 
4865 ##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) 
4867         return text
, signpos
, right_signpos
 
4870     def GetPlainValue(self
, candidate
=None): 
4871         """ Returns control's value stripped of the template text. 
4872             plainvalue = MaskedEditMixin.GetPlainValue() 
4874 ##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) 
4876         if candidate 
is None: text 
= self
._GetValue
() 
4877         else: text 
= candidate
 
4880 ##            dbg('returned ""', indent=0) 
4884             for idx 
in range( min(len(self
._template
), len(text
)) ): 
4885                 if self
._mask
[idx
] in maskchars
: 
4888             if self
._isFloat 
or self
._isInt
: 
4889 ##                dbg('plain so far: "%s"' % plain) 
4890                 plain 
= plain
.replace('(', '-').replace(')', ' ') 
4891 ##                dbg('plain after sign regularization: "%s"' % plain) 
4893                 if self
._signOk 
and self
._isNeg 
and plain
.count('-') == 0: 
4894                     # must be in reserved position; add to "plain value" 
4895                     plain 
= '-' + plain
.strip() 
4897                 if self
._fields
[0]._alignRight
: 
4898                     lpad 
= plain
.count(',') 
4899                     plain 
= ' ' * lpad 
+ plain
.replace(',','') 
4901                     plain 
= plain
.replace(',','') 
4902 ##                dbg('plain after pad and group:"%s"' % plain) 
4904 ##            dbg('returned "%s"' % plain.rstrip(), indent=0) 
4905             return plain
.rstrip() 
4908     def IsEmpty(self
, value
=None): 
4910         Returns True if control is equal to an empty value. 
4911         (Empty means all editable positions in the template == fillChar.) 
4913         if value 
is None: value 
= self
._GetValue
() 
4914         if value 
== self
._template 
and not self
._defaultValue
: 
4915 ####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") 
4916             return True     # (all mask chars == fillChar by defn) 
4917         elif value 
== self
._template
: 
4919             for pos 
in range(len(self
._template
)): 
4920 ####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) 
4921 ####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) 
4922                 if self
._isMaskChar
(pos
) and value
[pos
] not in (' ', self
._fillChar
[pos
]): 
4924 ####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) 
4927 ####            dbg("IsEmpty? 0 (value doesn't match template)") 
4931     def IsDefault(self
, value
=None): 
4933         Returns True if the value specified (or the value of the control if not specified) 
4934         is equal to the default value. 
4936         if value 
is None: value 
= self
._GetValue
() 
4937         return value 
== self
._template
 
4940     def IsValid(self
, value
=None): 
4941         """ Indicates whether the value specified (or the current value of the control 
4942         if not specified) is considered valid.""" 
4943 ####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) 
4944         if value 
is None: value 
= self
._GetValue
() 
4945         ret 
= self
._CheckValid
(value
) 
4950     def _eraseSelection(self
, value
=None, sel_start
=None, sel_to
=None): 
4951         """ Used to blank the selection when inserting a new character. """ 
4952 ##        dbg("MaskedEditMixin::_eraseSelection", indent=1) 
4953         if value 
is None: value 
= self
._GetValue
() 
4954         if sel_start 
is None or sel_to 
is None: 
4955             sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
4956 ##        dbg('value: "%s"' % value) 
4957 ##        dbg("current sel_start, sel_to:", sel_start, sel_to) 
4959         newvalue 
= list(value
) 
4960         for i 
in range(sel_start
, sel_to
): 
4961             if self
._signOk 
and newvalue
[i
] in ('-', '(', ')'): 
4962 ##                dbg('found sign (%s) at' % newvalue[i], i) 
4964                 # balance parentheses: 
4965                 if newvalue
[i
] == '(': 
4966                     right_signpos 
= value
.find(')') 
4967                     if right_signpos 
!= -1: 
4968                         newvalue
[right_signpos
] = ' ' 
4970                 elif newvalue
[i
] == ')': 
4971                     left_signpos 
= value
.find('(') 
4972                     if left_signpos 
!= -1: 
4973                         newvalue
[left_signpos
] = ' ' 
4977             elif self
._isMaskChar
(i
): 
4978                 field 
= self
._FindField
(i
) 
4982                     newvalue
[i
] = self
._template
[i
] 
4984         value 
= string
.join(newvalue
,"") 
4985 ##        dbg('new value: "%s"' % value) 
4990     def _insertKey(self
, char
, pos
, sel_start
, sel_to
, value
, allowAutoSelect
=False): 
4991         """ Handles replacement of the character at the current insertion point.""" 
4992 ##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) 
4994         text 
= self
._eraseSelection
(value
) 
4995         field 
= self
._FindField
(pos
) 
4996         start
, end 
= field
._extent
 
5000         # if >= 2 chars selected in a right-insert field, do appropriate erase on field, 
5001         # then set selection to end, and do usual right insert. 
5002         if sel_start 
!= sel_to 
and sel_to 
>= sel_start
+2: 
5003             field 
= self
._FindField
(sel_start
) 
5004             if( field
._insertRight                          
# if right-insert 
5005                 and field
._allowInsert                      
# and allow insert at any point in field 
5006                 and field 
== self
._FindField
(sel_to
) ):     # and selection all in same field 
5007                 text 
= self
._OnErase
(just_return_value
=True)    # remove selection before insert 
5008 ##                dbg('text after (left)erase: "%s"' % text) 
5009                 pos 
= sel_start 
= sel_to
 
5011         if pos 
!= sel_start 
and sel_start 
== sel_to
: 
5012             # adjustpos must have moved the position; make selection match: 
5013             sel_start 
= sel_to 
= pos
 
5015 ##        dbg('field._insertRight?', field._insertRight) 
5016 ##        dbg('field._allowInsert?', field._allowInsert) 
5017 ##        dbg('sel_start, end', sel_start, end) 
5019 ##            dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar) 
5022         if( field
._insertRight                                  
# field allows right insert 
5023             and ((sel_start
, sel_to
) == field
._extent           
# and whole field selected 
5024                  or (sel_start 
== sel_to                        
# or nothing selected 
5025                      and (sel_start 
== end                      
# and cursor at right edge 
5026                           or (field
._allowInsert                
# or field allows right-insert 
5027                               and sel_start 
< end               
# next to other char in field: 
5028                               and text
[sel_start
] != field
._fillChar
) ) ) ) ): 
5029 ##            dbg('insertRight') 
5030             fstr 
= text
[start
:end
] 
5031             erasable_chars 
= [field
._fillChar
, ' '] 
5033             # if zero padding field, or a single digit, and currently a value of 0, allow erasure of 0: 
5034             if field
._padZero 
or (field
._isInt 
and (end 
- start 
== 1) and fstr
[0] == '0'): 
5035                 erasable_chars
.append('0') 
5038 ####            dbg("fstr[0]:'%s'" % fstr[0]) 
5039 ####            dbg('field_index:', field._index) 
5040 ####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) 
5041 ####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", self._signOk and field._index == 0 and fstr[0] in ('-','(')) 
5042             if fstr
[0] in erasable_chars 
or (self
._signOk 
and field
._index 
== 0 and fstr
[0] in ('-','(')): 
5044 ####                dbg('value:      "%s"' % text) 
5045 ####                dbg('fstr:       "%s"' % fstr) 
5046 ####                dbg("erased:     '%s'" % erased) 
5047                 field_sel_start 
= sel_start 
- start
 
5048                 field_sel_to 
= sel_to 
- start
 
5049 ##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start]) 
5050 ##                dbg('right fstr: "%s"' % fstr[field_sel_to:end]) 
5051                 fstr 
= fstr
[1:field_sel_start
] + char 
+ fstr
[field_sel_to
:end
] 
5052             if field
._alignRight 
and sel_start 
!= sel_to
: 
5053                 field_len 
= end 
- start
 
5054 ##                pos += (field_len - len(fstr))    # move cursor right by deleted amount 
5056 ##                dbg('setting pos to:', pos) 
5058                     fstr 
= '0' * (field_len 
- len(fstr
)) + fstr
 
5060                     fstr 
= fstr
.rjust(field_len
)   # adjust the field accordingly 
5061 ##            dbg('field str: "%s"' % fstr) 
5063             newtext 
= text
[:start
] + fstr 
+ text
[end
:] 
5064             if erased 
in ('-', '(') and self
._signOk
: 
5065                 newtext 
= erased 
+ newtext
[1:] 
5066 ##            dbg('newtext: "%s"' % newtext) 
5068             if self
._signOk 
and field
._index 
== 0: 
5069                 start 
-= 1             # account for sign position 
5071 ####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull) 
5072 ####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) 
5073             if( field
._moveOnFieldFull 
and pos 
== end
 
5074                 and len(fstr
.lstrip()) == end
-start
):   # if field now full 
5075                 newpos 
= self
._findNextEntry
(end
)       #   go to next field 
5077                 newpos 
= pos                            
# else keep cursor at current position 
5080 ##            dbg('not newtext') 
5082 ##                dbg('newpos:', newpos) 
5084             if self
._signOk 
and self
._useParens
: 
5085                 old_right_signpos 
= text
.find(')') 
5087             if field
._allowInsert 
and not field
._insertRight 
and sel_to 
<= end 
and sel_start 
>= start
: 
5088 ##                dbg('inserting within a left-insert-capable field') 
5089                 field_len 
= end 
- start
 
5090                 before 
= text
[start
:sel_start
] 
5091                 after 
= text
[sel_to
:end
].strip() 
5092 ####                dbg("current field:'%s'" % text[start:end]) 
5093 ####                dbg("before:'%s'" % before, "after:'%s'" % after) 
5094                 new_len 
= len(before
) + len(after
) + 1 # (for inserted char) 
5095 ####                dbg('new_len:', new_len) 
5097                 if new_len 
< field_len
: 
5098                     retained 
= after 
+ self
._template
[end
-(field_len
-new_len
):end
] 
5099                 elif new_len 
> end
-start
: 
5100                     retained 
= after
[1:] 
5104                 left 
= text
[0:start
] + before
 
5105 ####                dbg("left:'%s'" % left, "retained:'%s'" % retained) 
5106                 right   
= retained 
+ text
[end
:] 
5109                 right   
= text
[pos
+1:] 
5111             if 'unicode' in wx
.PlatformInfo 
and type(char
) != types
.UnicodeType
: 
5112                 # convert the keyboard constant to a unicode value, to 
5113                 # ensure it can be concatenated into the control value: 
5114                 char 
= char
.decode(self
._defaultEncoding
) 
5116             newtext 
= left 
+ char 
+ right
 
5117 ####            dbg('left:    "%s"' % left) 
5118 ####            dbg('right:   "%s"' % right) 
5119 ####            dbg('newtext: "%s"' % newtext) 
5121             if self
._signOk 
and self
._useParens
: 
5122                 # Balance parentheses: 
5123                 left_signpos 
= newtext
.find('(') 
5125                 if left_signpos 
== -1:     # erased '('; remove ')' 
5126                     right_signpos 
= newtext
.find(')') 
5127                     if right_signpos 
!= -1: 
5128                         newtext 
= newtext
[:right_signpos
] + ' ' + newtext
[right_signpos
+1:] 
5130                 elif old_right_signpos 
!= -1: 
5131                     right_signpos 
= newtext
.find(')') 
5133                     if right_signpos 
== -1: # just replaced right-paren 
5134                         if newtext
[pos
] == ' ': # we just erased '); erase '(' 
5135                             newtext 
= newtext
[:left_signpos
] + ' ' + newtext
[left_signpos
+1:] 
5136                         else:   # replaced with digit; move ') over 
5137                             if self
._ctrl
_constraints
._alignRight 
or self
._isFloat
: 
5138                                 newtext 
= newtext
[:-1] + ')' 
5140                                 rstripped_text 
= newtext
.rstrip() 
5141                                 right_signpos 
= len(rstripped_text
) 
5142 ##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) 
5143                                 newtext 
= newtext
[:right_signpos
] + ')' + newtext
[right_signpos
+1:] 
5145             if( field
._insertRight                                  
# if insert-right field (but we didn't start at right edge) 
5146                 and field
._moveOnFieldFull                          
# and should move cursor when full 
5147                 and len(newtext
[start
:end
].strip()) == end
-start
):  # and field now full 
5148                 newpos 
= self
._findNextEntry
(end
)                   #   go to next field 
5149 ##                dbg('newpos = nextentry =', newpos) 
5151 ##                dbg('pos:', pos, 'newpos:', pos+1) 
5156             new_select_to 
= newpos     
# (default return values) 
5160             if field
._autoSelect
: 
5161                 match_index
, partial_match 
= self
._autoComplete
(1,  # (always forward) 
5162                                                                 field
._compareChoices
, 
5164                                                                 compareNoCase
=field
._compareNoCase
, 
5165                                                                 current_index 
= field
._autoCompleteIndex
-1) 
5166                 if match_index 
is not None and partial_match
: 
5167                     matched_str 
= newtext
[start
:end
] 
5168                     newtext 
= newtext
[:start
] + field
._choices
[match_index
] + newtext
[end
:] 
5171                     if field
._insertRight
: 
5172                         # adjust position to just after partial match in field 
5173                         newpos 
= end 
- (len(field
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5175             elif self
._ctrl
_constraints
._autoSelect
: 
5176                 match_index
, partial_match 
= self
._autoComplete
( 
5177                                         1,  # (always forward) 
5178                                         self
._ctrl
_constraints
._compareChoices
, 
5180                                         self
._ctrl
_constraints
._compareNoCase
, 
5181                                         current_index 
= self
._ctrl
_constraints
._autoCompleteIndex 
- 1) 
5182                 if match_index 
is not None and partial_match
: 
5183                     matched_str 
= newtext
 
5184                     newtext 
= self
._ctrl
_constraints
._choices
[match_index
] 
5185                     edit_end 
= self
._ctrl
_constraints
._extent
[1] 
5186                     new_select_to 
= min(edit_end
, len(newtext
.rstrip())) 
5187                     match_field 
= self
._ctrl
_constraints
 
5188                     if self
._ctrl
_constraints
._insertRight
: 
5189                         # adjust position to just after partial match in control: 
5190                         newpos 
= self
._masklength 
- (len(self
._ctrl
_constraints
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5192 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) 
5194             return newtext
, newpos
, new_select_to
, match_field
, match_index
 
5196 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos) 
5198             return newtext
, newpos
 
5201     def _OnFocus(self
,event
): 
5203         This event handler is currently necessary to work around new default 
5204         behavior as of wxPython2.3.3; 
5205         The TAB key auto selects the entire contents of the wx.TextCtrl *after* 
5206         the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection 
5207         *here*, because it hasn't happened yet.  So to prevent this behavior, and 
5208         preserve the correct selection when the focus event is not due to tab, 
5209         we need to pull the following trick: 
5211 ##        dbg('MaskedEditMixin::_OnFocus') 
5212         if self
.IsBeingDeleted() or self
.GetParent().IsBeingDeleted(): 
5214         wx
.CallAfter(self
._fixSelection
) 
5219     def _CheckValid(self
, candidate
=None): 
5221         This is the default validation checking routine; It verifies that the 
5222         current value of the control is a "valid value," and has the side 
5223         effect of coloring the control appropriately. 
5226 ##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) 
5227         oldValid 
= self
._valid
 
5228         if candidate 
is None: value 
= self
._GetValue
() 
5229         else: value 
= candidate
 
5230 ##        dbg('value: "%s"' % value) 
5232         valid 
= True    # assume True 
5234         if not self
.IsDefault(value
) and self
._isDate
:                    ## Date type validation 
5235             valid 
= self
._validateDate
(value
) 
5236 ##            dbg("valid date?", valid) 
5238         elif not self
.IsDefault(value
) and self
._isTime
: 
5239             valid 
= self
._validateTime
(value
) 
5240 ##            dbg("valid time?", valid) 
5242         elif not self
.IsDefault(value
) and (self
._isInt 
or self
._isFloat
):  ## Numeric type 
5243             valid 
= self
._validateNumeric
(value
) 
5244 ##            dbg("valid Number?", valid) 
5246         if valid
:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault() 
5247             ## valid so far; ensure also allowed by any list or regex provided: 
5248             valid 
= self
._validateGeneric
(value
) 
5249 ##            dbg("valid value?", valid) 
5251 ##        dbg('valid?', valid) 
5255             self
._applyFormatting
() 
5256             if self
._valid 
!= oldValid
: 
5257 ##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) 
5258 ##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) 
5260 ##        dbg(indent=0, suspend=0) 
5264     def _validateGeneric(self
, candidate
=None): 
5265         """ Validate the current value using the provided list or Regex filter (if any). 
5267         if candidate 
is None: 
5268             text 
= self
._GetValue
() 
5272         valid 
= True    # assume True 
5273         for i 
in [-1] + self
._field
_indices
:   # process global constraints first: 
5274             field 
= self
._fields
[i
] 
5275             start
, end 
= field
._extent
 
5276             slice = text
[start
:end
] 
5277             valid 
= field
.IsValid(slice) 
5284     def _validateNumeric(self
, candidate
=None): 
5285         """ Validate that the value is within the specified range (if specified.)""" 
5286         if candidate 
is None: value 
= self
._GetValue
() 
5287         else: value 
= candidate
 
5289             groupchar 
= self
._fields
[0]._groupChar
 
5291                 number 
= float(value
.replace(groupchar
, '').replace(self
._decimalChar
, '.').replace('(', '-').replace(')', '')) 
5293                 number 
= long( value
.replace(groupchar
, '').replace('(', '-').replace(')', '')) 
5295                     if self
._fields
[0]._alignRight
: 
5296                         require_digit_at 
= self
._fields
[0]._extent
[1]-1 
5298                         require_digit_at 
= self
._fields
[0]._extent
[0] 
5299 ##                    dbg('require_digit_at:', require_digit_at) 
5300 ##                    dbg("value[rda]: '%s'" % value[require_digit_at]) 
5301                     if value
[require_digit_at
] not in list(string
.digits
): 
5305 ##            dbg('number:', number) 
5306             if self
._ctrl
_constraints
._hasRange
: 
5307                 valid 
= self
._ctrl
_constraints
._rangeLow 
<= number 
<= self
._ctrl
_constraints
._rangeHigh
 
5310             groupcharpos 
= value
.rfind(groupchar
) 
5311             if groupcharpos 
!= -1:  # group char present 
5312 ##                dbg('groupchar found at', groupcharpos) 
5313                 if self
._isFloat 
and groupcharpos 
> self
._decimalpos
: 
5314                     # 1st one found on right-hand side is past decimal point 
5315 ##                    dbg('groupchar in fraction; illegal') 
5318                     integer 
= value
[:self
._decimalpos
].strip() 
5320                     integer 
= value
.strip() 
5321 ##                dbg("integer:'%s'" % integer) 
5322                 if integer
[0] in ('-', '('): 
5323                     integer 
= integer
[1:] 
5324                 if integer
[-1] == ')': 
5325                     integer 
= integer
[:-1] 
5327                 parts 
= integer
.split(groupchar
) 
5328 ##                dbg('parts:', parts) 
5329                 for i 
in range(len(parts
)): 
5330                     if i 
== 0 and abs(int(parts
[0])) > 999: 
5331 ##                        dbg('group 0 too long; illegal') 
5334                     elif i 
> 0 and (len(parts
[i
]) != 3 or ' ' in parts
[i
]): 
5335 ##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i])) 
5339 ##            dbg('value not a valid number') 
5344     def _validateDate(self
, candidate
=None): 
5345         """ Validate the current date value using the provided Regex filter. 
5346             Generally used for character types.BufferType 
5348 ##        dbg('MaskedEditMixin::_validateDate', indent=1) 
5349         if candidate 
is None: value 
= self
._GetValue
() 
5350         else: value 
= candidate
 
5351 ##        dbg('value = "%s"' % value) 
5352         text 
= self
._adjustDate
(value
, force4digit_year
=True)     ## Fix the date up before validating it 
5353 ##        dbg('text =', text) 
5354         valid 
= True   # assume True until proven otherwise 
5357             # replace fillChar in each field with space: 
5358             datestr 
= text
[0:self
._dateExtent
] 
5360                 field 
= self
._fields
[i
] 
5361                 start
, end 
= field
._extent
 
5362                 fstr 
= datestr
[start
:end
] 
5363                 fstr
.replace(field
._fillChar
, ' ') 
5364                 datestr 
= datestr
[:start
] + fstr 
+ datestr
[end
:] 
5366             year
, month
, day 
= _getDateParts( datestr
, self
._datestyle
) 
5368 ##            dbg('self._dateExtent:', self._dateExtent) 
5369             if self
._dateExtent 
== 11: 
5370                 month 
= charmonths_dict
[month
.lower()] 
5374 ##            dbg('year, month, day:', year, month, day) 
5377 ##            dbg('cannot convert string to integer parts') 
5380 ##            dbg('cannot convert string to integer month') 
5384             # use wxDateTime to unambiguously try to parse the date: 
5385             # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, 
5386             # rather than 1-12, so handle accordingly: 
5392 ##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) 
5393                     dateHandler 
= wx
.DateTimeFromDMY(day
,month
,year
) 
5397 ##                    dbg('cannot convert string to valid date') 
5403                 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5404                 # so we eliminate them here: 
5405                 timeStr     
= text
[self
._dateExtent
+1:].strip()         ## time portion of the string 
5407 ##                    dbg('timeStr: "%s"' % timeStr) 
5409                         checkTime    
= dateHandler
.ParseTime(timeStr
) 
5410                         valid 
= checkTime 
== len(timeStr
) 
5414 ##                        dbg('cannot convert string to valid time') 
5416 ##        if valid: dbg('valid date') 
5421     def _validateTime(self
, candidate
=None): 
5422         """ Validate the current time value using the provided Regex filter. 
5423             Generally used for character types.BufferType 
5425 ##        dbg('MaskedEditMixin::_validateTime', indent=1) 
5426         # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5427         # so we eliminate them here: 
5428         if candidate 
is None: value 
= self
._GetValue
().strip() 
5429         else: value 
= candidate
.strip() 
5430 ##        dbg('value = "%s"' % value) 
5431         valid 
= True   # assume True until proven otherwise 
5433         dateHandler 
= wx
.DateTime_Today() 
5435             checkTime    
= dateHandler
.ParseTime(value
) 
5436 ##            dbg('checkTime:', checkTime, 'len(value)', len(value)) 
5437             valid 
= checkTime 
== len(value
) 
5442 ##            dbg('cannot convert string to valid time') 
5444 ##        if valid: dbg('valid time') 
5449     def _OnKillFocus(self
,event
): 
5450         """ Handler for EVT_KILL_FOCUS event. 
5452 ##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) 
5453         if self
.IsBeingDeleted() or self
.GetParent().IsBeingDeleted(): 
5455         if self
._mask 
and self
._IsEditable
(): 
5456             self
._AdjustField
(self
._GetInsertionPoint
()) 
5457             self
._CheckValid
()   ## Call valid handler 
5459         self
._LostFocus
()    ## Provided for subclass use 
5464     def _fixSelection(self
): 
5466         This gets called after the TAB traversal selection is made, if the 
5467         focus event was due to this, but before the EVT_LEFT_* events if 
5468         the focus shift was due to a mouse event. 
5470         The trouble is that, a priori, there's no explicit notification of 
5471         why the focus event we received.  However, the whole reason we need to 
5472         do this is because the default behavior on TAB traveral in a wx.TextCtrl is 
5473         now to select the entire contents of the window, something we don't want. 
5474         So we can *now* test the selection range, and if it's "the whole text" 
5475         we can assume the cause, change the insertion point to the start of 
5476         the control, and deselect. 
5478 ##        dbg('MaskedEditMixin::_fixSelection', indent=1) 
5479         # can get here if called with wx.CallAfter after underlying  
5480         # control has been destroyed on close, but after focus 
5482         if not self 
or not self
._mask 
or not self
._IsEditable
(): 
5486         sel_start
, sel_to 
= self
._GetSelection
() 
5487 ##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) 
5489         if( sel_start 
== 0 and sel_to 
>= len( self
._mask 
)   #(can be greater in numeric controls because of reserved space) 
5490             and (not self
._ctrl
_constraints
._autoSelect 
or self
.IsEmpty() or self
.IsDefault() ) ): 
5491             # This isn't normally allowed, and so assume we got here by the new 
5492             # "tab traversal" behavior, so we need to reset the selection 
5493             # and insertion point: 
5494 ##            dbg('entire text selected; resetting selection to start of control') 
5496             field 
= self
._FindField
(self
._GetInsertionPoint
()) 
5497             edit_start
, edit_end 
= field
._extent
 
5498             if field
._selectOnFieldEntry
: 
5499                 if self
._isFloat 
or self
._isInt 
and field 
== self
._fields
[0]: 
5501                 self
._SetInsertionPoint
(edit_start
) 
5502                 self
._SetSelection
(edit_start
, edit_end
) 
5504             elif field
._insertRight
: 
5505                 self
._SetInsertionPoint
(edit_end
) 
5506                 self
._SetSelection
(edit_end
, edit_end
) 
5508         elif (self
._isFloat 
or self
._isInt
): 
5510             text
, signpos
, right_signpos 
= self
._getAbsValue
() 
5511             if text 
is None or text 
== self
._template
: 
5512                 integer 
= self
._fields
[0] 
5513                 edit_start
, edit_end 
= integer
._extent
 
5515                 if integer
._selectOnFieldEntry
: 
5516 ##                    dbg('select on field entry:') 
5517                     self
._SetInsertionPoint
(0) 
5518                     self
._SetSelection
(0, edit_end
) 
5520                 elif integer
._insertRight
: 
5521 ##                    dbg('moving insertion point to end') 
5522                     self
._SetInsertionPoint
(edit_end
) 
5523                     self
._SetSelection
(edit_end
, edit_end
) 
5525 ##                    dbg('numeric ctrl is empty; start at beginning after sign') 
5526                     self
._SetInsertionPoint
(signpos
+1)   ## Move past minus sign space if signed 
5527                     self
._SetSelection
(signpos
+1, signpos
+1) 
5529         elif sel_start 
> self
._goEnd
(getPosOnly
=True): 
5530 ##            dbg('cursor beyond the end of the user input; go to end of it') 
5533 ##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) 
5538     def _Keypress(self
,key
): 
5539         """ Method provided to override OnChar routine. Return False to force 
5540             a skip of the 'normal' OnChar process. Called before class OnChar. 
5545     def _LostFocus(self
): 
5546         """ Method provided for subclasses. _LostFocus() is called after 
5547             the class processes its EVT_KILL_FOCUS event code. 
5552     def _OnDoubleClick(self
, event
): 
5553         """ selects field under cursor on dclick.""" 
5554         pos 
= self
._GetInsertionPoint
() 
5555         field 
= self
._FindField
(pos
) 
5556         start
, end 
= field
._extent
 
5557         self
._SetInsertionPoint
(start
) 
5558         self
._SetSelection
(start
, end
) 
5562         """ Method provided for subclasses. Called by internal EVT_TEXT 
5563             handler. Return False to override the class handler, True otherwise. 
5570         Used to override the default Cut() method in base controls, instead 
5571         copying the selection to the clipboard and then blanking the selection, 
5572         leaving only the mask in the selected area behind. 
5573         Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the 
5574         derived control because the mixin functions can't override a method of 
5577 ##        dbg("MaskedEditMixin::_Cut", indent=1) 
5578         value 
= self
._GetValue
() 
5579 ##        dbg('current value: "%s"' % value) 
5580         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
5581 ##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) 
5582         do 
= wx
.TextDataObject() 
5583         do
.SetText(value
[sel_start
:sel_to
].strip()) 
5584         wx
.TheClipboard
.Open() 
5585         wx
.TheClipboard
.SetData(do
) 
5586         wx
.TheClipboard
.Close() 
5588         if sel_to 
- sel_start 
!= 0: 
5593 # WS Note: overriding Copy is no longer necessary given that you 
5594 # can no longer select beyond the last non-empty char in the control. 
5596 ##    def _Copy( self ): 
5598 ##        Override the wx.TextCtrl's .Copy function, with our own 
5599 ##        that does validation.  Need to strip trailing spaces. 
5601 ##        sel_start, sel_to = self._GetSelection() 
5602 ##        select_len = sel_to - sel_start 
5603 ##        textval = wx.TextCtrl._GetValue(self) 
5605 ##        do = wx.TextDataObject() 
5606 ##        do.SetText(textval[sel_start:sel_to].strip()) 
5607 ##        wx.TheClipboard.Open() 
5608 ##        wx.TheClipboard.SetData(do) 
5609 ##        wx.TheClipboard.Close() 
5612     def _getClipboardContents( self 
): 
5613         """ Subroutine for getting the current contents of the clipboard. 
5615         do 
= wx
.TextDataObject() 
5616         wx
.TheClipboard
.Open() 
5617         success 
= wx
.TheClipboard
.GetData(do
) 
5618         wx
.TheClipboard
.Close() 
5623             # Remove leading and trailing spaces before evaluating contents 
5624             return do
.GetText().strip() 
5627     def _validatePaste(self
, paste_text
, sel_start
, sel_to
, raise_on_invalid
=False): 
5629         Used by paste routine and field choice validation to see 
5630         if a given slice of paste text is legal for the area in question: 
5631         returns validity, replacement text, and extent of paste in 
5635 ##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) 
5636         select_length 
= sel_to 
- sel_start
 
5637         maxlength 
= select_length
 
5638 ##        dbg('sel_to - sel_start:', maxlength) 
5640             maxlength 
= self
._masklength 
- sel_start
 
5644 ##        dbg('maxlength:', maxlength) 
5645         if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5646             paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5648         length_considered 
= len(paste_text
) 
5649         if length_considered 
> maxlength
: 
5650 ##            dbg('paste text will not fit into the %s:' % item, indent=0) 
5651             if raise_on_invalid
: 
5652 ##                dbg(indent=0, suspend=0) 
5653                 if item 
== 'control': 
5654                     ve 
= ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5655                     ve
.value 
= paste_text
 
5658                     ve 
= ValueError('"%s" will not fit into the selection' % paste_text
) 
5659                     ve
.value 
= paste_text
 
5662 ##                dbg(indent=0, suspend=0) 
5663                 return False, None, None 
5665         text 
= self
._template
 
5666 ##        dbg('length_considered:', length_considered) 
5669         replacement_text 
= "" 
5670         replace_to 
= sel_start
 
5672         while valid_paste 
and i 
< length_considered 
and replace_to 
< self
._masklength
: 
5673             if paste_text
[i
:] == self
._template
[replace_to
:length_considered
]: 
5674                 # remainder of paste matches template; skip char-by-char analysis 
5675 ##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) 
5676                 replacement_text 
+= paste_text
[i
:] 
5677                 replace_to 
= i 
= length_considered
 
5680             char 
= paste_text
[i
] 
5681             field 
= self
._FindField
(replace_to
) 
5682             if not field
._compareNoCase
: 
5683                 if field
._forceupper
:   char 
= char
.upper() 
5684                 elif field
._forcelower
: char 
= char
.lower() 
5686 ##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) 
5687 ##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) 
5688             if not self
._isTemplateChar
(replace_to
) and self
._isCharAllowed
( char
, replace_to
, allowAutoSelect
=False, ignoreInsertRight
=True): 
5689                 replacement_text 
+= char
 
5690 ##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) 
5691 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5694             elif( char 
== self
._template
[replace_to
] 
5695                   or (self
._signOk 
and 
5696                           ( (i 
== 0 and (char 
== '-' or (self
._useParens 
and char 
== '('))) 
5697                             or (i 
== self
._masklength 
- 1 and self
._useParens 
and char 
== ')') ) ) ): 
5698                 replacement_text 
+= char
 
5699 ##                dbg("'%(char)s' == template(%(replace_to)d)" % locals()) 
5700 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5704                 next_entry 
= self
._findNextEntry
(replace_to
, adjustInsert
=False) 
5705                 if next_entry 
== replace_to
: 
5708                     replacement_text 
+= self
._template
[replace_to
:next_entry
] 
5709 ##                    dbg("skipping template; next_entry =", next_entry) 
5710 ##                    dbg("replacement_text:", '"'+replacement_text+'"') 
5711                     replace_to 
= next_entry  
# so next_entry will be considered on next loop 
5713         if not valid_paste 
and raise_on_invalid
: 
5714 ##            dbg('raising exception', indent=0, suspend=0) 
5715             ve 
= ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text
, self
.name
)) 
5716             ve
.value 
= paste_text
 
5720         elif i 
< len(paste_text
): 
5722             if raise_on_invalid
: 
5723 ##                dbg('raising exception', indent=0, suspend=0) 
5724                 ve 
= ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5725                 ve
.value 
= paste_text
 
5728 ##        dbg('valid_paste?', valid_paste) 
5730 ##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) 
5732 ##        dbg(indent=0, suspend=0) 
5733         return valid_paste
, replacement_text
, replace_to
 
5736     def _Paste( self
, value
=None, raise_on_invalid
=False, just_return_value
=False ): 
5738         Used to override the base control's .Paste() function, 
5739         with our own that does validation. 
5740         Note: _Paste must be called from a Paste() override in the 
5741         derived control because the mixin functions can't override a 
5742         method of a sibling class. 
5744 ##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) 
5746             paste_text 
= self
._getClipboardContents
() 
5750         if paste_text 
is not None: 
5752             if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5753                 paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5755 ##            dbg('paste text: "%s"' % paste_text) 
5756             # (conversion will raise ValueError if paste isn't legal) 
5757             sel_start
, sel_to 
= self
._GetSelection
() 
5758 ##            dbg('selection:', (sel_start, sel_to)) 
5760             # special case: handle allowInsert fields properly 
5761             field 
= self
._FindField
(sel_start
) 
5762             edit_start
, edit_end 
= field
._extent
 
5764             if field
._allowInsert 
and sel_to 
<= edit_end 
and (sel_start 
+ len(paste_text
) < edit_end 
or field
._insertRight
): 
5765                 if field
._insertRight
: 
5766                     # want to paste to the left; see if it will fit: 
5767                     left_text 
= self
._GetValue
()[edit_start
:sel_start
].lstrip() 
5768 ##                    dbg('len(left_text):', len(left_text)) 
5769 ##                    dbg('len(paste_text):', len(paste_text)) 
5770 ##                    dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
5771                     if sel_start 
- (len(left_text
) - (sel_to 
- sel_start
) + len(paste_text
)) >= edit_start
: 
5772                         # will fit! create effective paste text, and move cursor back to do so: 
5773                         paste_text 
= left_text 
+ paste_text
 
5774                         sel_start 
-= len(left_text
) 
5775                         paste_text 
= paste_text
.rjust(sel_to 
- sel_start
) 
5776 ##                        dbg('modified paste_text to be: "%s"' % paste_text) 
5777 ##                        dbg('modified selection to:', (sel_start, sel_to)) 
5779 ##                        dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
5782                     paste_text 
= paste_text 
+ self
._GetValue
()[sel_to
:edit_end
].rstrip() 
5783 ##                    dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text) 
5786                 new_pos 
= sel_start 
+ len(paste_text
)   # store for subsequent positioning 
5787 ##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) 
5788 ##                dbg('expanded selection to:', (sel_start, sel_to)) 
5790             # Another special case: paste won't fit, but it's a right-insert field where entire 
5791             # non-empty value is selected, and there's room if the selection is expanded leftward: 
5792             if( len(paste_text
) > sel_to 
- sel_start
 
5793                 and field
._insertRight
 
5794                 and sel_start 
> edit_start
 
5795                 and sel_to 
>= edit_end
 
5796                 and not self
._GetValue
()[edit_start
:sel_start
].strip() ): 
5797                 # text won't fit within selection, but left of selection is empty; 
5798                 # check to see if we can expand selection to accommodate the value: 
5799                 empty_space 
= sel_start 
- edit_start
 
5800                 amount_needed 
= len(paste_text
) - (sel_to 
- sel_start
) 
5801                 if amount_needed 
<= empty_space
: 
5802                     sel_start 
-= amount_needed
 
5803 ##                    dbg('expanded selection to:', (sel_start, sel_to)) 
5806             # another special case: deal with signed values properly: 
5808                 signedvalue
, signpos
, right_signpos 
= self
._getSignedValue
() 
5809                 paste_signpos 
= paste_text
.find('-') 
5810                 if paste_signpos 
== -1: 
5811                     paste_signpos 
= paste_text
.find('(') 
5813                 # if paste text will result in signed value: 
5814 ####                dbg('paste_signpos != -1?', paste_signpos != -1) 
5815 ####                dbg('sel_start:', sel_start, 'signpos:', signpos) 
5816 ####                dbg('field._insertRight?', field._insertRight) 
5817 ####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) 
5818                 if paste_signpos 
!= -1 and (sel_start 
<= signpos
 
5819                                             or (field
._insertRight 
and sel_start 
- len(paste_text
) <= signpos
)): 
5823                 # remove "sign" from paste text, so we can auto-adjust for sign type after paste: 
5824                 paste_text 
= paste_text
.replace('-', ' ').replace('(',' ').replace(')','') 
5825 ##                dbg('unsigned paste text: "%s"' % paste_text) 
5829             # another special case: deal with insert-right fields when selection is empty and 
5830             # cursor is at end of field: 
5831 ####            dbg('field._insertRight?', field._insertRight) 
5832 ####            dbg('sel_start == edit_end?', sel_start == edit_end) 
5833 ####            dbg('sel_start', sel_start, 'sel_to', sel_to) 
5834             if field
._insertRight 
and sel_start 
== edit_end 
and sel_start 
== sel_to
: 
5835                 sel_start 
-= len(paste_text
) 
5838 ##                dbg('adjusted selection:', (sel_start, sel_to)) 
5840             raise_on_invalid 
= raise_on_invalid 
or field
._raiseOnInvalidPaste
 
5842                 valid_paste
, replacement_text
, replace_to 
= self
._validatePaste
(paste_text
, sel_start
, sel_to
, raise_on_invalid
) 
5844 ##                dbg('exception thrown', indent=0) 
5848 ##                dbg('paste text not legal for the selection or portion of the control following the cursor;') 
5849                 if not wx
.Validator_IsSilent(): 
5854             text 
= self
._eraseSelection
() 
5856             new_text 
= text
[:sel_start
] + replacement_text 
+ text
[replace_to
:] 
5858                 new_text 
= string
.ljust(new_text
,self
._masklength
) 
5860                 new_text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=new_text
) 
5863                         new_text 
= new_text
[:signpos
] + '(' + new_text
[signpos
+1:right_signpos
] + ')' + new_text
[right_signpos
+1:] 
5865                         new_text 
= new_text
[:signpos
] + '-' + new_text
[signpos
+1:] 
5869 ##            dbg("new_text:", '"'+new_text+'"') 
5871             if not just_return_value
: 
5872                 if new_text 
!= self
._GetValue
(): 
5873                     self
.modified 
= True 
5877                     wx
.CallAfter(self
._SetValue
, new_text
) 
5879                         new_pos 
= sel_start 
+ len(replacement_text
) 
5880                     wx
.CallAfter(self
._SetInsertionPoint
, new_pos
) 
5883                 return new_text
, replace_to
 
5884         elif just_return_value
: 
5886             return self
._GetValue
(), sel_to
 
5889     def _Undo(self
, value
=None, prev
=None, just_return_results
=False): 
5890         """ Provides an Undo() method in base controls. """ 
5891 ##        dbg("MaskedEditMixin::_Undo", indent=1) 
5893             value 
= self
._GetValue
() 
5895             prev 
= self
._prevValue
 
5896 ##        dbg('current value:  "%s"' % value) 
5897 ##        dbg('previous value: "%s"' % prev) 
5899 ##            dbg('no previous value', indent=0) 
5903             # Determine what to select: (relies on fixed-length strings) 
5904             # (This is a lot harder than it would first appear, because 
5905             # of mask chars that stay fixed, and so break up the "diff"...) 
5907             # Determine where they start to differ: 
5909             length 
= len(value
)     # (both are same length in masked control) 
5911             while( value
[:i
] == prev
[:i
] ): 
5916             # handle signed values carefully, so undo from signed to unsigned or vice-versa 
5919                 text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=prev
) 
5921                     if prev
[signpos
] == '(' and prev
[right_signpos
] == ')': 
5925                     # eliminate source of "far-end" undo difference if using balanced parens: 
5926                     value 
= value
.replace(')', ' ') 
5927                     prev 
= prev
.replace(')', ' ') 
5928                 elif prev
[signpos
] == '-': 
5933             # Determine where they stop differing in "undo" result: 
5934             sm 
= difflib
.SequenceMatcher(None, a
=value
, b
=prev
) 
5935             i
, j
, k 
= sm
.find_longest_match(sel_start
, length
, sel_start
, length
) 
5936 ##            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] ) 
5938             if k 
== 0:                              # no match found; select to end 
5941                 code_5tuples 
= sm
.get_opcodes() 
5942                 for op
, i1
, i2
, j1
, j2 
in code_5tuples
: 
5943 ##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) 
5947                 # look backward through operations needed to produce "previous" value; 
5948                 # first change wins: 
5949                 for next_op 
in range(len(code_5tuples
)-1, -1, -1): 
5950                     op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5951 ##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) 
5952                     field 
= self
._FindField
(i2
) 
5953                     if op 
== 'insert' and prev
[j1
:j2
] != self
._template
[j1
:j2
]: 
5954 ##                        dbg('insert found: selection =>', (j1, j2)) 
5959                     elif op 
== 'delete' and value
[i1
:i2
] != self
._template
[i1
:i2
]: 
5960                         edit_start
, edit_end 
= field
._extent
 
5961                         if field
._insertRight 
and (field
._allowInsert 
or i2 
== edit_end
): 
5967 ##                        dbg('delete found: selection =>', (sel_start, sel_to)) 
5970                     elif op 
== 'replace': 
5971                         if not prev
[i1
:i2
].strip() and field
._insertRight
: 
5972                             sel_start 
= sel_to 
= j2
 
5976 ##                        dbg('replace found: selection =>', (sel_start, sel_to)) 
5982                     # now go forwards, looking for earlier changes: 
5983 ##                    dbg('searching forward...') 
5984                     for next_op 
in range(len(code_5tuples
)): 
5985                         op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5986                         field 
= self
._FindField
(i1
) 
5989                         elif op 
== 'replace': 
5990                             if field
._insertRight
: 
5991                                 # if replace with spaces in an insert-right control, ignore "forward" replace 
5992                                 if not prev
[i1
:i2
].strip(): 
5995 ##                                    dbg('setting sel_start to', j1) 
5998 ##                                    dbg('setting sel_start to', i1) 
6001 ##                                dbg('setting sel_start to', i1) 
6003 ##                            dbg('saw replace; breaking') 
6005                         elif op 
== 'insert' and not value
[i1
:i2
]: 
6006 ##                            dbg('forward %s found' % op) 
6007                             if prev
[j1
:j2
].strip(): 
6008 ##                                dbg('item to insert non-empty; setting sel_start to', j1) 
6011                             elif not field
._insertRight
: 
6012 ##                                dbg('setting sel_start to inserted space:', j1) 
6015                         elif op 
== 'delete': 
6016 ##                            dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip())) 
6017                             if field
._insertRight
: 
6018                                 if value
[i1
:i2
].lstrip(): 
6019 ##                                    dbg('setting sel_start to ', j1) 
6021 ##                                    dbg('breaking loop') 
6026 ##                                dbg('saw delete; breaking') 
6029 ##                            dbg('unknown code!') 
6030                             # we've got what we need 
6035 ##                    dbg('no insert,delete or replace found (!)') 
6036                     # do "left-insert"-centric processing of difference based on l.c.s.: 
6037                     if i 
== j 
and j 
!= sel_start
:         # match starts after start of selection 
6038                         sel_to 
= sel_start 
+ (j
-sel_start
)  # select to start of match 
6040                         sel_to 
= j                          
# (change ends at j) 
6043             # There are several situations where the calculated difference is 
6044             # not what we want to select.  If changing sign, or just adding 
6045             # group characters, we really don't want to highlight the characters 
6046             # changed, but instead leave the cursor where it is. 
6047             # Also, there a situations in which the difference can be ambiguous; 
6050             # current value:    11234 
6051             # previous value:   1111234 
6053             # Where did the cursor actually lie and which 1s were selected on the delete 
6056             # Also, difflib can "get it wrong;" Consider: 
6058             # current value:    "       128.66" 
6059             # previous value:   "       121.86" 
6061             # difflib produces the following opcodes, which are sub-optimal: 
6062             #    equal value[0:9] (       12) prev[0:9] (       12) 
6063             #   insert value[9:9] () prev[9:11] (1.) 
6064             #    equal value[9:10] (8) prev[11:12] (8) 
6065             #   delete value[10:11] (.) prev[12:12] () 
6066             #    equal value[11:12] (6) prev[12:13] (6) 
6067             #   delete value[12:13] (6) prev[13:13] () 
6069             # This should have been: 
6070             #    equal value[0:9] (       12) prev[0:9] (       12) 
6071             #  replace value[9:11] (8.6) prev[9:11] (1.8) 
6072             #    equal value[12:13] (6) prev[12:13] (6) 
6074             # But it didn't figure this out! 
6076             # To get all this right, we use the previous selection recorded to help us... 
6078             if (sel_start
, sel_to
) != self
._prevSelection
: 
6079 ##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) 
6081                 prev_sel_start
, prev_sel_to 
= self
._prevSelection
 
6082                 field 
= self
._FindField
(sel_start
) 
6084                       and sel_start 
< self
._masklength
 
6085                       and (prev
[sel_start
] in ('-', '(', ')') 
6086                                      or value
[sel_start
] in ('-', '(', ')')) ): 
6087                     # change of sign; leave cursor alone... 
6088 ##                    dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')')) 
6089 ##                    dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')')) 
6090 ##                    dbg('setting selection to previous one') 
6091                     sel_start
, sel_to 
= self
._prevSelection
 
6093                 elif field
._groupdigits 
and (value
[sel_start
:sel_to
] == field
._groupChar
 
6094                                              or prev
[sel_start
:sel_to
] == field
._groupChar
): 
6095                     # do not highlight grouping changes 
6096 ##                    dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar) 
6097 ##                    dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar) 
6098 ##                    dbg('setting selection to previous one') 
6099                     sel_start
, sel_to 
= self
._prevSelection
 
6102                     calc_select_len 
= sel_to 
- sel_start
 
6103                     prev_select_len 
= prev_sel_to 
- prev_sel_start
 
6105 ##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) 
6106 ##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) 
6108                     if prev_select_len 
>= calc_select_len
: 
6109                         # old selection was bigger; trust it: 
6110 ##                        dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len) 
6111                         if not field
._insertRight
: 
6112 ##                            dbg('setting selection to previous one') 
6113                             sel_start
, sel_to 
= self
._prevSelection
 
6115                             sel_to 
= self
._prevSelection
[1] 
6116 ##                            dbg('setting selection to', (sel_start, sel_to)) 
6118                     elif( sel_to 
> prev_sel_to                  
# calculated select past last selection 
6119                           and prev_sel_to 
< len(self
._template
) # and prev_sel_to not at end of control 
6120                           and sel_to 
== len(self
._template
) ):  # and calculated selection goes to end of control 
6122                         i
, j
, k 
= sm
.find_longest_match(prev_sel_to
, length
, prev_sel_to
, length
) 
6123 ##                        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] ) 
6125                             # difflib must not have optimized opcodes properly; 
6129                         # look for possible ambiguous diff: 
6131                         # if last change resulted in no selection, test from resulting cursor position: 
6132                         if prev_sel_start 
== prev_sel_to
: 
6133                             calc_select_len 
= sel_to 
- sel_start
 
6134                             field 
= self
._FindField
(prev_sel_start
) 
6136                             # determine which way to search from last cursor position for ambiguous change: 
6137                             if field
._insertRight
: 
6138                                 test_sel_start 
= prev_sel_start
 
6139                                 test_sel_to 
= prev_sel_start 
+ calc_select_len
 
6141                                 test_sel_start 
= prev_sel_start 
- calc_select_len
 
6142                                 test_sel_to 
= prev_sel_start
 
6144                             test_sel_start
, test_sel_to 
= prev_sel_start
, prev_sel_to
 
6146 ##                        dbg('test selection:', (test_sel_start, test_sel_to)) 
6147 ##                        dbg('calc change: "%s"' % prev[sel_start:sel_to]) 
6148 ##                        dbg('test change: "%s"' % prev[test_sel_start:test_sel_to]) 
6150                         # if calculated selection spans characters, and same characters 
6151                         # "before" the previous insertion point are present there as well, 
6152                         # select the ones related to the last known selection instead. 
6153                         if( sel_start 
!= sel_to
 
6154                             and test_sel_to 
< len(self
._template
) 
6155                             and prev
[test_sel_start
:test_sel_to
] == prev
[sel_start
:sel_to
] ): 
6157                             sel_start
, sel_to 
= test_sel_start
, test_sel_to
 
6159                 # finally, make sure that the old and new values are 
6160                 # different where we say they're different: 
6161                 while( sel_to 
- 1 > 0 
6162                         and sel_to 
> sel_start
 
6163                         and value
[sel_to
-1:] == prev
[sel_to
-1:]): 
6165                 while( sel_start 
+ 1 < self
._masklength
 
6166                         and sel_start 
< sel_to
 
6167                         and value
[:sel_start
+1] == prev
[:sel_start
+1]): 
6170 ##            dbg('sel_start, sel_to:', sel_start, sel_to) 
6171 ##            dbg('previous value: "%s"' % prev) 
6173             if just_return_results
: 
6174                 return prev
, (sel_start
, sel_to
) 
6176             self
._SetValue
(prev
) 
6177             self
._SetInsertionPoint
(sel_start
) 
6178             self
._SetSelection
(sel_start
, sel_to
) 
6181 ##            dbg('no difference between previous value') 
6183             if just_return_results
: 
6184                 return prev
, self
._GetSelection
() 
6187     def _OnClear(self
, event
): 
6188         """ Provides an action for context menu delete operation """ 
6192     def _OnContextMenu(self
, event
): 
6193 ##        dbg('MaskedEditMixin::OnContextMenu()', indent=1) 
6195         menu
.Append(wx
.ID_UNDO
, "Undo", "") 
6196         menu
.AppendSeparator() 
6197         menu
.Append(wx
.ID_CUT
, "Cut", "") 
6198         menu
.Append(wx
.ID_COPY
, "Copy", "") 
6199         menu
.Append(wx
.ID_PASTE
, "Paste", "") 
6200         menu
.Append(wx
.ID_CLEAR
, "Delete", "") 
6201         menu
.AppendSeparator() 
6202         menu
.Append(wx
.ID_SELECTALL
, "Select All", "") 
6204         wx
.EVT_MENU(menu
, wx
.ID_UNDO
, self
._OnCtrl
_Z
) 
6205         wx
.EVT_MENU(menu
, wx
.ID_CUT
, self
._OnCtrl
_X
) 
6206         wx
.EVT_MENU(menu
, wx
.ID_COPY
, self
._OnCtrl
_C
) 
6207         wx
.EVT_MENU(menu
, wx
.ID_PASTE
, self
._OnCtrl
_V
) 
6208         wx
.EVT_MENU(menu
, wx
.ID_CLEAR
, self
._OnClear
) 
6209         wx
.EVT_MENU(menu
, wx
.ID_SELECTALL
, self
._OnCtrl
_A
) 
6211         # ## WSS: The base control apparently handles 
6212         # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE 
6213         # and wx.ID_CLEAR menu items even if the menu is one 
6214         # we created.  However, it doesn't do undo properly, 
6215         # so we're keeping track of previous values ourselves. 
6216         # Therefore, we have to override the default update for 
6217         # that item on the menu: 
6218         wx
.EVT_UPDATE_UI(self
, wx
.ID_UNDO
, self
._UndoUpdateUI
) 
6219         self
._contextMenu 
= menu
 
6221         self
.PopupMenu(menu
, event
.GetPosition()) 
6223         self
._contextMenu 
= None 
6226     def _UndoUpdateUI(self
, event
): 
6227         if self
._prevValue 
is None or self
._prevValue 
== self
._curValue
: 
6228             self
._contextMenu
.Enable(wx
.ID_UNDO
, False) 
6230             self
._contextMenu
.Enable(wx
.ID_UNDO
, True) 
6233     def _OnCtrlParametersChanged(self
): 
6235         Overridable function to allow derived classes to take action as a 
6236         result of parameter changes prior to possibly changing the value 
6241  ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6242 class MaskedEditAccessorsMixin
: 
6244     To avoid a ton of boiler-plate, and to automate the getter/setter generation 
6245     for each valid control parameter so we never forget to add the functions when 
6246     adding parameters, this class programmatically adds the masked edit mixin 
6247     parameters to itself. 
6248     (This makes it easier for Designers like Boa to deal with masked controls.) 
6250     To further complicate matters, this is done with an extra level of inheritance, 
6251     so that "general" classes like masked.TextCtrl can have all possible attributes, 
6252     while derived classes, like masked.TimeCtrl and masked.NumCtrl can prevent 
6253     exposure of those optional attributes of their base class that do not make 
6254     sense for their derivation. 
6256     Therefore, we define: 
6257         BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) 
6259         masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). 
6261     This allows us to then derive: 
6262         masked.NumCtrl( BaseMaskedTextCtrl ) 
6264     and not have to expose all the same accessor functions for the 
6265     derived control when they don't all make sense for it. 
6269     # Define the default set of attributes exposed by the most generic masked controls: 
6270     exposed_basectrl_params 
= MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys() 
6271     exposed_basectrl_params
.remove('index') 
6272     exposed_basectrl_params
.remove('extent') 
6273     exposed_basectrl_params
.remove('foregroundColour')   # (base class already has this) 
6275     for param 
in exposed_basectrl_params
: 
6276         propname 
= param
[0].upper() + param
[1:] 
6277         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
6278         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
6280         if param.find('Colour
') != -1: 
6281             # add non-british spellings, for backward-compatibility 
6282             propname.replace('Colour
', 'Color
') 
6284             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
6285             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
6290 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6291 ## these are helper subroutines: 
6293 def _movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): 
6294     """ addseparators = add separator character every three numerals if True 
6296     fmt0 = fmtstring.split('.') 
6299     val  = origvalue.split('.')[0].strip() 
6300     ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) 
6303     return (ret,len(fmt1)) 
6306 def _isDateType( fmtstring ): 
6307     """ Checks the mask and returns True if it fits an allowed 
6308         date or datetime format. 
6310     dateMasks = ("^##/##/####", 
6322     reString  = "|".join(dateMasks) 
6323     filter = re.compile( reString) 
6324     if re.match(filter,fmtstring): return True 
6327 def _isTimeType( fmtstring ): 
6328     """ Checks the mask and returns True if it fits an allowed 
6331     reTimeMask = "^##:##(:##)?( (AM|PM))?" 
6332     filter = re.compile( reTimeMask ) 
6333     if re.match(filter,fmtstring): return True 
6337 def _isFloatingPoint( fmtstring): 
6338     filter = re.compile("[ ]?[#]+\.[#]+\n") 
6339     if re.match(filter,fmtstring+"\n"): return True 
6343 def _isInteger( fmtstring ): 
6344     filter = re.compile("[#]+\n") 
6345     if re.match(filter,fmtstring+"\n"): return True 
6349 def _getDateParts( dateStr, dateFmt ): 
6350     if len(dateStr) > 11: clip = dateStr[0:11] 
6351     else:                 clip = dateStr 
6352     if clip[-2] not in string.digits: 
6353         clip = clip[:-1]    # (got part of time; drop it) 
6355     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6356     slices  = clip.split(dateSep) 
6357     if dateFmt == "MDY": 
6358         y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts 
6359     elif dateFmt == "DMY": 
6360         y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts 
6361     elif dateFmt == "YMD": 
6362         y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts 
6364         y,m,d = None, None, None 
6371 def _getDateSepChar(dateStr): 
6372     clip   = dateStr[0:10] 
6373     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6377 def _makeDate( year, month, day, dateFmt, dateStr): 
6378     sep    = _getDateSepChar( dateStr) 
6379     if dateFmt == "MDY": 
6380         return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts 
6381     elif dateFmt == "DMY": 
6382         return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts 
6383     elif dateFmt == "YMD": 
6384         return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts 
6389 def _getYear(dateStr,dateFmt): 
6390     parts = _getDateParts( dateStr, dateFmt) 
6393 def _getMonth(dateStr,dateFmt): 
6394     parts = _getDateParts( dateStr, dateFmt) 
6397 def _getDay(dateStr,dateFmt): 
6398     parts = _getDateParts( dateStr, dateFmt) 
6401 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6402 class __test(wx.PySimpleApp): 
6404             from wx.lib.rcsizer import RowColSizer 
6405             self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) 
6406             self.panel = wx.Panel( self.frame, -1) 
6407             self.sizer = RowColSizer() 
6412             id, id1 = wx.NewId(), wx.NewId() 
6413             self.command1  = wx.Button( self.panel, id, "&Close" ) 
6414             self.command2  = wx.Button( self.panel, id1, "&AutoFormats" ) 
6415             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6416             self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) 
6417             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) 
6418 ##            self.panel.SetDefaultItem(self.command1 ) 
6419             self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) 
6421             self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) 
6422             self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) 
6423             self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) 
6424             self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) 
6425             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) 
6426             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) 
6429             label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. 
6430 Note that all controls have been auto-sized by including F in the format code. 
6431 Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" 
6432             label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." 
6434             self.label1 = wx.StaticText( self.panel, -1, label) 
6435             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6436             self.label3 = wx.StaticText( self.panel, -1, "Mask Value") 
6437             self.label4 = wx.StaticText( self.panel, -1, "Format") 
6438             self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") 
6439             self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") 
6440             self.label7 = wx.StaticText( self.panel, -1, label2) 
6441             self.label7.SetForegroundColour("Blue") 
6442             self.label1.SetForegroundColour("Blue") 
6443             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6444             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6445             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6446             self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6447             self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6449             self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) 
6450             self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) 
6451             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6452             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6453             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6454             self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) 
6455             self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) 
6457             # The following list is of the controls for the demo. Feel free to play around with 
6460             #description        mask                    excl format     regexp                              range,list,initial 
6461            ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''), 
6462            ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''), 
6463            ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''), 
6464            ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''), 
6465            ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''), 
6466            ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'), 
6467            ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''), 
6468            ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'), 
6469            ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''), 
6470            ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '), 
6471            ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '), 
6472            ("Month selector",   "XXX",                  "", 'F',        "",                                 (), 
6473                 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), 
6474            ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (), 
6475                 ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") 
6478             for control in controls: 
6479                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6480                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6481                 self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) 
6482                 self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) 
6484                 if control in controls[:]:#-2]: 
6485                     newControl  = MaskedTextCtrl( self.panel, -1, "", 
6487                                                     excludeChars = control[2], 
6488                                                     formatcodes  = control[3], 
6490                                                     validRegex   = control[4], 
6491                                                     validRange   = control[5], 
6492                                                     choices      = control[6], 
6493                                                     defaultValue = control[7], 
6495                     if control[6]: newControl.SetCtrlParameters(choiceRequired = True) 
6497                     newControl = MaskedComboBox(  self.panel, -1, "", 
6498                                                     choices = control[7], 
6499                                                     choiceRequired  = True, 
6501                                                     formatcodes  = control[3], 
6502                                                     excludeChars = control[2], 
6504                                                     validRegex   = control[4], 
6505                                                     validRange   = control[5], 
6507                 self.editList.append( newControl ) 
6509                 self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) 
6512             self.sizer.AddGrowableCol(4) 
6514             self.panel.SetSizer(self.sizer) 
6515             self.panel.SetAutoLayout(1) 
6522         def onClick(self, event): 
6525         def onClickPage(self, event): 
6526             self.page2 = __test2(self.frame,-1,"") 
6527             self.page2.Show(True) 
6529         def _onCheck1(self,event): 
6530             """ Set required value on/off """ 
6531             value = event.IsChecked() 
6533                 for control in self.editList: 
6534                     control.SetCtrlParameters(emptyInvalid=True) 
6537                 for control in self.editList: 
6538                     control.SetCtrlParameters(emptyInvalid=False) 
6540             self.panel.Refresh() 
6542         def _onCheck2(self,event): 
6543             """ Highlight empty values""" 
6544             value = event.IsChecked() 
6546                 for control in self.editList: 
6547                     control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') 
6550                 for control in self.editList: 
6551                     control.SetCtrlParameters( emptyBackgroundColour = 'White') 
6553             self.panel.Refresh() 
6556 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6558 class __test2(wx.Frame): 
6559         def __init__(self, parent, id, caption): 
6560             wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) 
6561             from wx.lib.rcsizer import RowColSizer 
6562             self.panel = wx.Panel( self, -1) 
6563             self.sizer = RowColSizer() 
6569 All these controls have been created by passing a single parameter, the AutoFormat code. 
6570 The class contains an internal dictionary of types and formats (autoformats). 
6571 To see a great example of validations in action, try entering a bad email address, then tab out.""" 
6573             self.label1 = wx.StaticText( self.panel, -1, label) 
6574             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6575             self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") 
6576             self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") 
6577             self.label1.SetForegroundColour("Blue") 
6578             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6579             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6580             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6582             self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) 
6583             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6584             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6585             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6587             id, id1 = wx.NewId(), wx.NewId() 
6588             self.command1  = wx.Button( self.panel, id, "&Close") 
6589             self.command2  = wx.Button( self.panel, id1, "&Print Formats") 
6590             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) 
6591             self.panel.SetDefaultItem(self.command1) 
6592             self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) 
6594             # The following list is of the controls for the demo. Feel free to play around with 
6597            ("Phone No","USPHONEFULLEXT"), 
6598            ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), 
6599            ("US Date MMDDYYYY","USDATEMMDDYYYY/"), 
6600            ("Time (with seconds)","TIMEHHMMSS"), 
6601            ("Military Time\n(without seconds)","24HRTIMEHHMM"), 
6602            ("Social Sec#","USSOCIALSEC"), 
6603            ("Credit Card","CREDITCARD"), 
6604            ("Expiration MM/YY","EXPDATEMMYY"), 
6605            ("Percentage","PERCENT"), 
6606            ("Person's Age","AGE"), 
6607            ("US Zip Code","USZIP"), 
6608            ("US Zip+4","USZIPPLUS4"), 
6609            ("Email Address","EMAIL"), 
6610            ("IP Address", "(derived control IpAddrCtrl)") 
6613             for control in controls: 
6614                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6615                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6616                 if control in controls[:-1]: 
6617                     self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", 
6618                                                       autoformat  = control[1], 
6620                                 row=rowcount,col=2,flag=wx.ALL,border=5) 
6622                     self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), 
6623                                     row=rowcount,col=2,flag=wx.ALL,border=5) 
6626             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6627             self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) 
6628             self.sizer.AddGrowableCol(3) 
6630             self.panel.SetSizer(self.sizer) 
6631             self.panel.SetAutoLayout(1) 
6633         def onClick(self, event): 
6636         def onClickPrint(self, event): 
6637             for format in masktags.keys(): 
6638                 sep = "+------------------------+" 
6639                 print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) 
6641 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6643 if __name__ == "__main__": 
6649 ## =================================== 
6651 ## 1. WS: For some reason I don't understand, the control is generating two (2) 
6652 ##      EVT_TEXT events for every one (1) .SetValue() of the underlying control. 
6653 ##      I've been unsuccessful in determining why or in my efforts to make just one 
6654 ##      occur.  So, I've added a hack to save the last seen value from the 
6655 ##      control in the EVT_TEXT handler, and if *different*, call event.Skip() 
6656 ##      to propagate it down the event chain, and let the application see it. 
6658 ## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the 
6659 ##      behavior of the underlying control that I can't fix.  The problems are: 
6660 ##      a) The background coloring doesn't work in the text field of the control; 
6661 ##         instead, there's a only border around it that assumes the correct color. 
6662 ##      b) The control will not pass WXK_TAB to the event handler, no matter what 
6663 ##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to 
6664 ##         indicate that we want these events.  As a result, MaskedComboBox 
6665 ##         doesn't do the nice field-tabbing that MaskedTextCtrl does. 
6666 ##      c) Auto-complete had to be reimplemented for the control because programmatic 
6667 ##         setting of the value of the text field does not set up the auto complete 
6668 ##         the way that the control processing keystrokes does.  (But I think I've 
6669 ##         implemented a fairly decent approximation.)  Because of this the control 
6670 ##         also won't auto-complete on dropdown, and there's no event I can catch 
6671 ##         to work around this problem. 
6672 ##      d) There is no method provided for getting the selection; the hack I've 
6673 ##         implemented has its flaws, not the least of which is that due to the 
6674 ##         strategy that I'm using, the paste buffer is always replaced by the 
6675 ##         contents of the control's selection when in focus, on each keystroke; 
6676 ##         this makes it impossible to paste anything into a MaskedComboBox 
6677 ##         at the moment... :-( 
6678 ##      e) The other deficient behavior, likely induced by the workaround for (d), 
6679 ##         is that you can can't shift-left to select more than one character 
6683 ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their 
6684 ##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in 
6685 ##      wxWindows, shift-tab won't take you backwards through the fields of 
6686 ##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will 
6687 ##      work like shift-tab and tab ought to. 
6691 ## =============================## 
6692 ##  1. Add Popup list for auto-completable fields that simulates combobox on individual 
6693 ##     fields.  Example: City validates against list of cities, or zip vs zip code list. 
6694 ##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" 
6696 ##  3. Fix shift-left selection for MaskedComboBox. 
6697 ##  5. Transform notion of "decimal control" to be less "entire control"-centric, 
6698 ##     so that monetary symbols can be included and still have the appropriate 
6699 ##     semantics.  (Big job, as currently written, but would make control even 
6700 ##     more useful for business applications.) 
6704 ## ==================== 
6706 ##  1. Added value member to ValueError exceptions, so that people can catch them 
6707 ##     and then display their own errors, and added attribute raiseOnInvalidPaste, 
6708 ##     so one doesn't have to subclass the controls simply to force generation of 
6709 ##     a ValueError on a bad paste operation. 
6710 ##  2. Fixed handling of unicode charsets by converting to explicit control char 
6711 ##     set testing for passing those keystrokes to the base control, and then  
6712 ##     changing the semantics of the * maskchar to indicate any visible char. 
6713 ##  3. Added '|' mask specification character, which allows splitting of contiguous 
6714 ##     mask characters into separate fields, allowing finer control of behavior 
6719 ##  1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete 
6720 ##     cuts, shift-insert pastes, and ctrl-insert copies. 
6723 ##  1. Now ignores kill focus events when being destroyed. 
6724 ##  2. Added missing call to set insertion point on changing fields. 
6725 ##  3. Modified SetKeyHandler() to accept None as means of removing one. 
6726 ##  4. Fixed keyhandler processing for group and decimal character changes. 
6727 ##  5. Fixed a problem that prevented input into the integer digit of a  
6728 ##     integerwidth=1 numctrl, if the current value was 0. 
6729 ##  6. Fixed logic involving processing of "_signOk" flag, to remove default 
6730 ##     sign key handlers if false, so that SetAllowNegative(False) in the 
6731 ##     NumCtrl works properly. 
6732 ##  7. Fixed selection logic for numeric controls so that if selectOnFieldEntry 
6733 ##     is true, and the integer portion of an integer format control is selected 
6734 ##     and the sign position is selected, the sign keys will always result in a 
6735 ##     negative value, rather than toggling the previous sign. 
6739 ##  1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail. 
6740 ##  2. Added proper support for unicode version of wxPython 
6741 ##  3. Added * as mask char meaning "all ansi chars" (ordinals 32-255). 
6742 ##  4. Converted doc strings to use reST format, for ePyDoc documentation. 
6743 ##  5. Renamed helper functions, classes, etc. not intended to be visible in public 
6744 ##     interface to code. 
6747 ##  1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead 
6748 ##     shifts the text to the left accordingly. 
6749 ##  2. Fixed _SetValue() to place cursor after last character inserted, rather than end of 
6751 ##  3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes 
6752 ##     (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping 
6754 ##  4. Fixed autoselect behavior to work similarly to (2) above, so that combobox 
6755 ##     selection will only select the non-empty text, as per request. 
6756 ##  5. Fixed tabbing to work with 2.5.2 semantics. 
6757 ##  6. Fixed size calculation to handle changing fonts 
6760 ##  1. Reorganized masked controls into separate package, renamed things accordingly 
6761 ##  2. Split actual controls out of this file into their own files. 
6763 ##  (Reported) bugs fixed: 
6764 ##   1. Crash ensues if you attempt to change the mask of a read-only 
6765 ##      MaskedComboBox after initial construction. 
6766 ##   2. Changed strategy of defining Get/Set property functions so that 
6767 ##      these are now generated dynamically at runtime, rather than as 
6768 ##      part of the class definition.  (This makes it possible to have 
6769 ##      more general base classes that have many more options for configuration 
6770 ##      without requiring that derivations support the same options.) 
6771 ##   3. Fixed IsModified for _Paste() and _OnErase(). 
6774 ##   1. Fixed "attribute function inheritance," since base control is more 
6775 ##      generic than subsequent derivations, not all property functions of a 
6776 ##      generic control should be exposed in those derivations.  New strategy 
6777 ##      uses base control classes (eg. BaseMaskedTextCtrl) that should be 
6778 ##      used to derive new class types, and mixed with their own mixins to 
6779 ##      only expose those attributes from the generic masked controls that 
6780 ##      make sense for the derivation.  (This makes Boa happier.) 
6781 ##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less 
6785 ##  (Reported) bugs fixed: 
6786 ##   1. Right-click menu allowed "cut" operation that destroyed mask 
6787 ##      (was implemented by base control) 
6788 ##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all 
6789 ##      got converted to lower case. 
6790 ##   3. MaskedComboBox selection didn't deal with spaces in values 
6791 ##      properly when autocompleting, and didn't have a concept of "next" 
6792 ##      match for handling choice list duplicates. 
6793 ##   4. Size of MaskedComboBox was always default. 
6794 ##   5. Email address regexp allowed some "non-standard" things, and wasn't 
6796 ##   6. Couldn't easily reset MaskedComboBox contents programmatically. 
6797 ##   7. Couldn't set emptyInvalid during construction. 
6798 ##   8. Under some versions of wxPython, readonly comboboxes can apparently 
6799 ##      return a GetInsertionPoint() result (655535), causing masked control 
6801 ##   9. Specifying an empty mask caused the controls to traceback. 
6802 ##  10. Can't specify float ranges for validRange. 
6803 ##  11. '.' from within a the static portion of a restricted IP address 
6804 ##      destroyed the mask from that point rightward; tab when cursor is 
6805 ##      before 1st field takes cursor past that field. 
6808 ##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) 
6809 ##  13. Added auto-select option on char input for masked controls with 
6811 ##  14. Added '>' formatcode, allowing insert within a given or each field 
6812 ##      as appropriate, rather than requiring "overwrite".  This makes single 
6813 ##      field controls that just have validation rules (eg. EMAIL) much more 
6814 ##      friendly.  The same flag controls left shift when deleting vs just 
6815 ##      blanking the value, and for right-insert fields, allows right-insert 
6816 ##      at any non-blank (non-sign) position in the field. 
6817 ##  15. Added option to use to indicate negative values for numeric controls. 
6818 ##  16. Improved OnFocus handling of numeric controls. 
6819 ##  17. Enhanced Home/End processing to allow operation on a field level, 
6821 ##  18. Added individual Get/Set functions for control parameters, for 
6822 ##      simplified integration with Boa Constructor. 
6823 ##  19. Standardized "Colour" parameter names to match wxPython, with 
6824 ##      non-british spellings still supported for backward-compatibility. 
6825 ##  20. Added '&' mask specification character for punctuation only (no letters 
6827 ##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide 
6828 ##      unified interface to the masked edit subclasses. 
6832 ##   1. Made it possible to configure grouping, decimal and shift-decimal characters, 
6833 ##      to make controls more usable internationally. 
6834 ##   2. Added code to smart "adjust" value strings presented to .SetValue() 
6835 ##      for right-aligned numeric format controls if they are shorter than 
6836 ##      than the control width,  prepending the missing portion, prepending control 
6837 ##      template left substring for the missing characters, so that setting 
6838 ##      numeric values is easier. 
6839 ##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved 
6840 ##      for b-c), as this makes more sense. 
6843 ##   1. Fixed .SetValue() to replace the current value, rather than the current 
6844 ##      selection. Also changed it to generate ValueError if presented with 
6845 ##      either a value which doesn't follow the format or won't fit.  Also made 
6846 ##      set value adjust numeric and date controls as if user entered the value. 
6847 ##      Expanded doc explaining how SetValue() works. 
6848 ##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to 
6849 ##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. 
6850 ##   3. Made all date autoformats automatically pick implied "datestyle". 
6851 ##   4. Added IsModified override, since base wx.TextCtrl never reports modified if 
6852 ##      .SetValue used to change the value, which is what the masked edit controls 
6854 ##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when 
6855 ##      using tab to "leave field" and auto-adjust. 
6856 ##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, 
6857 ##      and bug in ._Paste() that didn't account for signs in signed masks either. 
6858 ##   7. Fixed issues with _adjustPos for right-insert fields causing improper 
6859 ##      selection/replacement of values 
6860 ##   8. Fixed _OnHome handler to properly handle extending current selection to 
6861 ##      beginning of control. 
6862 ##   9. Exposed all (valid) autoformats to demo, binding descriptions to 
6864 ##  10. Fixed a couple of bugs in email regexp. 
6865 ##  11. Made maskchardict an instance var, to make mask chars to be more 
6866 ##      amenable to international use. 
6867 ##  12. Clarified meaning of '-' formatcode in doc. 
6868 ##  13. Fixed a couple of coding bugs being flagged by Python2.1. 
6869 ##  14. Fixed several issues with sign positioning, erasure and validity 
6870 ##      checking for "numeric" masked controls. 
6871 ##  15. Added validation to IpAddrCtrl.SetValue(). 
6874 ##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) 
6875 ##      vs. literal font facename, and use wxTELETYPE as the font family 
6877 ##   2. Switched to use of dbg module vs. locally defined version. 
6878 ##   3. Revamped entire control structure to use Field classes to hold constraint 
6879 ##      and formatting data, to make code more hierarchical, allow for more 
6880 ##      sophisticated masked edit construction. 
6881 ##   4. Better strategy for managing options, and better validation on keywords. 
6882 ##   5. Added 'V' format code, which requires that in order for a character 
6883 ##      to be accepted, it must result in a string that passes the validRegex. 
6884 ##   6. Added 'S' format code which means "select entire field when navigating 
6886 ##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) 
6887 ##   8. Added '<' format code to allow fields to require explicit cursor movement 
6889 ##   9. Added validFunc option to other validation mechanisms, that allows derived 
6890 ##      classes to add dynamic validation constraints to the control. 
6891 ##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also 
6892 ##      fixed failure to obey case conversion codes when pasting. 
6893 ##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... 
6894 ##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' 
6895 ##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, 
6896 ##      individual field validation to prevent field values > 255, and require explicit 
6897 ##      tab/. to change fields. 
6898 ##  14. Added handler for left double-click to select field under cursor. 
6899 ##  15. Fixed handling for "Read-only" styles. 
6900 ##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor 
6901 ##      attribute, for more consistent and controllable coloring. 
6902 ##  17. Added retainFieldValidation parameter, allowing top-level constraints 
6903 ##      such as "validRequired" to be set independently of field-level equivalent. 
6904 ##      (needed in TimeCtrl for bounds constraints.) 
6905 ##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed 
6906 ##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, 
6908 ##  19. Fixed maskchar setting for upper/lowercase, to work in all locales. 
6912 ##   1. Decimal point behavior restored for decimal and integer type controls: 
6913 ##      decimal point now trucates the portion > 0. 
6914 ##   2. Return key now works like the tab character and moves to the next field, 
6915 ##      provided no default button is set for the form panel on which the control 
6917 ##   3. Support added in _FindField() for subclasses controls (like timecontrol) 
6918 ##      to determine where the current insertion point is within the mask (i.e. 
6919 ##      which sub-'field'). See method documentation for more info and examples. 
6920 ##   4. Added Field class and support for all constraints to be field-specific 
6921 ##      in addition to being globally settable for the control. 
6922 ##      Choices for each field are validated for length and pastability into 
6923 ##      the field in question, raising ValueError if not appropriate for the control. 
6924 ##      Also added selective additional validation based on individual field constraints. 
6925 ##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all 
6926 ##      auto-complete fields with choice lists, supplying the 1st entry in 
6927 ##      the choice list if the field is empty, and cycling through the list in 
6928 ##      the appropriate direction if already a match.  WXK_DOWN will also auto- 
6929 ##      complete if the field is partially completed and a match can be made. 
6930 ##      SHIFT-WXK_UP/DOWN will also take you to the next field after any 
6931 ##      auto-completion performed. 
6932 ##   5. Added autoCompleteKeycodes=[] parameters for allowing further 
6933 ##      customization of the control.  Any keycode supplied as a member 
6934 ##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If 
6935 ##      requireFieldChoice is set, then a valid value from each non-empty 
6936 ##      choice list will be required for the value of the control to validate. 
6937 ##   6. Fixed "auto-sizing" to be relative to the font actually used, rather 
6938 ##      than making assumptions about character width. 
6939 ##   7. Fixed GetMaskParameter(), which was non-functional in previous version. 
6940 ##   8. Fixed exceptions raised to provide info on which control had the error. 
6941 ##   9. Fixed bug in choice management of MaskedComboBox. 
6942 ##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of 
6943 ##     the form '# #'.  Modified control code for IpAddrCtrl so that '.' 
6944 ##     in the middle of a field clips the rest of that field, similar to 
6945 ##     decimal and integer controls. 
6949 ##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. 
6950 ##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). 
6951 ##   3. New support for selecting text within the control.(thanks Will Sadkin!) 
6952 ##      Shift-End and Shift-Home now select text as you would expect 
6953 ##      Control-Shift-End selects to the end of the mask string, even if value not entered. 
6954 ##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control. 
6955 ##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- 
6956 ##      for some reason I couldn't find the original email but thanks!!!) 
6957 ##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome, 
6958 ##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. 
6959 ##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). 
6960 ##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) 
6961 ##   7. New mechanism for replacing default behavior for any given key, using 
6962 ##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available 
6963 ##      for easier subclassing of the control. 
6964 ##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs 
6965 ##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut" 
6966 ##      semantics, erasing the selection, rather than erasing the entire control. 
6967 ##   9. Added option for an "default value" (ie. the template) for use when a single fillChar 
6968 ##      is not desired in every position.  Added IsDefault() function to mean "does the value 
6969 ##      equal the template?" and modified .IsEmpty() to mean "do all of the editable 
6970 ##      positions in the template == the fillChar?" 
6971 ##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, 
6973 ##  11. MaskedComboBox now adds the capability to validate from list of valid values. 
6974 ##      Example: City validates against list of cities, or zip vs zip code list. 
6975 ##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being 
6976 ##      passed to the next handler in the event chain, causing updates to the 
6977 ##      control to be invisible to the parent code. 
6978 ##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within 
6979 ##      the control, that auto-reformats as you move between cells. 
6980 ##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. 
6981 ##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} 
6982 ##  16. Fixed major bugs in date validation, due to the fact that 
6983 ##      wxDateTime.ParseDate is too liberal, and will accept any form that 
6984 ##      makes any kind of sense, regardless of the datestyle you specified 
6985 ##      for the control.  Unfortunately, the strategy used to fix it only 
6986 ##      works for versions of wxPython post 2.3.3.1, as a C++ assert box 
6987 ##      seems to show up on an invalid date otherwise, instead of a catchable 
6989 ##  17. Enhanced date adjustment to automatically adjust heuristic based on 
6990 ##      current year, making last century/this century determination on 
6991 ##      2-digit year based on distance between today's year and value; 
6992 ##      if > 50 year separation, assume last century (and don't assume last 
6993 ##      century is 20th.) 
6994 ##  18. Added autoformats and support for including HHMMSS as well as HHMM for 
6995 ##      date times, and added similar time, and militaray time autoformats. 
6996 ##  19. Enhanced tabbing logic so that tab takes you to the next field if the 
6997 ##      control is a multi-field control. 
6998 ##  20. Added stub method called whenever the control "changes fields", that 
6999 ##      can be overridden by subclasses (eg. IpAddrCtrl.) 
7000 ##  21. Changed a lot of code to be more functionally-oriented so side-effects 
7001 ##      aren't as problematic when maintaining code and/or adding features. 
7002 ##      Eg: IsValid() now does not have side-effects; it merely reflects the 
7003 ##      validity of the value of the control; to determine validity AND recolor 
7004 ##      the control, _CheckValid() should be used with a value argument of None. 
7005 ##      Similarly, made most reformatting function take an optional candidate value 
7006 ##      rather than just using the current value of the control, and only 
7007 ##      have them change the value of the control if a candidate is not specified. 
7008 ##      In this way, you can do validation *before* changing the control. 
7009 ##  22. Changed validRequired to mean "disallow chars that result in invalid 
7010 ##      value."  (Old meaning now represented by emptyInvalid.)  (This was 
7011 ##      possible once I'd made the changes in (19) above.) 
7012 ##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they 
7013 ##      can be set/modified/retrieved after construction.  Removed individual 
7014 ##      parameter setting functions, in favor of this mechanism, so that 
7015 ##      all adjustment of the control based on changing parameter values can 
7016 ##      be handled in one place with unified mechanism. 
7017 ##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability 
7018 ##      to type "grouping char" (ie. ',') and validate as appropriate. 
7019 ##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. 
7020 ##  26. Fixed assumption about "decimal or integer" masks so that they're only 
7021 ##      made iff there's no validRegex associated with the field.  (This 
7022 ##      is so things like zipcodes which look like integers can have more 
7023 ##      restrictive validation (ie. must be 5 digits.) 
7024 ##  27. Added a ton more doc strings to explain use and derivation requirements 
7025 ##      and did regularization of the naming conventions. 
7026 ##  28. Fixed a range bug in _adjustKey preventing z from being handled properly. 
7027 ##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to 
7028 ##      reformat the value and move the next field as appropriate. (shift-'.', 
7029 ##      ie. '>' moves to the previous field. 
7032 ##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending 
7034 ##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the 
7035 ##      year position, the control will expand the value to four digits, using numerals below 
7036 ##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). 
7037 ##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} 
7038 ##   3. revalid parameter renamed validRegex to conform to standard for all validation 
7039 ##      parameters (see 2 new ones below). 
7040 ##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types. 
7041 ##      Allows the developer to specify a valid low/high range of values. 
7042 ##   5. New optional init parameter = validList. Used for character types. Allows developer 
7043 ##      to send a list of values to the control to be used for specific validation. 
7044 ##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams. 
7045 ##   6. Date type fields now use wxDateTime's parser to validate the date and time. 
7046 ##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing 
7047 ##      me toward this solution! 
7048 ##   7. Date fields now automatically expand 2-digit years when it can. For example, 
7049 ##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year 
7050 ##      date is entered it will be expanded in any case when the user tabs out of the 
7052 ##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, 
7053 ##      SetSignedForeColor allow accessto override default class coloring behavior. 
7054 ##   9. Documentation updated and improved. 
7055 ##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. 
7056 ##      Two new options (checkboxes) - test highlight empty and disallow empty. 
7057 ##  11. Home and End now work more intuitively, moving to the first and last user-entry 
7058 ##      value, respectively. 
7059 ##  12. New class function: SetRequired(bool). Sets the control's entry required flag 
7060 ##      (i.e. disallow empty values if True). 
7063 ##   1. get_plainValue method renamed to GetPlainValue following the wxWindows 
7064 ##      StudlyCaps(tm) standard (thanks Paul Moore).  ;) 
7065 ##   2. New format code 'F' causes the control to auto-fit (auto-size) itself 
7066 ##      based on the length of the mask template. 
7067 ##   3. Class now supports "autoformat" codes. These can be passed to the class 
7068 ##      on instantiation using the parameter autoformat="code". If the code is in 
7069 ##      the dictionary, it will self set the mask, formatting, and validation string. 
7070 ##      I have included a number of samples, but I am hoping that someone out there 
7071 ##      can help me to define a whole bunch more. 
7072 ##   4. I have added a second page to the demo (as well as a second demo class, test2) 
7073 ##      to showcase how autoformats work. The way they self-format and self-size is, 
7074 ##      I must say, pretty cool. 
7075 ##   5. Comments added and some internal cosmetic revisions re: matching the code 
7076 ##      standards for class submission. 
7077 ##   6. Regex validation is now done in real time - field turns yellow immediately 
7078 ##      and stays yellow until the entered value is valid 
7079 ##   7. Cursor now skips over template characters in a more intuitive way (before the 
7081 ##   8. Change, Keypress and LostFocus methods added for convenience of subclasses. 
7082 ##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, 
7083 ##      and EVT_KILL_FOCUS, respectively. 
7084 ##   9. Decimal and numeric handlers have been rewritten and now work more intuitively. 
7087 ##   1. New .IsEmpty() method returns True if the control's value is equal to the 
7088 ##      blank template string 
7089 ##   2. Control now supports a new init parameter: revalid. Pass a regular expression 
7090 ##      that the value will have to match when the control loses focus. If invalid, 
7091 ##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next). 
7092 ##   3. Demo now shows revalid functionality. Try entering a partial value, such as a 
7093 ##      partial social security number. 
7094 ##   4. New .IsValid() value returns True if the control is empty, or if the value matches 
7095 ##      the revalid expression. If not, .IsValid() returns False. 
7096 ##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never 
7097 ##      presses the decimal point. 
7098 ##   6. Cursor now goes to the beginning of the field if the user clicks in an 
7099 ##      "empty" field intead of leaving the insertion point in the middle of the 
7101 ##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. 
7102 ##   8. New formatcodes init parameter replaces other init params and adds functions. 
7103 ##      String passed to control on init controls: 
7107 ##        R Show negative #s in red 
7109 ##        - Signed numerals 
7110 ##        0 Numeric fields get leading zeros 
7111 ##   9. Ctrl-X in any field clears the current value. 
7112 ##   10. Code refactored and made more modular (esp in OnChar method). Should be more 
7113 ##       easy to read and understand. 
7114 ##   11. Demo enhanced. 
7115 ##   12. Now has _doc_. 
7118 ##   1. GetPlainValue() now returns the value without the template characters; 
7119 ##      so, for example, a social security number (123-33-1212) would return as 
7120 ##      123331212; also removes white spaces from numeric/decimal values, so 
7121 ##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value. 
7122 ##   2. Press '.' in an integer style masked control and truncate any trailing digits. 
7123 ##   3. Code moderately refactored. Internal names improved for clarity. Additional 
7124 ##      internal documentation. 
7125 ##   4. Home and End keys now supported to move cursor to beginning or end of field. 
7126 ##   5. Un-signed integers and decimals now supported. 
7127 ##   6. Cosmetic improvements to the demo. 
7128 ##   7. Class renamed to MaskedTextCtrl. 
7129 ##   8. Can now specify include characters that will override the basic 
7130 ##      controls: for example, includeChars = "@." for email addresses 
7131 ##   9. Added mask character 'C' -> allow any upper or lowercase character 
7132 ##   10. .SetSignColor(str:color) sets the foreground color for negative values 
7133 ##       in signed controls (defaults to red) 
7134 ##   11. Overview documentation written. 
7137 ##   1. Tab now works properly when pressed in last position 
7138 ##   2. Decimal types now work (e.g. #####.##) 
7139 ##   3. Signed decimal or numeric values supported (i.e. negative numbers) 
7140 ##   4. Negative decimal or numeric values now can show in red. 
7141 ##   5. Can now specify an "exclude list" with the excludeChars parameter. 
7142 ##      See date/time formatted example - you can only enter A or P in the 
7143 ##      character mask space (i.e. AM/PM). 
7144 ##   6. Backspace now works properly, including clearing data from a selected 
7145 ##      region but leaving template characters intact. Also delete key. 
7146 ##   7. Left/right arrows now work properly. 
7147 ##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3