1 #---------------------------------------------------------------------------- 
   3 # Authors:      Jeff Childers, Will Sadkin 
   4 # Email:        jchilders_98@yahoo.com, wsadkin@nameconnector.com 
   6 # Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003 
   7 # Portions:     (c) 2002 by Will Sadkin, 2002-2003 
   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 
  59     is a sublassed text control that can carefully control the user's input 
  60     based on a mask string you provide. 
  62     General usage example:: 
  64         control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####') 
  66     The example above will create a text control that allows only numbers to be 
  67     entered and then only in the positions indicated in the mask by the # sign. 
  70     is a similar subclass of wxComboBox that allows the same sort of masking, 
  71     but also can do auto-complete of values, and can require the value typed 
  72     to be in the list of choices to be colored appropriately. 
  75     is actually a factory function for several types of masked edit controls: 
  77     =================   ================================================== 
  78     masked.TextCtrl     standard masked edit text box 
  79     masked.ComboBox     adds combobox capabilities 
  80     masked.IpAddrCtrl   adds special semantics for IP address entry 
  81     masked.TimeCtrl     special subclass handling lots of types as values 
  82     masked.NumCtrl      special subclass handling numeric values 
  83     =================   ================================================== 
  85     It works by looking for a *controlType* parameter in the keyword 
  86     arguments of the control, to determine what kind of instance to return. 
  87     If not specified as a keyword argument, the default control type returned 
  88     will be masked.TextCtrl. 
  90     Each of the above classes has its own set of arguments, but masked.Ctrl 
  91     provides a single "unified" interface for masked controls.  Those for 
  92     masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented 
  93     below; the others have their own demo pages and interface descriptions. 
  94     (See end of following discussion for how to configure the wx.MaskedCtrl() 
  95     to select the above control types.) 
  97 ========================= 
  99 Initialization Parameters 
 100 ------------------------- 
 102     Allowed mask characters and function: 
 104     =========  ========================================================== 
 106     =========  ========================================================== 
 107         #       Allow numeric only (0-9) 
 108         N       Allow letters and numbers (0-9) 
 109         A       Allow uppercase letters only 
 110         a       Allow lowercase letters only 
 111         C       Allow any letter, upper or lower 
 112         X       Allow string.letters, string.punctuation, string.digits 
 113         &       Allow string.punctuation only 
 114         \*       Allow any ansi character 
 115     =========  ========================================================== 
 118     These controls define these sets of characters using string.letters, 
 119     string.uppercase, etc.  These sets are affected by the system locale 
 120     setting, so in order to have the masked controls accept characters 
 121     that are specific to your users' language, your application should 
 123     For example, to allow international characters to be used in the 
 124     above masks, you can place the following in your code as part of 
 125     your application's initialization code:: 
 128       locale.setlocale(locale.LC_ALL, '') 
 130     The controls now also support (by popular demand) all "ansi" chars, 
 131     that is, all ascii codes between 32 and 255, by use of the * mask character. 
 133   Using these mask characters, a variety of template masks can be built. See 
 134   the demo for some other common examples include date+time, social security 
 135   number, etc.  If any of these characters are needed as template rather 
 136   than mask characters, they can be escaped with \, ie. \N means "literal N". 
 137   (use \\ for literal backslash, as in: r'CCC\\NNN'.) 
 141       Masks containing only # characters and one optional decimal point 
 142       character are handled specially, as "numeric" controls.  Such 
 143       controls have special handling for typing the '-' key, handling 
 144       the "decimal point" character as truncating the integer portion, 
 145       optionally allowing grouping characters and so forth. 
 146       There are several parameters and format codes that only make sense 
 147       when combined with such masks, eg. groupChar, decimalChar, and so 
 148       forth (see below).  These allow you to construct reasonable 
 149       numeric entry controls. 
 152       Changing the mask for a control deletes any previous field classes 
 153       (and any associated validation or formatting constraints) for them. 
 156   By default, masked edit controls use a fixed width font, so that 
 157   the mask characters are fixed within the control, regardless of 
 158   subsequent modifications to the value.  Set to False if having 
 159   the control font be the same as other controls is required. (This is 
 160   a control-level parameter.) 
 163   (Applies to unicode systems only) By default, the default unicode encoding 
 164   used is latin1, or iso-8859-1.  If necessary, you can set this control-level 
 165   parameter to govern the codec used to decode your keyboard inputs. 
 166   (This is a control-level parameter.) 
 169   These other properties can be passed to the class when instantiating it: 
 170     Formatcodes are specified as a string of single character formatting 
 171     codes that modify  behavior of the control:: 
 176             R  Right-align field(s) 
 177             r  Right-insert in field(s) (implies R) 
 178             <  Stay in field until explicit navigation out of it 
 180             >  Allow insert/delete within partially filled fields (as 
 181                opposed to the default "overwrite" mode for fixed-width 
 182                masked edit controls.)  This allows single-field controls 
 183                or each field within a multi-field control to optionally 
 184                behave more like standard text controls. 
 185                (See EMAIL or phone number autoformat examples.) 
 187                *Note: This also governs whether backspace/delete operations 
 188                shift contents of field to right of cursor, or just blank the 
 191                Also, when combined with 'r', this indicates that the field 
 192                or control allows right insert anywhere within the current 
 193                non-empty value in the field.  (Otherwise right-insert behavior 
 194                is only performed to when the entire right-insertable field is 
 195                selected or the cursor is at the right edge of the field.* 
 198             ,  Allow grouping character in integer fields of numeric controls 
 199                and auto-group/regroup digits (if the result fits) when leaving 
 200                such a field.  (If specified, .SetValue() will attempt to 
 202                ',' is also the default grouping character.  To change the 
 203                grouping character and/or decimal character, use the groupChar 
 204                and decimalChar parameters, respectively. 
 205                Note: typing the "decimal point" character in such fields will 
 206                clip the value to that left of the cursor for integer 
 207                fields of controls with "integer" or "floating point" masks. 
 208                If the ',' format code is specified, this will also cause the 
 209                resulting digits to be regrouped properly, using the current 
 211             -  Prepend and reserve leading space for sign to mask and allow 
 212                signed values (negative #s shown in red by default.) Can be 
 213                used with argument useParensForNegatives (see below.) 
 214             0  integer fields get leading zeros 
 217             F  Auto-Fit: the control calulates its size from 
 218                the length of the template mask 
 219             V  validate entered chars against validRegex before allowing them 
 220                to be entered vs. being allowed by basic mask and then having 
 221                the resulting value just colored as invalid. 
 222                (See USSTATE autoformat demo for how this can be used.) 
 223             S  select entire field when navigating to new field 
 228   These controls have two options for the initial state of the control. 
 229   If a blank control with just the non-editable characters showing 
 230   is desired, simply leave the constructor variable fillChar as its 
 231   default (' ').  If you want some other character there, simply 
 232   change the fillChar to that value.  Note: changing the control's fillChar 
 233   will implicitly reset all of the fields' fillChars to this value. 
 235   If you need different default characters in each mask position, 
 236   you can specify a defaultValue parameter in the constructor, or 
 237   set them for each field individually. 
 238   This value must satisfy the non-editable characters of the mask, 
 239   but need not conform to the replaceable characters. 
 244   These parameters govern what character is used to group numbers 
 245   and is used to indicate the decimal point for numeric format controls. 
 246   The default groupChar is ',', the default decimalChar is '.' 
 247   By changing these, you can customize the presentation of numbers 
 252         formatcodes = ',', groupChar='\''                  allows  12'345.34 
 253         formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34 
 255   (These are control-level parameters.) 
 258   The default "shiftDecimalChar" (used for "backwards-tabbing" until 
 259   shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for 
 260   other keyboards, you may want to customize this, eg '?' for shift ',' on 
 261   AZERTY keyboards, ':' or ';' for other European keyboards, etc. 
 262   (This is a control-level parameter.) 
 264 useParensForNegatives=False 
 265   This option can be used with signed numeric format controls to 
 266   indicate signs via () rather than '-'. 
 267   (This is a control-level parameter.) 
 270   This option can be used to have a field or the control try to 
 271   auto-complete on each keystroke if choices have been specified. 
 273 autoCompleteKeycodes=[] 
 274   By default, DownArrow, PageUp and PageDown will auto-complete a 
 275   partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp 
 276   and PageDown will also auto-complete, but if the field already 
 277   contains a matched value, these keys will cycle through the list 
 278   of choices forward or backward as appropriate.  Shift-Up and 
 279   Shift-Down also take you to the next/previous field after any 
 280   auto-complete action. 
 282   Additional auto-complete keys can be specified via this parameter. 
 283   Any keys so specified will act like PageDown. 
 284   (This is a control-level parameter.) 
 288 Validating User Input 
 289 ===================== 
 290 There are a variety of initialization parameters that are used to validate 
 291 user input.  These parameters can apply to the control as a whole, and/or 
 292 to individual fields: 
 294         =====================  ================================================================== 
 295         excludeChars           A string of characters to exclude even if otherwise allowed 
 296         includeChars           A string of characters to allow even if otherwise disallowed 
 297         validRegex             Use a regular expression to validate the contents of the text box 
 298         validRange             Pass a rangeas list (low,high) to limit numeric fields/values 
 299         choices                A list of strings that are allowed choices for the control. 
 300         choiceRequired         value must be member of choices list 
 301         compareNoCase          Perform case-insensitive matching when validating against list 
 302                                *Note: for masked.ComboBox, this defaults to True.* 
 303         emptyInvalid           Boolean indicating whether an empty value should be considered  
 306         validFunc              A function to call of the form: bool = func(candidate_value) 
 307                                which will return True if the candidate_value satisfies some 
 308                                external criteria for the control in addition to the the 
 309                                other validation, or False if not.  (This validation is 
 310                                applied last in the chain of validations.) 
 312         validRequired          Boolean indicating whether or not keys that are allowed by the 
 313                                mask, but result in an invalid value are allowed to be entered 
 314                                into the control.  Setting this to True implies that a valid 
 315                                default value is set for the control. 
 317         retainFieldValidation  False by default; if True, this allows individual fields to 
 318                                retain their own validation constraints independently of any 
 319                                subsequent changes to the control's overall parameters. 
 320                                (This is a control-level parameter.) 
 322         validator              Validators are not normally needed for masked controls, because 
 323                                of the nature of the validation and control of input.  However, 
 324                                you can supply one to provide data transfer routines for the 
 326         =====================  ================================================================== 
 331   The following parameters have been provided to allow you to change the default 
 332   coloring behavior of the control.   These can be set at construction, or via 
 333   the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow': 
 335         ========================  ======================================================================= 
 336         emptyBackgroundColour      Control Background color when identified as empty. Default=White 
 337         invalidBackgroundColour    Control Background color when identified as Not valid. Default=Yellow 
 338         validBackgroundColour      Control Background color when identified as Valid. Default=white 
 339         ========================  ======================================================================= 
 342   The following parameters control the default foreground color coloring behavior of the 
 343   control. Pass a color as string e.g. 'Yellow': 
 345         ========================  ====================================================================== 
 346         foregroundColour           Control foreground color when value is not negative.  Default=Black 
 347         signedForegroundColour     Control foreground color when value is negative. Default=Red 
 348         ========================  ====================================================================== 
 353   Each part of the mask that allows user input is considered a field.  The fields 
 354   are represented by their own class instances.  You can specify field-specific 
 355   constraints by constructing or accessing the field instances for the control 
 356   and then specifying those constraints via parameters. 
 359   This parameter allows you to specify Field instances containing 
 360   constraints for the individual fields of a control, eg: local 
 361   choice lists, validation rules, functions, regexps, etc. 
 362   It can be either an ordered list or a dictionary.  If a list, 
 363   the fields will be applied as fields 0, 1, 2, etc. 
 364   If a dictionary, it should be keyed by field index. 
 365   the values should be a instances of maskededit.Field. 
 367   Any field not represented by the list or dictionary will be 
 368   implicitly created by the control. 
 372     fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ] 
 377                1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']), 
 378                3: ( Field(choices=['01', '02', '03'], choiceRequired=True) 
 381   The following parameters are available for individual fields, with the 
 382   same semantics as for the whole control but applied to the field in question: 
 384     ==============  ============================================================================= 
 385     fillChar        if set for a field, it will override the control's fillChar for that field 
 386     groupChar       if set for a field, it will override the control's default 
 387     defaultValue    sets field-specific default value; overrides any default from control 
 388     compareNoCase   overrides control's settings 
 389     emptyInvalid    determines whether field is required to be filled at all times 
 390     validRequired   if set, requires field to contain valid value 
 391     ==============  ============================================================================= 
 393   If any of the above parameters are subsequently specified for the control as a 
 394   whole, that new value will be propagated to each field, unless the 
 395   retainFieldValidation control-level parameter is set. 
 397     ==============  ============================== 
 398     formatcodes      Augments control's settings 
 406     ==============  ============================== 
 410 Control Class Functions 
 411 ======================= 
 412 .GetPlainValue(value=None) 
 413                     Returns the value specified (or the control's text value 
 414                     not specified) without the formatting text. 
 415                     In the example above, might return phone no='3522640075', 
 416                     whereas control.GetValue() would return '(352) 264-0075' 
 418                     Returns the control's value to its default, and places the 
 419                     cursor at the beginning of the control. 
 421                     Does "smart replacement" of passed value into the control, as does 
 422                     the .Paste() method.  As with other text entry controls, the 
 423                     .SetValue() text replacement begins at left-edge of the control, 
 424                     with missing mask characters inserted as appropriate. 
 425                     .SetValue will also adjust integer, float or date mask entry values, 
 426                     adding commas, auto-completing years, etc. as appropriate. 
 427                     For "right-aligned" numeric controls, it will also now automatically 
 428                     right-adjust any value whose length is less than the width of the 
 429                     control before attempting to set the value. 
 430                     If a value does not follow the format of the control's mask, or will 
 431                     not fit into the control, a ValueError exception will be raised. 
 435                       mask = '(###) ###-####' 
 436                           .SetValue('1234567890')           => '(123) 456-7890' 
 437                           .SetValue('(123)4567890')         => '(123) 456-7890' 
 438                           .SetValue('(123)456-7890')        => '(123) 456-7890' 
 439                           .SetValue('123/4567-890')         => illegal paste; ValueError 
 441                       mask = '#{6}.#{2}', formatcodes = '_,-', 
 442                           .SetValue('111')                  => ' 111   .  ' 
 443                           .SetValue(' %9.2f' % -111.12345 ) => '   -111.12' 
 444                           .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00' 
 445                           .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError 
 447                       mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control 
 448                           .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit 
 449                           .SetValue('%.2f' % 111 )          => '    111.00' 
 450                           .SetValue('%.2f' % -111.12345 )   => '   -111.12' 
 454                     Returns True if the value specified (or the value of the control 
 455                     if not specified) passes validation tests 
 457                     Returns True if the value specified (or the value of the control 
 458                     if not specified) is equal to an "empty value," ie. all 
 459                     editable characters == the fillChar for their respective fields. 
 460 .IsDefault(value=None) 
 461                     Returns True if the value specified (or the value of the control 
 462                     if not specified) is equal to the initial value of the control. 
 465                     Recolors the control as appropriate to its current settings. 
 467 .SetCtrlParameters(\*\*kwargs) 
 468                     This function allows you to set up and/or change the control parameters 
 469                     after construction; it takes a list of key/value pairs as arguments, 
 470                     where the keys can be any of the mask-specific parameters in the constructor. 
 474                         ctl = masked.TextCtrl( self, -1 ) 
 475                         ctl.SetCtrlParameters( mask='###-####', 
 476                                                defaultValue='555-1212', 
 479 .GetCtrlParameter(parametername) 
 480                     This function allows you to retrieve the current value of a parameter 
 483   *Note:* Each of the control parameters can also be set using its 
 484       own Set and Get function.  These functions follow a regular form: 
 485       All of the parameter names start with lower case; for their 
 486       corresponding Set/Get function, the parameter name is capitalized. 
 490           ctl.SetMask('###-####') 
 491           ctl.SetDefaultValue('555-1212') 
 492           ctl.GetChoiceRequired() 
 495   *Note:* After any change in parameters, the choices for the 
 496       control are reevaluated to ensure that they are still legal.  If you 
 497       have large choice lists, it is therefore more efficient to set parameters 
 498       before setting the choices available. 
 500 .SetFieldParameters(field_index, \*\*kwargs) 
 501                     This function allows you to specify change individual field 
 502                     parameters after construction. (Indices are 0-based.) 
 504 .GetFieldParameter(field_index, parametername) 
 505                     Allows the retrieval of field parameters after construction 
 508 The control detects certain common constructions. In order to use the signed feature 
 509 (negative numbers and coloring), the mask has to be all numbers with optionally one 
 510 decimal point. Without a decimal (e.g. '######', the control will treat it as an integer 
 511 value. With a decimal (e.g. '###.##'), the control will act as a floating point control 
 512 (i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the 
 513 integer control truncates the value.  However, for a true numeric control, 
 514 masked.NumCtrl provides all this, and true numeric input/output support as well. 
 517 Check your controls by calling each control's .IsValid() function and the 
 518 .IsEmpty() function to determine which controls have been a) filled in and 
 519 b) filled in properly. 
 522 Regular expression validations can be used flexibly and creatively. 
 523 Take a look at the demo; the zip-code validation succeeds as long as the 
 524 first five numerals are entered. the last four are optional, but if 
 525 any are entered, there must be 4 to be valid. 
 527 masked.Ctrl Configuration 
 528 ========================= 
 529 masked.Ctrl works by looking for a special *controlType* 
 530 parameter in the variable arguments of the control, to determine 
 531 what kind of instance to return. 
 532 controlType can be one of:: 
 540 These constants are also available individually, ie, you can 
 541 use either of the following:: 
 543     from wxPython.wx.lib.masked import MaskedCtrl, controlTypes 
 544     from wxPython.wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR 
 546 If not specified as a keyword argument, the default controlType is 
 552 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
 557   All methods of the Mixin that are not meant to be exposed to the external 
 558   interface are prefaced with '_'.  Those functions that are primarily 
 559   intended to be internal subroutines subsequently start with a lower-case 
 560   letter; those that are primarily intended to be used and/or overridden 
 561   by derived subclasses start with a capital letter. 
 563   The following methods must be used and/or defined when deriving a control 
 564   from MaskedEditMixin.  NOTE: if deriving from a *masked edit* control 
 565   (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary, 
 566   as it's already been done for you in the base class. 
 569                         This function must be called after the associated base 
 570                         control has been initialized in the subclass __init__ 
 571                         function.  It sets the initial value of the control, 
 572                         either to the value specified if non-empty, the 
 573                         default value if specified, or the "template" for 
 574                         the empty control as necessary.  It will also set/reset 
 575                         the font if necessary and apply formatting to the 
 576                         control at this time. 
 580                         Each class derived from MaskedEditMixin must define 
 581                         the function for getting the start and end of the 
 582                         current text selection.  The reason for this is 
 583                         that not all controls have the same function name for 
 584                         doing this; eg. wx.TextCtrl uses .GetSelection(), 
 585                         whereas we had to write a .GetMark() function for 
 586                         wxComboBox, because .GetSelection() for the control 
 587                         gets the currently selected list item from the combo 
 588                         box, and the control doesn't (yet) natively provide 
 589                         a means of determining the text selection. 
 592                         Similarly to _GetSelection, each class derived from 
 593                         MaskedEditMixin must define the function for setting 
 594                         the start and end of the current text selection. 
 595                         (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for 
 598         ._GetInsertionPoint() 
 599         ._SetInsertionPoint() 
 601                         For consistency, and because the mixin shouldn't rely 
 602                         on fixed names for any manipulations it does of any of 
 603                         the base controls, we require each class derived from 
 604                         MaskedEditMixin to define these functions as well. 
 607         ._SetValue()    REQUIRED 
 608                         Each class derived from MaskedEditMixin must define 
 609                         the functions used to get and set the raw value of the 
 611                         This is necessary so that recursion doesn't take place 
 612                         when setting the value, and so that the mixin can 
 613                         call the appropriate function after doing all its 
 614                         validation and manipulation without knowing what kind 
 615                         of base control it was mixed in with.  To handle undo 
 616                         functionality, the ._SetValue() must record the current 
 617                         selection prior to setting the value. 
 623                         Each class derived from MaskedEditMixin must redefine 
 624                         these functions to call the _Cut(), _Paste(), _Undo() 
 625                         and _SetValue() methods, respectively for the control, 
 626                         so as to prevent programmatic corruption of the control's 
 627                         value.  This must be done in each derivation, as the 
 628                         mixin cannot itself override a member of a sibling class. 
 631                         Each class derived from MaskedEditMixin must define 
 632                         the function used to refresh the base control. 
 635                         Each class derived from MaskedEditMixin must redefine 
 636                         this function so that it checks the validity of the 
 637                         control (via self._CheckValid) and then refreshes 
 638                         control using the base class method. 
 640         ._IsEditable()  REQUIRED 
 641                         Each class derived from MaskedEditMixin must define 
 642                         the function used to determine if the base control is 
 643                         editable or not.  (For masked.ComboBox, this has to 
 644                         be done with code, rather than specifying the proper 
 645                         function in the base control, as there isn't one...) 
 646         ._CalcSize()    REQUIRED 
 647                         Each class derived from MaskedEditMixin must define 
 648                         the function used to determine how wide the control 
 649                         should be given the mask.  (The mixin function 
 650                         ._calcSize() provides a baseline estimate.) 
 655   Event handlers are "chained", and MaskedEditMixin usually 
 656   swallows most of the events it sees, thereby preventing any other 
 657   handlers from firing in the chain.  It is therefore required that 
 658   each class derivation using the mixin to have an option to hook up 
 659   the event handlers itself or forego this operation and let a 
 660   subclass of the masked control do so.  For this reason, each 
 661   subclass should probably include the following code: 
 663     if setupEventHandling: 
 664         ## Setup event handlers 
 665         EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection 
 666         EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator 
 667         EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick 
 668         EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu 
 669         EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab. 
 670         EVT_CHAR( self, self._OnChar )              ## handle each keypress 
 671         EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep 
 672                                                             ## track of previous value for undo 
 674   where setupEventHandling is an argument to its constructor. 
 676   These 5 handlers must be "wired up" for the masked edit 
 677   controls to provide default behavior.  (The setupEventHandling 
 678   is an argument to masked.TextCtrl and masked.ComboBox, so 
 679   that controls derived from *them* may replace one of these 
 680   handlers if they so choose.) 
 682   If your derived control wants to preprocess events before 
 683   taking action, it should then set up the event handling itself, 
 684   so it can be first in the event handler chain. 
 687   The following routines are available to facilitate changing 
 688   the default behavior of masked edit controls: 
 690         ._SetKeycodeHandler(keycode, func) 
 691         ._SetKeyHandler(char, func) 
 692                         Use to replace default handling for any given keycode. 
 693                         func should take the key event as argument and return 
 694                         False if no further action is required to handle the 
 696                             self._SetKeycodeHandler(WXK_UP, self.IncrementValue) 
 697                             self._SetKeyHandler('-', self._OnChangeSign) 
 699         (Setting a func of None removes any keyhandler for the given key.) 
 701         "Navigation" keys are assumed to change the cursor position, and 
 702         therefore don't cause automatic motion of the cursor as insertable 
 705         ._AddNavKeycode(keycode, handler=None) 
 706         ._AddNavKey(char, handler=None) 
 707                         Allows controls to specify other keys (and optional handlers) 
 708                         to be treated as navigational characters. (eg. '.' in IpAddrCtrl) 
 710         ._GetNavKeycodes()  Returns the current list of navigational keycodes. 
 712         ._SetNavKeycodes(key_func_tuples) 
 713                         Allows replacement of the current list of keycode 
 714                         processed as navigation keys, and bind associated 
 715                         optional keyhandlers. argument is a list of key/handler 
 716                         tuples.  Passing a value of None for the handler in a 
 717                         given tuple indicates that default processing for the key 
 720         ._FindField(pos) Returns the Field object associated with this position 
 723         ._FindFieldExtent(pos, getslice=False, value=None) 
 724                         Returns edit_start, edit_end of the field corresponding 
 725                         to the specified position within the control, and 
 726                         optionally also returns the current contents of that field. 
 727                         If value is specified, it will retrieve the slice the corresponding 
 728                         slice from that value, rather than the current value of the 
 732                         This is, the function that gets called for a given position 
 733                         whenever the cursor is adjusted to leave a given field. 
 734                         By default, it adjusts the year in date fields if mask is a date, 
 735                         It can be overridden by a derived class to 
 736                         adjust the value of the control at that time. 
 737                         (eg. IpAddrCtrl reformats the address in this way.) 
 739         ._Change()      Called by internal EVT_TEXT handler. Return False to force 
 740                         skip of the normal class change event. 
 741         ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force 
 742                         skip of the normal class keypress event. 
 743         ._LostFocus()   Called by internal EVT_KILL_FOCUS handler 
 746                         This is the default EVT_KEY_DOWN routine; it just checks for 
 747                         "navigation keys", and if event.ControlDown(), it fires the 
 748                         mixin's _OnChar() routine, as such events are not always seen 
 749                         by the "cooked" EVT_CHAR routine. 
 751         ._OnChar(event) This is the main EVT_CHAR handler for the 
 754     The following routines are used to handle standard actions 
 756         _OnArrow(event)         used for arrow navigation events 
 757         _OnCtrl_A(event)        'select all' 
 758         _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive) 
 759         _OnCtrl_S(event)        'save' (does nothing) 
 760         _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste 
 761         _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection 
 762         _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any) 
 764         _OnChangeField(event)   primarily used for tab events, but can be 
 765                                 used for other keys (eg. '.' in IpAddrCtrl) 
 767         _OnErase(event)         used for backspace and delete 
 771     The following routine provides a hook back to any class derivations, so that 
 772     they can react to parameter changes before any value is set/reset as a result of 
 773     those changes.  (eg. masked.ComboBox needs to detect when the choices list is 
 774     modified, either implicitly or explicitly, so it can reset the base control 
 775     to have the appropriate choice list *before* the initial value is reset to match.) 
 777         _OnCtrlParametersChanged() 
 781     For convenience, each class derived from MaskedEditMixin should 
 782     define an accessors mixin, so that it exposes only those parameters 
 783     that make sense for the derivation.  This is done with an intermediate 
 784     level of inheritance, ie: 
 786     class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): 
 788     class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): 
 789     class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): 
 790     class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): 
 791     class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): 
 792     class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): 
 796     Each accessors mixin defines Get/Set functions for the base class parameters 
 797     that are appropriate for that derivation. 
 798     This allows the base classes to be "more generic," exposing the widest 
 799     set of options, while not requiring derived classes to be so general. 
 810 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
 811 # be a good place to implement the 2.3 logger class 
 812 from wx
.tools
.dbg 
import Logger
 
 817 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 819 ## Constants for identifying control keys and classes of keys: 
 821 WXK_CTRL_A 
= (ord('A')+1) - ord('A')   ## These keys are not already defined in wx 
 822 WXK_CTRL_C 
= (ord('C')+1) - ord('A') 
 823 WXK_CTRL_S 
= (ord('S')+1) - ord('A') 
 824 WXK_CTRL_V 
= (ord('V')+1) - ord('A') 
 825 WXK_CTRL_X 
= (ord('X')+1) - ord('A') 
 826 WXK_CTRL_Z 
= (ord('Z')+1) - ord('A') 
 829     wx
.WXK_BACK
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
, wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_TAB
, 
 830     wx
.WXK_HOME
, wx
.WXK_END
, wx
.WXK_RETURN
, wx
.WXK_PRIOR
, wx
.WXK_NEXT
 
 834     wx
.WXK_BACK
, wx
.WXK_DELETE
, WXK_CTRL_A
, WXK_CTRL_C
, WXK_CTRL_S
, WXK_CTRL_V
, 
 835     WXK_CTRL_X
, WXK_CTRL_Z
 
 839 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 841 ## Constants for masking. This is where mask characters 
 843 ##  maskchars used to identify valid mask characters from all others 
 844 ##   #- allow numeric 0-9 only 
 845 ##   A- allow uppercase only. Combine with forceupper to force lowercase to upper 
 846 ##   a- allow lowercase only. Combine with forcelower to force upper to lowercase 
 847 ##   X- allow any character (string.letters, string.punctuation, string.digits) 
 848 ## Note: locale settings affect what "uppercase", lowercase, etc comprise. 
 850 maskchars 
= ("#","A","a","X","C","N",'*','&') 
 852 for i 
in xrange(32, 256): 
 855 months 
= '(01|02|03|04|05|06|07|08|09|10|11|12)' 
 856 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)' 
 857 charmonths_dict 
= {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 
 858                    'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} 
 860 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)' 
 861 hours  
= '(0\d| \d|1[012])' 
 862 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)' 
 863 minutes 
= """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ 
 864 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ 
 865 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ 
 868 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' 
 870 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(',') 
 872 state_names 
= ['Alabama','Alaska','Arizona','Arkansas', 
 873                'California','Colorado','Connecticut', 
 874                'Delaware','District of Columbia', 
 875                'Florida','Georgia','Hawaii', 
 876                'Idaho','Illinois','Indiana','Iowa', 
 877                'Kansas','Kentucky','Louisiana', 
 878                'Maine','Maryland','Massachusetts','Michigan', 
 879                'Minnesota','Mississippi','Missouri','Montana', 
 880                'Nebraska','Nevada','New Hampshire','New Jersey', 
 881                'New Mexico','New York','North Carolina','North Dakokta', 
 882                'Ohio','Oklahoma','Oregon', 
 883                'Pennsylvania','Puerto Rico','Rhode Island', 
 884                'South Carolina','South Dakota', 
 885                'Tennessee','Texas','Utah', 
 886                'Vermont','Virginia', 
 887                'Washington','West Virginia', 
 888                'Wisconsin','Wyoming'] 
 890 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 892 ## The following dictionary defines the current set of autoformats: 
 896            'mask': "(###) ###-#### x:###", 
 897            'formatcodes': 'F^->', 
 898            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 899            'description': "Phone Number w/opt. ext" 
 902            'mask': "###-###-#### x:###", 
 903            'formatcodes': 'F^->', 
 904            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 905            'description': "Phone Number\n (w/hyphens and opt. ext)" 
 908            'mask': "(###) ###-####", 
 909            'formatcodes': 'F^->', 
 910            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 911            'description': "Phone Number only" 
 914            'mask': "###-###-####", 
 915            'formatcodes': 'F^->', 
 916            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 917            'description': "Phone Number\n(w/hyphens)" 
 921            'formatcodes': 'F!V', 
 922            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(states
,'|'), 
 924            'choiceRequired': True, 
 925            'description': "US State Code" 
 928            'mask': "ACCCCCCCCCCCCCCCCCCC", 
 930            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(state_names
,'|'), 
 931            'choices': state_names
, 
 932            'choiceRequired': True, 
 933            'description': "US State Name" 
 936        "USDATETIMEMMDDYYYY/HHMMSS": { 
 937            'mask': "##/##/#### ##:##:## AM", 
 938            'excludeChars': am_pm_exclude
, 
 939            'formatcodes': 'DF!', 
 940            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 941            'description': "US Date + Time" 
 943        "USDATETIMEMMDDYYYY-HHMMSS": { 
 944            'mask': "##-##-#### ##:##:## AM", 
 945            'excludeChars': am_pm_exclude
, 
 946            'formatcodes': 'DF!', 
 947            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 948            'description': "US Date + Time\n(w/hypens)" 
 950        "USDATE24HRTIMEMMDDYYYY/HHMMSS": { 
 951            'mask': "##/##/#### ##:##:##", 
 953            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 954            'description': "US Date + 24Hr (Military) Time" 
 956        "USDATE24HRTIMEMMDDYYYY-HHMMSS": { 
 957            'mask': "##-##-#### ##:##:##", 
 959            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 960            'description': "US Date + 24Hr Time\n(w/hypens)" 
 962        "USDATETIMEMMDDYYYY/HHMM": { 
 963            'mask': "##/##/#### ##:## AM", 
 964            'excludeChars': am_pm_exclude
, 
 965            'formatcodes': 'DF!', 
 966            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 967            'description': "US Date + Time\n(without seconds)" 
 969        "USDATE24HRTIMEMMDDYYYY/HHMM": { 
 970            'mask': "##/##/#### ##:##", 
 972            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 973            'description': "US Date + 24Hr Time\n(without seconds)" 
 975        "USDATETIMEMMDDYYYY-HHMM": { 
 976            'mask': "##-##-#### ##:## AM", 
 977            'excludeChars': am_pm_exclude
, 
 978            'formatcodes': 'DF!', 
 979            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 980            'description': "US Date + Time\n(w/hypens and w/o secs)" 
 982        "USDATE24HRTIMEMMDDYYYY-HHMM": { 
 983            'mask': "##-##-#### ##:##", 
 985            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 986            'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" 
 989            'mask': "##/##/####", 
 991            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4}', 
 992            'description': "US Date\n(MMDDYYYY)" 
 997            'validRegex': '^' + months 
+ '/' + days 
+ '/\d\d', 
 998            'description': "US Date\n(MMDDYY)" 
1000        "USDATEMMDDYYYY-": { 
1001            'mask': "##-##-####", 
1002            'formatcodes': 'DF', 
1003            'validRegex': '^' + months 
+ '-' + days 
+ '-' +'\d{4}', 
1004            'description': "MM-DD-YYYY" 
1007        "EUDATEYYYYMMDD/": { 
1008            'mask': "####/##/##", 
1009            'formatcodes': 'DF', 
1010            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days
, 
1011            'description': "YYYY/MM/DD" 
1013        "EUDATEYYYYMMDD.": { 
1014            'mask': "####.##.##", 
1015            'formatcodes': 'DF', 
1016            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days
, 
1017            'description': "YYYY.MM.DD" 
1019        "EUDATEDDMMYYYY/": { 
1020            'mask': "##/##/####", 
1021            'formatcodes': 'DF', 
1022            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4}', 
1023            'description': "DD/MM/YYYY" 
1025        "EUDATEDDMMYYYY.": { 
1026            'mask': "##.##.####", 
1027            'formatcodes': 'DF', 
1028            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4}', 
1029            'description': "DD.MM.YYYY" 
1031        "EUDATEDDMMMYYYY.": { 
1032            'mask': "##.CCC.####", 
1033            'formatcodes': 'DF', 
1034            'validRegex': '^' + days 
+ '.' + charmonths 
+ '.' + '\d{4}', 
1035            'description': "DD.Month.YYYY" 
1037        "EUDATEDDMMMYYYY/": { 
1038            'mask': "##/CCC/####", 
1039            'formatcodes': 'DF', 
1040            'validRegex': '^' + days 
+ '/' + charmonths 
+ '/' + '\d{4}', 
1041            'description': "DD/Month/YYYY" 
1044        "EUDATETIMEYYYYMMDD/HHMMSS": { 
1045            'mask': "####/##/## ##:##:## AM", 
1046            'excludeChars': am_pm_exclude
, 
1047            'formatcodes': 'DF!', 
1048            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1049            'description': "YYYY/MM/DD HH:MM:SS" 
1051        "EUDATETIMEYYYYMMDD.HHMMSS": { 
1052            'mask': "####.##.## ##:##:## AM", 
1053            'excludeChars': am_pm_exclude
, 
1054            'formatcodes': 'DF!', 
1055            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1056            'description': "YYYY.MM.DD HH:MM:SS" 
1058        "EUDATETIMEDDMMYYYY/HHMMSS": { 
1059            'mask': "##/##/#### ##:##:## AM", 
1060            'excludeChars': am_pm_exclude
, 
1061            'formatcodes': 'DF!', 
1062            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1063            'description': "DD/MM/YYYY HH:MM:SS" 
1065        "EUDATETIMEDDMMYYYY.HHMMSS": { 
1066            'mask': "##.##.#### ##:##:## AM", 
1067            'excludeChars': am_pm_exclude
, 
1068            'formatcodes': 'DF!', 
1069            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1070            'description': "DD.MM.YYYY HH:MM:SS" 
1073        "EUDATETIMEYYYYMMDD/HHMM": { 
1074            'mask': "####/##/## ##:## AM", 
1075            'excludeChars': am_pm_exclude
, 
1076            'formatcodes': 'DF!', 
1077            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1078            'description': "YYYY/MM/DD HH:MM" 
1080        "EUDATETIMEYYYYMMDD.HHMM": { 
1081            'mask': "####.##.## ##:## AM", 
1082            'excludeChars': am_pm_exclude
, 
1083            'formatcodes': 'DF!', 
1084            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1085            'description': "YYYY.MM.DD HH:MM" 
1087        "EUDATETIMEDDMMYYYY/HHMM": { 
1088            'mask': "##/##/#### ##:## AM", 
1089            'excludeChars': am_pm_exclude
, 
1090            'formatcodes': 'DF!', 
1091            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1092            'description': "DD/MM/YYYY HH:MM" 
1094        "EUDATETIMEDDMMYYYY.HHMM": { 
1095            'mask': "##.##.#### ##:## AM", 
1096            'excludeChars': am_pm_exclude
, 
1097            'formatcodes': 'DF!', 
1098            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1099            'description': "DD.MM.YYYY HH:MM" 
1102        "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { 
1103            'mask': "####/##/## ##:##:##", 
1104            'formatcodes': 'DF', 
1105            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1106            'description': "YYYY/MM/DD 24Hr Time" 
1108        "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { 
1109            'mask': "####.##.## ##:##:##", 
1110            'formatcodes': 'DF', 
1111            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1112            'description': "YYYY.MM.DD 24Hr Time" 
1114        "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { 
1115            'mask': "##/##/#### ##:##:##", 
1116            'formatcodes': 'DF', 
1117            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1118            'description': "DD/MM/YYYY 24Hr Time" 
1120        "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { 
1121            'mask': "##.##.#### ##:##:##", 
1122            'formatcodes': 'DF', 
1123            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1124            'description': "DD.MM.YYYY 24Hr Time" 
1126        "EUDATE24HRTIMEYYYYMMDD/HHMM": { 
1127            'mask': "####/##/## ##:##", 
1128            'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1129            'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" 
1131        "EUDATE24HRTIMEYYYYMMDD.HHMM": { 
1132            'mask': "####.##.## ##:##", 
1133            'formatcodes': 'DF', 
1134            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1135            'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" 
1137        "EUDATE24HRTIMEDDMMYYYY/HHMM": { 
1138            'mask': "##/##/#### ##:##", 
1139            'formatcodes': 'DF', 
1140            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1141            'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" 
1143        "EUDATE24HRTIMEDDMMYYYY.HHMM": { 
1144            'mask': "##.##.#### ##:##", 
1145            'formatcodes': 'DF', 
1146            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1147            'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" 
1151            'mask': "##:##:## AM", 
1152            'excludeChars': am_pm_exclude
, 
1153            'formatcodes': 'TF!', 
1154            'validRegex': '^' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1155            'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" 
1159            'excludeChars': am_pm_exclude
, 
1160            'formatcodes': 'TF!', 
1161            'validRegex': '^' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1162            'description': "HH:MM (A|P)M\n(see TimeCtrl)" 
1166            'formatcodes': 'TF', 
1167            'validRegex': '^' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1168            'description': "24Hr HH:MM:SS\n(see TimeCtrl)" 
1172            'formatcodes': 'TF', 
1173            'validRegex': '^' + milhours 
+ ':' + minutes
, 
1174            'description': "24Hr HH:MM\n(see TimeCtrl)" 
1177            'mask': "###-##-####", 
1179            'validRegex': "\d{3}-\d{2}-\d{4}", 
1180            'description': "Social Sec#" 
1183            'mask': "####-####-####-####", 
1185            'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", 
1186            'description': "Credit Card" 
1191            'validRegex': "^" + months 
+ "/\d\d", 
1192            'description': "Expiration MM/YY" 
1197            'validRegex': "^\d{5}", 
1198            'description': "US 5-digit zip code" 
1201            'mask': "#####-####", 
1203            'validRegex': "\d{5}-(\s{4}|\d{4})", 
1204            'description': "US zip+4 code" 
1209            'validRegex': "^0.\d\d", 
1210            'description': "Percentage" 
1215            'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]", 
1216            'description': "Age" 
1219            'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 
1220            'excludeChars': " \\/*&%$#!+='\"", 
1221            'formatcodes': "F>", 
1222            '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}\]) *$", 
1223            'description': "Email address" 
1226            'mask': "###.###.###.###", 
1227            'formatcodes': 'F_Sr', 
1228            '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}", 
1229            'description': "IP Address\n(see IpAddrCtrl)" 
1233 # build demo-friendly dictionary of descriptions of autoformats 
1235 for key
, value 
in masktags
.items(): 
1236     autoformats
.append((key
, value
['description'])) 
1239 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1243     This class manages the individual fields in a masked edit control. 
1244     Each field has a zero-based index, indicating its position in the 
1245     control, an extent, an associated mask, and a plethora of optional 
1246     parameters.  Fields can be instantiated and then associated with 
1247     parent masked controls, in order to provide field-specific configuration. 
1248     Alternatively, fields will be implicitly created by the parent control 
1249     if not provided at construction, at which point, the fields can then 
1250     manipulated by the controls .SetFieldParameters() method. 
1253               'index': None,                    ## which field of mask; set by parent control. 
1254               'mask': "",                       ## mask chars for this field 
1255               'extent': (),                     ## (edit start, edit_end) of field; set by parent control. 
1256               'formatcodes':  "",               ## codes indicating formatting options for the control 
1257               'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given 
1258               'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits 
1259               'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction 
1260               'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard 
1261               'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. 
1262               'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar 
1263               'excludeChars': "",               ## optional string of chars to exclude even if main mask type does 
1264               'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't 
1265               'validRegex':   "",               ## optional regular expression to use to validate the control 
1266               'validRange':   (),               ## Optional hi-low range for numerics 
1267               'choices':    [],                 ## Optional list for character expressions 
1268               'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list 
1269               'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search 
1270               'autoSelect': False,              ## Set to True to try auto-completion on each keystroke: 
1271               'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl 
1272               'validRequired': False,           ## Set to True to disallow input that results in an invalid value 
1273               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID 
1274               'description': "",                ## primarily for autoformats, but could be useful elsewhere 
1277     # This list contains all parameters that when set at the control level should 
1278     # propagate down to each field: 
1279     propagating_params 
= ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', 
1280                           'compareNoCase', 'emptyInvalid', 'validRequired') 
1282     def __init__(self
, **kwargs
): 
1284         This is the "constructor" for setting up parameters for fields. 
1285         a field_index of -1 is used to indicate "the entire control." 
1287 ####        dbg('Field::Field', indent=1) 
1288         # Validate legitimate set of parameters: 
1289         for key 
in kwargs
.keys(): 
1290             if key 
not in Field
.valid_params
.keys(): 
1292                 raise TypeError('invalid parameter "%s"' % (key
)) 
1294         # Set defaults for each parameter for this instance, and fully 
1295         # populate initial parameter list for configuration: 
1296         for key
, value 
in Field
.valid_params
.items(): 
1297             setattr(self
, '_' + key
, copy
.copy(value
)) 
1298             if not kwargs
.has_key(key
): 
1299                 kwargs
[key
] = copy
.copy(value
) 
1301         self
._autoCompleteIndex 
= -1 
1302         self
._SetParameters
(**kwargs
) 
1303         self
._ValidateParameters
(**kwargs
) 
1308     def _SetParameters(self
, **kwargs
): 
1310         This function can be used to set individual or multiple parameters for 
1311         a masked edit field parameter after construction. 
1314 ##        dbg('maskededit.Field::_SetParameters', indent=1) 
1315         # Validate keyword arguments: 
1316         for key 
in kwargs
.keys(): 
1317             if key 
not in Field
.valid_params
.keys(): 
1318 ##                dbg(indent=0, suspend=0) 
1319                 raise AttributeError('invalid keyword argument "%s"' % key
) 
1321 ##        if self._index is not None: dbg('field index:', self._index) 
1322 ##        dbg('parameters:', indent=1) 
1323         for key
, value 
in kwargs
.items(): 
1324 ##            dbg('%s:' % key, value) 
1329         old_fillChar 
= self
._fillChar   
# store so we can change choice lists accordingly if it changes 
1331         # First, Assign all parameters specified: 
1332         for key 
in Field
.valid_params
.keys(): 
1333             if kwargs
.has_key(key
): 
1334                 setattr(self
, '_' + key
, kwargs
[key
] ) 
1336         if kwargs
.has_key('formatcodes'):   # (set/changed) 
1337             self
._forceupper  
= '!' in self
._formatcodes
 
1338             self
._forcelower  
= '^' in self
._formatcodes
 
1339             self
._groupdigits 
= ',' in self
._formatcodes
 
1340             self
._okSpaces    
= '_' in self
._formatcodes
 
1341             self
._padZero     
= '0' in self
._formatcodes
 
1342             self
._autofit     
= 'F' in self
._formatcodes
 
1343             self
._insertRight 
= 'r' in self
._formatcodes
 
1344             self
._allowInsert 
= '>' in self
._formatcodes
 
1345             self
._alignRight  
= 'R' in self
._formatcodes 
or 'r' in self
._formatcodes
 
1346             self
._moveOnFieldFull 
= not '<' in self
._formatcodes
 
1347             self
._selectOnFieldEntry 
= 'S' in self
._formatcodes
 
1349             if kwargs
.has_key('groupChar'): 
1350                 self
._groupChar 
= kwargs
['groupChar'] 
1351             if kwargs
.has_key('decimalChar'): 
1352                 self
._decimalChar 
= kwargs
['decimalChar'] 
1353             if kwargs
.has_key('shiftDecimalChar'): 
1354                 self
._shiftDecimalChar 
= kwargs
['shiftDecimalChar'] 
1356         if kwargs
.has_key('formatcodes') or kwargs
.has_key('validRegex'): 
1357             self
._regexMask   
= 'V' in self
._formatcodes 
and self
._validRegex
 
1359         if kwargs
.has_key('fillChar'): 
1360             self
._old
_fillChar 
= old_fillChar
 
1361 ####            dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1363         if kwargs
.has_key('mask') or kwargs
.has_key('validRegex'):  # (set/changed) 
1364             self
._isInt 
= _isInteger(self
._mask
) 
1365 ##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) 
1367 ##        dbg(indent=0, suspend=0) 
1370     def _ValidateParameters(self
, **kwargs
): 
1372         This function can be used to validate individual or multiple parameters for 
1373         a masked edit field parameter after construction. 
1376 ##        dbg('maskededit.Field::_ValidateParameters', indent=1) 
1377 ##        if self._index is not None: dbg('field index:', self._index) 
1378 ####        dbg('parameters:', indent=1) 
1379 ##        for key, value in kwargs.items(): 
1380 ####            dbg('%s:' % key, value) 
1382 ####        dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1384         # Verify proper numeric format params: 
1385         if self
._groupdigits 
and self
._groupChar 
== self
._decimalChar
: 
1386 ##            dbg(indent=0, suspend=0) 
1387             raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self
._groupChar
, self
._decimalChar
)) 
1390         # Now go do validation, semantic and inter-dependency parameter processing: 
1391         if kwargs
.has_key('choices') or kwargs
.has_key('compareNoCase') or kwargs
.has_key('choiceRequired'): # (set/changed) 
1393             self
._compareChoices 
= [choice
.strip() for choice 
in self
._choices
] 
1395             if self
._compareNoCase 
and self
._choices
: 
1396                 self
._compareChoices 
= [item
.lower() for item 
in self
._compareChoices
] 
1398             if kwargs
.has_key('choices'): 
1399                 self
._autoCompleteIndex 
= -1 
1402         if kwargs
.has_key('validRegex'):    # (set/changed) 
1403             if self
._validRegex
: 
1405                     if self
._compareNoCase
: 
1406                         self
._filter 
= re
.compile(self
._validRegex
, re
.IGNORECASE
) 
1408                         self
._filter 
= re
.compile(self
._validRegex
) 
1410 ##                    dbg(indent=0, suspend=0) 
1411                     raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self
._index
), self
._validRegex
)) 
1415         if kwargs
.has_key('validRange'):    # (set/changed) 
1416             self
._hasRange  
= False 
1419             if self
._validRange
: 
1420                 if type(self
._validRange
) != types
.TupleType 
or len( self
._validRange 
)!= 2 or self
._validRange
[0] > self
._validRange
[1]: 
1421 ##                    dbg(indent=0, suspend=0) 
1422                     raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' 
1423                                     % (str(self
._index
), repr(self
._validRange
)) ) 
1425                 self
._hasRange  
= True 
1426                 self
._rangeLow  
= self
._validRange
[0] 
1427                 self
._rangeHigh 
= self
._validRange
[1] 
1429         if kwargs
.has_key('choices') or (len(self
._choices
) and len(self
._choices
[0]) != len(self
._mask
)):       # (set/changed) 
1430             self
._hasList   
= False 
1431             if self
._choices 
and type(self
._choices
) not in (types
.TupleType
, types
.ListType
): 
1432 ##                dbg(indent=0, suspend=0) 
1433                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1434             elif len( self
._choices
) > 0: 
1435                 for choice 
in self
._choices
: 
1436                     if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
1437 ##                        dbg(indent=0, suspend=0) 
1438                         raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1440                 length 
= len(self
._mask
) 
1441 ##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) 
1442                 if len(self
._choices
) and length
: 
1443                     if len(self
._choices
[0]) > length
: 
1444                         # changed mask without respecifying choices; readjust the width as appropriate: 
1445                         self
._choices 
= [choice
.strip() for choice 
in self
._choices
] 
1446                     if self
._alignRight
: 
1447                         self
._choices 
= [choice
.rjust( length 
) for choice 
in self
._choices
] 
1449                         self
._choices 
= [choice
.ljust( length 
) for choice 
in self
._choices
] 
1450 ##                    dbg('aligned choices:', self._choices) 
1452                 if hasattr(self
, '_template'): 
1453                     # Verify each choice specified is valid: 
1454                     for choice 
in self
._choices
: 
1455                         if self
.IsEmpty(choice
) and not self
._validRequired
: 
1456                             # allow empty values even if invalid, (just colored differently) 
1458                         if not self
.IsValid(choice
): 
1459 ##                            dbg(indent=0, suspend=0) 
1460                             raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
1461                 self
._hasList 
= True 
1463 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) 
1464 ####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) 
1465         if kwargs
.has_key('fillChar') and len(self
._choices
) > 0: 
1466             if kwargs
['fillChar'] != ' ': 
1467                 self
._choices 
= [choice
.replace(' ', self
._fillChar
) for choice 
in self
._choices
] 
1469                 self
._choices 
= [choice
.replace(self
._old
_fillChar
, self
._fillChar
) for choice 
in self
._choices
] 
1470 ##            dbg('updated choices:', self._choices) 
1473         if kwargs
.has_key('autoSelect') and kwargs
['autoSelect']: 
1474             if not self
._hasList
: 
1475 ##                dbg('no list to auto complete; ignoring "autoSelect=True"') 
1476                 self
._autoSelect 
= False 
1478         # reset field validity assumption: 
1480 ##        dbg(indent=0, suspend=0) 
1483     def _GetParameter(self
, paramname
): 
1485         Routine for retrieving the value of any given parameter 
1487         if Field
.valid_params
.has_key(paramname
): 
1488             return getattr(self
, '_' + paramname
) 
1490             TypeError('Field._GetParameter: invalid parameter "%s"' % key
) 
1493     def IsEmpty(self
, slice): 
1495         Indicates whether the specified slice is considered empty for the 
1498 ##        dbg('Field::IsEmpty("%s")' % slice, indent=1) 
1499         if not hasattr(self
, '_template'): 
1501             raise AttributeError('_template') 
1503 ##        dbg('self._template: "%s"' % self._template) 
1504 ##        dbg('self._defaultValue: "%s"' % str(self._defaultValue)) 
1505         if slice == self
._template 
and not self
._defaultValue
: 
1509         elif slice == self
._template
: 
1511             for pos 
in range(len(self
._template
)): 
1512 ####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) 
1513                 if slice[pos
] not in (' ', self
._fillChar
): 
1516 ##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) 
1519 ##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) 
1523     def IsValid(self
, slice): 
1525         Indicates whether the specified slice is considered a valid value for the 
1529 ##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) 
1530         valid 
= True    # assume true to start 
1532         if self
.IsEmpty(slice): 
1533 ##            dbg(indent=0, suspend=0) 
1534             if self
._emptyInvalid
: 
1539         elif self
._hasList 
and self
._choiceRequired
: 
1540 ##            dbg("(member of list required)") 
1541             # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): 
1542             if self
._fillChar 
!= ' ': 
1543                 slice = slice.replace(self
._fillChar
, ' ') 
1544 ##                dbg('updated slice:"%s"' % slice) 
1545             compareStr 
= slice.strip() 
1547             if self
._compareNoCase
: 
1548                 compareStr 
= compareStr
.lower() 
1549             valid 
= compareStr 
in self
._compareChoices
 
1551         elif self
._hasRange 
and not self
.IsEmpty(slice): 
1552 ##            dbg('validating against range') 
1554                 # allow float as well as int ranges (int comparisons for free.) 
1555                 valid 
= self
._rangeLow 
<= float(slice) <= self
._rangeHigh
 
1559         elif self
._validRegex 
and self
._filter
: 
1560 ##            dbg('validating against regex') 
1561             valid 
= (re
.match( self
._filter
, slice) is not None) 
1563         if valid 
and self
._validFunc
: 
1564 ##            dbg('validating against supplied function') 
1565             valid 
= self
._validFunc
(slice) 
1566 ##        dbg('valid?', valid, indent=0, suspend=0) 
1570     def _AdjustField(self
, slice): 
1571         """ 'Fixes' an integer field. Right or left-justifies, as required.""" 
1572 ##        dbg('Field::_AdjustField("%s")' % slice, indent=1) 
1573         length 
= len(self
._mask
) 
1574 ####        dbg('length(self._mask):', length) 
1575 ####        dbg('self._useParensForNegatives?', self._useParensForNegatives) 
1577             if self
._useParensForNegatives
: 
1578                 signpos 
= slice.find('(') 
1579                 right_signpos 
= slice.find(')') 
1580                 intStr 
= slice.replace('(', '').replace(')', '')    # drop sign, if any 
1582                 signpos 
= slice.find('-') 
1583                 intStr 
= slice.replace( '-', '' )                   # drop sign, if any 
1586             intStr 
= intStr
.replace(' ', '')                        # drop extra spaces 
1587             intStr 
= string
.replace(intStr
,self
._fillChar
,"")       # drop extra fillchars 
1588             intStr 
= string
.replace(intStr
,"-","")                  # drop sign, if any 
1589             intStr 
= string
.replace(intStr
, self
._groupChar
, "")    # lose commas/dots 
1590 ####            dbg('intStr:"%s"' % intStr) 
1591             start
, end 
= self
._extent
 
1592             field_len 
= end 
- start
 
1593             if not self
._padZero 
and len(intStr
) != field_len 
and intStr
.strip(): 
1594                 intStr 
= str(long(intStr
)) 
1595 ####            dbg('raw int str: "%s"' % intStr) 
1596 ####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) 
1597             if self
._groupdigits
: 
1600                 for i 
in range(len(intStr
)-1, -1, -1): 
1601                     new 
= intStr
[i
] + new
 
1603                         new 
= self
._groupChar 
+ new
 
1605                 if new 
and new
[0] == self
._groupChar
: 
1607                 if len(new
) <= length
: 
1608                     # expanded string will still fit and leave room for sign: 
1610                 # else... leave it without the commas... 
1612 ##            dbg('padzero?', self._padZero) 
1613 ##            dbg('len(intStr):', len(intStr), 'field length:', length) 
1614             if self
._padZero 
and len(intStr
) < length
: 
1615                 intStr 
= '0' * (length 
- len(intStr
)) + intStr
 
1616                 if signpos 
!= -1:   # we had a sign before; restore it 
1617                     if self
._useParensForNegatives
: 
1618                         intStr 
= '(' + intStr
[1:] 
1619                         if right_signpos 
!= -1: 
1622                         intStr 
= '-' + intStr
[1:] 
1623             elif signpos 
!= -1 and slice[0:signpos
].strip() == '':    # - was before digits 
1624                 if self
._useParensForNegatives
: 
1625                     intStr 
= '(' + intStr
 
1626                     if right_signpos 
!= -1: 
1629                     intStr 
= '-' + intStr
 
1630             elif right_signpos 
!= -1: 
1631                 # must have had ')' but '(' was before field; re-add ')' 
1635         slice = slice.strip() # drop extra spaces 
1637         if self
._alignRight
:     ## Only if right-alignment is enabled 
1638             slice = slice.rjust( length 
) 
1640             slice = slice.ljust( length 
) 
1641         if self
._fillChar 
!= ' ': 
1642             slice = slice.replace(' ', self
._fillChar
) 
1643 ##        dbg('adjusted slice: "%s"' % slice, indent=0) 
1647 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1649 class MaskedEditMixin
: 
1651     This class allows us to abstract the masked edit functionality that could 
1652     be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.) 
1653     It forms the basis for all of the lib.masked controls. 
1655     valid_ctrl_params 
= { 
1656               'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control 
1657               'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary 
1658               'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask 
1659               'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year 
1660               'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete 
1661               'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control 
1662               'defaultEncoding': 'latin1',      ## optional argument to indicate unicode codec to use (unicode ctrls only) 
1663               'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently, 
1664                                                 ## from field validation constraints 
1665               'emptyBackgroundColour': "White", 
1666               'validBackgroundColour': "White", 
1667               'invalidBackgroundColour': "Yellow", 
1668               'foregroundColour': "Black", 
1669               'signedForegroundColour': "Red", 
1673     def __init__(self
, name 
= 'MaskedEdit', **kwargs
): 
1675         This is the "constructor" for setting up the mixin variable parameters for the composite class. 
1680         # set up flag for doing optional things to base control if possible 
1681         if not hasattr(self
, 'controlInitialized'): 
1682             self
.controlInitialized 
= False 
1684         # Set internal state var for keeping track of whether or not a character 
1685         # action results in a modification of the control, since .SetValue() 
1686         # doesn't modify the base control's internal state: 
1687         self
.modified 
= False 
1688         self
._previous
_mask 
= None 
1690         # Validate legitimate set of parameters: 
1691         for key 
in kwargs
.keys(): 
1692             if key
.replace('Color', 'Colour') not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1693                 raise TypeError('%s: invalid parameter "%s"' % (name
, key
)) 
1695         ## Set up dictionary that can be used by subclasses to override or add to default 
1696         ## behavior for individual characters.  Derived subclasses needing to change 
1697         ## default behavior for keys can either redefine the default functions for the 
1698         ## common keys or add functions for specific keys to this list.  Each function 
1699         ## added should take the key event as argument, and return False if the key 
1700         ## requires no further processing. 
1702         ## Initially populated with navigation and function control keys: 
1703         self
._keyhandlers 
= { 
1704             # default navigation keys and handlers: 
1705             wx
.WXK_BACK
:   self
._OnErase
, 
1706             wx
.WXK_LEFT
:   self
._OnArrow
, 
1707             wx
.WXK_RIGHT
:  self
._OnArrow
, 
1708             wx
.WXK_UP
:     self
._OnAutoCompleteField
, 
1709             wx
.WXK_DOWN
:   self
._OnAutoCompleteField
, 
1710             wx
.WXK_TAB
:    self
._OnChangeField
, 
1711             wx
.WXK_HOME
:   self
._OnHome
, 
1712             wx
.WXK_END
:    self
._OnEnd
, 
1713             wx
.WXK_RETURN
: self
._OnReturn
, 
1714             wx
.WXK_PRIOR
:  self
._OnAutoCompleteField
, 
1715             wx
.WXK_NEXT
:   self
._OnAutoCompleteField
, 
1717             # default function control keys and handlers: 
1718             wx
.WXK_DELETE
: self
._OnErase
, 
1719             WXK_CTRL_A
: self
._OnCtrl
_A
, 
1720             WXK_CTRL_C
: self
._OnCtrl
_C
, 
1721             WXK_CTRL_S
: self
._OnCtrl
_S
, 
1722             WXK_CTRL_V
: self
._OnCtrl
_V
, 
1723             WXK_CTRL_X
: self
._OnCtrl
_X
, 
1724             WXK_CTRL_Z
: self
._OnCtrl
_Z
, 
1727         ## bind standard navigational and control keycodes to this instance, 
1728         ## so that they can be augmented and/or changed in derived classes: 
1729         self
._nav 
= list(nav
) 
1730         self
._control 
= list(control
) 
1732         ## Dynamically evaluate and store string constants for mask chars 
1733         ## so that locale settings can be made after this module is imported 
1734         ## and the controls created after that is done can allow the 
1735         ## appropriate characters: 
1736         self
.maskchardict  
= { 
1738             'A': string
.uppercase
, 
1739             'a': string
.lowercase
, 
1740             'X': string
.letters 
+ string
.punctuation 
+ string
.digits
, 
1741             'C': string
.letters
, 
1742             'N': string
.letters 
+ string
.digits
, 
1743             '&': string
.punctuation
, 
1747         ## self._ignoreChange is used by MaskedComboBox, because 
1748         ## of the hack necessary to determine the selection; it causes 
1749         ## EVT_TEXT messages from the combobox to be ignored if set. 
1750         self
._ignoreChange 
= False 
1752         # These are used to keep track of previous value, for undo functionality: 
1753         self
._curValue  
= None 
1754         self
._prevValue 
= None 
1758         # Set defaults for each parameter for this instance, and fully 
1759         # populate initial parameter list for configuration: 
1760         for key
, value 
in MaskedEditMixin
.valid_ctrl_params
.items(): 
1761             setattr(self
, '_' + key
, copy
.copy(value
)) 
1762             if not kwargs
.has_key(key
): 
1763 ####                dbg('%s: "%s"' % (key, repr(value))) 
1764                 kwargs
[key
] = copy
.copy(value
) 
1766         # Create a "field" that holds global parameters for control constraints 
1767         self
._ctrl
_constraints 
= self
._fields
[-1] = Field(index
=-1) 
1768         self
.SetCtrlParameters(**kwargs
) 
1772     def SetCtrlParameters(self
, **kwargs
): 
1774         This public function can be used to set individual or multiple masked edit 
1775         parameters after construction.  (See maskededit module overview for the list 
1776         of valid parameters.) 
1779 ##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1) 
1780 ####        dbg('kwargs:', indent=1) 
1781 ##        for key, value in kwargs.items(): 
1782 ####            dbg(key, '=', value) 
1785         # Validate keyword arguments: 
1786         constraint_kwargs 
= {} 
1788         for key
, value 
in kwargs
.items(): 
1789             key 
= key
.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling 
1790             if key 
not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1791 ##                dbg(indent=0, suspend=0) 
1792                 raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key
, self
.name
)) 
1793             elif key 
in Field
.valid_params
.keys(): 
1794                 constraint_kwargs
[key
] = value
 
1796                 ctrl_kwargs
[key
] = value
 
1801         if ctrl_kwargs
.has_key('autoformat'): 
1802             autoformat 
= ctrl_kwargs
['autoformat'] 
1806         # handle "parochial name" backward compatibility: 
1807         if autoformat 
and autoformat
.find('MILTIME') != -1 and autoformat 
not in masktags
.keys(): 
1808             autoformat 
= autoformat
.replace('MILTIME', '24HRTIME') 
1810         if autoformat 
!= self
._autoformat 
and autoformat 
in masktags
.keys(): 
1811 ##            dbg('autoformat:', autoformat) 
1812             self
._autoformat                  
= autoformat
 
1813             mask                              
= masktags
[self
._autoformat
]['mask'] 
1814             # gather rest of any autoformat parameters: 
1815             for param
, value 
in masktags
[self
._autoformat
].items(): 
1816                 if param 
== 'mask': continue    # (must be present; already accounted for) 
1817                 constraint_kwargs
[param
] = value
 
1819         elif autoformat 
and not autoformat 
in masktags
.keys(): 
1820             raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat
)) 
1822 ##            dbg('autoformat not selected') 
1823             if kwargs
.has_key('mask'): 
1824                 mask 
= kwargs
['mask'] 
1825 ##                dbg('mask:', mask) 
1827         ## Assign style flags 
1829 ##            dbg('preserving previous mask') 
1830             mask 
= self
._previous
_mask   
# preserve previous mask 
1832 ##            dbg('mask (re)set') 
1833             reset_args
['reset_mask'] = mask
 
1834             constraint_kwargs
['mask'] = mask
 
1836             # wipe out previous fields; preserve new control-level constraints 
1837             self
._fields 
= {-1: self._ctrl_constraints}
 
1840         if ctrl_kwargs
.has_key('fields'): 
1841             # do field parameter type validation, and conversion to internal dictionary 
1843             fields 
= ctrl_kwargs
['fields'] 
1844             if type(fields
) in (types
.ListType
, types
.TupleType
): 
1845                 for i 
in range(len(fields
)): 
1847                     if not isinstance(field
, Field
): 
1848 ##                        dbg(indent=0, suspend=0) 
1849                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1850                     self
._fields
[i
] = field
 
1852             elif type(fields
) == types
.DictionaryType
: 
1853                 for index
, field 
in fields
.items(): 
1854                     if not isinstance(field
, Field
): 
1855 ##                        dbg(indent=0, suspend=0) 
1856                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1857                     self
._fields
[index
] = field
 
1859 ##                dbg(indent=0, suspend=0) 
1860                 raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields
)) 
1862         # Assign constraint parameters for entire control: 
1863 ####        dbg('control constraints:', indent=1) 
1864 ##        for key, value in constraint_kwargs.items(): 
1865 ####            dbg('%s:' % key, value) 
1868         # determine if changing parameters that should affect the entire control: 
1869         for key 
in MaskedEditMixin
.valid_ctrl_params
.keys(): 
1870             if key 
in ( 'mask', 'fields' ): continue    # (processed separately) 
1871             if ctrl_kwargs
.has_key(key
): 
1872                 setattr(self
, '_' + key
, ctrl_kwargs
[key
]) 
1874         # Validate color parameters, converting strings to named colors and validating 
1875         # result if appropriate: 
1876         for key 
in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', 
1877                     'foregroundColour', 'signedForegroundColour'): 
1878             if ctrl_kwargs
.has_key(key
): 
1879                 if type(ctrl_kwargs
[key
]) in (types
.StringType
, types
.UnicodeType
): 
1880                     c 
= wx
.NamedColour(ctrl_kwargs
[key
]) 
1881                     if c
.Get() == (-1, -1, -1): 
1882                         raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1884                         # replace attribute with wxColour object: 
1885                         setattr(self
, '_' + key
, c
) 
1886                         # attach a python dynamic attribute to wxColour for debug printouts 
1887                         c
._name 
= ctrl_kwargs
[key
] 
1889                 elif type(ctrl_kwargs
[key
]) != type(wx
.BLACK
): 
1890                     raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1893 ##        dbg('self._retainFieldValidation:', self._retainFieldValidation) 
1894         if not self
._retainFieldValidation
: 
1895             # Build dictionary of any changing parameters which should be propagated to the 
1897             for arg 
in Field
.propagating_params
: 
1898 ####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) 
1899 ####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) 
1900                 reset_args
[arg
] = kwargs
.has_key(arg
) and kwargs
[arg
] != getattr(self
._ctrl
_constraints
, '_'+arg
) 
1901 ####                dbg('reset_args[%s]?' % arg, reset_args[arg]) 
1903         # Set the control-level constraints: 
1904         self
._ctrl
_constraints
._SetParameters
(**constraint_kwargs
) 
1906         # This routine does the bulk of the interdependent parameter processing, determining 
1907         # the field extents of the mask if changed, resetting parameters as appropriate, 
1908         # determining the overall template value for the control, etc. 
1909         self
._configure
(mask
, **reset_args
) 
1911         # now that we've propagated the field constraints and mask portions to the 
1912         # various fields, validate the constraints 
1913         self
._ctrl
_constraints
._ValidateParameters
(**constraint_kwargs
) 
1915         # Validate that all choices for given fields are at least of the 
1916         # necessary length, and that they all would be valid pastes if pasted 
1917         # into their respective fields: 
1918 ####        dbg('validating choices') 
1919         self
._validateChoices
() 
1922         self
._autofit 
= self
._ctrl
_constraints
._autofit
 
1925         self
._isDate     
= 'D' in self
._ctrl
_constraints
._formatcodes 
and _isDateType(mask
) 
1926         self
._isTime     
= 'T' in self
._ctrl
_constraints
._formatcodes 
and _isTimeType(mask
) 
1928             # Set _dateExtent, used in date validation to locate date in string; 
1929             # always set as though year will be 4 digits, even if mask only has 
1930             # 2 digits, so we can always properly process the intended year for 
1931             # date validation (leap years, etc.) 
1932             if self
._mask
.find('CCC') != -1: self
._dateExtent 
= 11 
1933             else:                            self
._dateExtent 
= 10 
1935             self
._4digityear 
= len(self
._mask
) > 8 and self
._mask
[9] == '#' 
1937         if self
._isDate 
and self
._autoformat
: 
1938             # Auto-decide datestyle: 
1939             if self
._autoformat
.find('MDDY')    != -1: self
._datestyle 
= 'MDY' 
1940             elif self
._autoformat
.find('YMMD')  != -1: self
._datestyle 
= 'YMD' 
1941             elif self
._autoformat
.find('YMMMD') != -1: self
._datestyle 
= 'YMD' 
1942             elif self
._autoformat
.find('DMMY')  != -1: self
._datestyle 
= 'DMY' 
1943             elif self
._autoformat
.find('DMMMY') != -1: self
._datestyle 
= 'DMY' 
1945         # Give derived controls a chance to react to parameter changes before 
1946         # potentially changing current value of the control. 
1947         self
._OnCtrlParametersChanged
() 
1949         if self
.controlInitialized
: 
1950             # Then the base control is available for configuration; 
1951             # take action on base control based on new settings, as appropriate. 
1952             if kwargs
.has_key('useFixedWidthFont'): 
1953                 # Set control font - fixed width by default 
1956             if reset_args
.has_key('reset_mask'): 
1957 ##                dbg('reset mask') 
1958                 curvalue 
= self
._GetValue
() 
1959                 if curvalue
.strip(): 
1961 ##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) 
1962                         self
._SetInitialValue
(self
._GetValue
()) 
1963                     except Exception, e
: 
1964 ##                        dbg('exception caught:', e) 
1965 ##                        dbg("current value doesn't work; attempting to reset to template") 
1966                         self
._SetInitialValue
() 
1968 ##                    dbg('attempting to _SetInitialValue() with template') 
1969                     self
._SetInitialValue
() 
1971             elif kwargs
.has_key('useParensForNegatives'): 
1972                 newvalue 
= self
._getSignedValue
()[0] 
1974                 if newvalue 
is not None: 
1975                     # Adjust for new mask: 
1976                     if len(newvalue
) < len(self
._mask
): 
1978                     elif len(newvalue
) > len(self
._mask
): 
1979                         if newvalue
[-1] in (' ', ')'): 
1980                             newvalue 
= newvalue
[:-1] 
1982 ##                    dbg('reconfiguring value for parens:"%s"' % newvalue) 
1983                     self
._SetValue
(newvalue
) 
1985                     if self
._prevValue 
!= newvalue
: 
1986                         self
._prevValue 
= newvalue  
# disallow undo of sign type 
1989 ##                dbg('calculated size:', self._CalcSize()) 
1990                 self
.SetClientSize(self
._CalcSize
()) 
1991                 width 
= self
.GetSize().width
 
1992                 height 
= self
.GetBestSize().height
 
1993 ##                dbg('setting client size to:', (width, height)) 
1994                 self
.SetBestFittingSize((width
, height
)) 
1996             # Set value/type-specific formatting 
1997             self
._applyFormatting
() 
1998 ##        dbg(indent=0, suspend=0) 
2000     def SetMaskParameters(self
, **kwargs
): 
2001         """ old name for the SetCtrlParameters function  (DEPRECATED)""" 
2002         return self
.SetCtrlParameters(**kwargs
) 
2005     def GetCtrlParameter(self
, paramname
): 
2007         Routine for retrieving the value of any given parameter 
2009         if MaskedEditMixin
.valid_ctrl_params
.has_key(paramname
.replace('Color','Colour')): 
2010             return getattr(self
, '_' + paramname
.replace('Color', 'Colour')) 
2011         elif Field
.valid_params
.has_key(paramname
): 
2012             return self
._ctrl
_constraints
._GetParameter
(paramname
) 
2014             TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2016     def GetMaskParameter(self
, paramname
): 
2017         """ old name for the GetCtrlParameters function  (DEPRECATED)""" 
2018         return self
.GetCtrlParameter(paramname
) 
2021 ## This idea worked, but Boa was unable to use this solution... 
2022 ##    def _attachMethod(self, func): 
2024 ##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) 
2027 ##    def _DefinePropertyFunctions(exposed_params): 
2028 ##        for param in exposed_params: 
2029 ##            propname = param[0].upper() + param[1:] 
2031 ##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2032 ##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2033 ##            self._attachMethod(locals()['Set%s' % propname]) 
2034 ##            self._attachMethod(locals()['Get%s' % propname]) 
2036 ##            if param.find('Colour') != -1: 
2037 ##                # add non-british spellings, for backward-compatibility 
2038 ##                propname.replace('Colour', 'Color') 
2040 ##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2041 ##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2042 ##                self._attachMethod(locals()['Set%s' % propname]) 
2043 ##                self._attachMethod(locals()['Get%s' % propname]) 
2047     def SetFieldParameters(self
, field_index
, **kwargs
): 
2049         Routine provided to modify the parameters of a given field. 
2050         Because changes to fields can affect the overall control, 
2051         direct access to the fields is prevented, and the control 
2052         is always "reconfigured" after setting a field parameter. 
2053         (See maskededit module overview for the list of valid field-level 
2056         if field_index 
not in self
._field
_indices
: 
2057             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2058         # set parameters as requested: 
2059         self
._fields
[field_index
]._SetParameters
(**kwargs
) 
2061         # Possibly reprogram control template due to resulting changes, and ensure 
2062         # control-level params are still propagated to fields: 
2063         self
._configure
(self
._previous
_mask
) 
2064         self
._fields
[field_index
]._ValidateParameters
(**kwargs
) 
2066         if self
.controlInitialized
: 
2067             if kwargs
.has_key('fillChar') or kwargs
.has_key('defaultValue'): 
2068                 self
._SetInitialValue
() 
2071                     # this is tricky, because, as Robin explains: 
2072                     # "Basically there are two sizes to deal with, that are potentially  
2073                     #  different.  The client size is the inside size and may, depending 
2074                     #  on platform, exclude the borders and such.  The normal size is 
2075                     #  the outside size that does include the borders.  What you are 
2076                     #  calculating (in _CalcSize) is the client size, but the sizers 
2077                     #  deal with the full size and so that is the minimum size that 
2078                     #  we need to set with SetBestFittingSize.  The root of the problem is 
2079                     #  that in _calcSize the current client size height is returned, 
2080                     #  instead of a height based on the current font.  So I suggest using 
2081                     #  _calcSize to just get the width, and then use GetBestSize to 
2083                     self
.SetClientSize(self
._CalcSize
()) 
2084                     width 
= self
.GetSize().width
 
2085                     height 
= self
.GetBestSize().height
 
2086                     self
.SetBestFittingSize((width
, height
)) 
2089             # Set value/type-specific formatting 
2090             self
._applyFormatting
() 
2093     def GetFieldParameter(self
, field_index
, paramname
): 
2095         Routine provided for getting a parameter of an individual field. 
2097         if field_index 
not in self
._field
_indices
: 
2098             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2099         elif Field
.valid_params
.has_key(paramname
): 
2100             return self
._fields
[field_index
]._GetParameter
(paramname
) 
2102             TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2105     def _SetKeycodeHandler(self
, keycode
, func
): 
2107         This function adds and/or replaces key event handling functions 
2108         used by the control.  <func> should take the event as argument 
2109         and return False if no further action on the key is necessary. 
2112             self
._keyhandlers
[keycode
] = func
 
2113         elif self
._keyhandlers
.has_key(keycode
): 
2114             del self
._keyhandlers
[keycode
] 
2117     def _SetKeyHandler(self
, char
, func
): 
2119         This function adds and/or replaces key event handling functions 
2120         for ascii characters.  <func> should take the event as argument 
2121         and return False if no further action on the key is necessary. 
2123         self
._SetKeycodeHandler
(ord(char
), func
) 
2126     def _AddNavKeycode(self
, keycode
, handler
=None): 
2128         This function allows a derived subclass to augment the list of 
2129         keycodes that are considered "navigational" keys. 
2131         self
._nav
.append(keycode
) 
2133             self
._keyhandlers
[keycode
] = handler
 
2134         elif self
.keyhandlers
.has_key(keycode
): 
2135             del self
._keyhandlers
[keycode
] 
2139     def _AddNavKey(self
, char
, handler
=None): 
2141         This function is a convenience function so you don't have to 
2142         remember to call ord() for ascii chars to be used for navigation. 
2144         self
._AddNavKeycode
(ord(char
), handler
) 
2147     def _GetNavKeycodes(self
): 
2149         This function retrieves the current list of navigational keycodes for 
2155     def _SetNavKeycodes(self
, keycode_func_tuples
): 
2157         This function allows you to replace the current list of keycode processed 
2158         as navigation keys, and bind associated optional keyhandlers. 
2161         for keycode
, func 
in keycode_func_tuples
: 
2162             self
._nav
.append(keycode
) 
2164                 self
._keyhandlers
[keycode
] = func
 
2165             elif self
.keyhandlers
.has_key(keycode
): 
2166                 del self
._keyhandlers
[keycode
] 
2169     def _processMask(self
, mask
): 
2171         This subroutine expands {n} syntax in mask strings, and looks for escaped 
2172         special characters and returns the expanded mask, and an dictionary 
2173         of booleans indicating whether or not a given position in the mask is 
2174         a mask character or not. 
2176 ##        dbg('_processMask: mask', mask, indent=1) 
2177         # regular expression for parsing c{n} syntax: 
2178         rex 
= re
.compile('([' +string
.join(maskchars
,"") + '])\{(\d+)\}') 
2180         match 
= rex
.search(s
) 
2181         while match
:    # found an(other) occurrence 
2182             maskchr 
= s
[match
.start(1):match
.end(1)]            # char to be repeated 
2183             repcount 
= int(s
[match
.start(2):match
.end(2)])      # the number of times 
2184             replacement 
= string
.join( maskchr 
* repcount
, "")  # the resulting substr 
2185             s 
= s
[:match
.start(1)] + replacement 
+ s
[match
.end(2)+1:]   #account for trailing '}' 
2186             match 
= rex
.search(s
)                               # look for another such entry in mask 
2188         self
._decimalChar 
= self
._ctrl
_constraints
._decimalChar
 
2189         self
._shiftDecimalChar 
= self
._ctrl
_constraints
._shiftDecimalChar
 
2191         self
._isFloat      
= _isFloatingPoint(s
) and not self
._ctrl
_constraints
._validRegex
 
2192         self
._isInt      
= _isInteger(s
) and not self
._ctrl
_constraints
._validRegex
 
2193         self
._signOk     
= '-' in self
._ctrl
_constraints
._formatcodes 
and (self
._isFloat 
or self
._isInt
) 
2194         self
._useParens  
= self
._ctrl
_constraints
._useParensForNegatives
 
2196 ####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) 
2197 ####        dbg('isFloatingPoint(%s)?' % (s), _isFloatingPoint(s), 
2198 ##            'ctrl regex:', self._ctrl_constraints._validRegex) 
2200         if self
._signOk 
and s
[0] != ' ': 
2202             if self
._ctrl
_constraints
._defaultValue 
and self
._ctrl
_constraints
._defaultValue
[0] != ' ': 
2203                 self
._ctrl
_constraints
._defaultValue 
= ' ' + self
._ctrl
_constraints
._defaultValue
 
2208                 self
._ctrl
_constraints
._defaultValue 
+= ' ' 
2210         # Now, go build up a dictionary of booleans, indexed by position, 
2211         # indicating whether or not a given position is masked or not 
2215             if s
[i
] == '\\':            # if escaped character: 
2216                 ismasked
[i
] = False     #     mark position as not a mask char 
2217                 if i
+1 < len(s
):        #     if another char follows... 
2218                     s 
= s
[:i
] + s
[i
+1:] #         elide the '\' 
2219                     if i
+2 < len(s
) and s
[i
+1] == '\\': 
2220                         # if next char also a '\', char is a literal '\' 
2221                         s 
= s
[:i
] + s
[i
+1:]     # elide the 2nd '\' as well 
2222             else:                       # else if special char, mark position accordingly 
2223                 ismasked
[i
] = s
[i
] in maskchars
 
2224 ####            dbg('ismasked[%d]:' % i, ismasked[i], s) 
2225             i 
+= 1                      # increment to next char 
2226 ####        dbg('ismasked:', ismasked) 
2227 ##        dbg('new mask: "%s"' % s, indent=0) 
2232     def _calcFieldExtents(self
): 
2234         Subroutine responsible for establishing/configuring field instances with 
2235         indices and editable extents appropriate to the specified mask, and building 
2236         the lookup table mapping each position to the corresponding field. 
2238         self
._lookupField 
= {} 
2241             ## Create dictionary of positions,characters in mask 
2243             for charnum 
in range( len( self
._mask
)): 
2244                 self
.maskdict
[charnum
] = self
._mask
[charnum
:charnum
+1] 
2246             # For the current mask, create an ordered list of field extents 
2247             # and a dictionary of positions that map to field indices: 
2249             if self
._signOk
: start 
= 1 
2253                 # Skip field "discovery", and just construct a 2-field control with appropriate 
2254                 # constraints for a floating-point entry. 
2256                 # .setdefault always constructs 2nd argument even if not needed, so we do this 
2257                 # the old-fashioned way... 
2258                 if not self
._fields
.has_key(0): 
2259                     self
._fields
[0] = Field() 
2260                 if not self
._fields
.has_key(1): 
2261                     self
._fields
[1] = Field() 
2263                 self
._decimalpos 
= string
.find( self
._mask
, '.') 
2264 ##                dbg('decimal pos =', self._decimalpos) 
2266                 formatcodes 
= self
._fields
[0]._GetParameter
('formatcodes') 
2267                 if 'R' not in formatcodes
: formatcodes 
+= 'R' 
2268                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, self
._decimalpos
), 
2269                                                mask
=self
._mask
[start
:self
._decimalpos
], formatcodes
=formatcodes
) 
2270                 end 
= len(self
._mask
) 
2271                 if self
._signOk 
and self
._useParens
: 
2273                 self
._fields
[1]._SetParameters
(index
=1, extent
=(self
._decimalpos
+1, end
), 
2274                                                mask
=self
._mask
[self
._decimalpos
+1:end
]) 
2276                 for i 
in range(self
._decimalpos
+1): 
2277                     self
._lookupField
[i
] = 0 
2279                 for i 
in range(self
._decimalpos
+1, len(self
._mask
)+1): 
2280                     self
._lookupField
[i
] = 1 
2283                 # Skip field "discovery", and just construct a 1-field control with appropriate 
2284                 # constraints for a integer entry. 
2285                 if not self
._fields
.has_key(0): 
2286                     self
._fields
[0] = Field(index
=0) 
2287                 end 
= len(self
._mask
) 
2288                 if self
._signOk 
and self
._useParens
: 
2290                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, end
), 
2291                                                mask
=self
._mask
[start
:end
]) 
2292                 for i 
in range(len(self
._mask
)+1): 
2293                     self
._lookupField
[i
] = 0 
2295                 # generic control; parse mask to figure out where the fields are: 
2298                 i 
= self
._findNextEntry
(pos
,adjustInsert
=False)  # go to 1st entry point: 
2299                 if i 
< len(self
._mask
):   # no editable chars! 
2300                     for j 
in range(pos
, i
+1): 
2301                         self
._lookupField
[j
] = field_index
 
2302                     pos 
= i       
# figure out field for 1st editable space: 
2304                 while i 
<= len(self
._mask
): 
2305 ####                    dbg('searching: outer field loop: i = ', i) 
2306                     if self
._isMaskChar
(i
): 
2307 ####                        dbg('1st char is mask char; recording edit_start=', i) 
2309                         # Skip to end of editable part of current field: 
2310                         while i 
< len(self
._mask
) and self
._isMaskChar
(i
): 
2311                             self
._lookupField
[i
] = field_index
 
2313 ####                        dbg('edit_end =', i) 
2315                         self
._lookupField
[i
] = field_index
 
2316 ####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) 
2317                         if not self
._fields
.has_key(field_index
): 
2318                             kwargs 
= Field
.valid_params
.copy() 
2319                             kwargs
['index'] = field_index
 
2320                             kwargs
['extent'] = (edit_start
, edit_end
) 
2321                             kwargs
['mask'] = self
._mask
[edit_start
:edit_end
] 
2322                             self
._fields
[field_index
] = Field(**kwargs
) 
2324                             self
._fields
[field_index
]._SetParameters
( 
2326                                                                 extent
=(edit_start
, edit_end
), 
2327                                                                 mask
=self
._mask
[edit_start
:edit_end
]) 
2329                     i 
= self
._findNextEntry
(pos
, adjustInsert
=False)  # go to next field: 
2331                         for j 
in range(pos
, i
+1): 
2332                             self
._lookupField
[j
] = field_index
 
2333                     if i 
>= len(self
._mask
): 
2334                         break           # if past end, we're done 
2337 ####                        dbg('next field:', field_index) 
2339         indices 
= self
._fields
.keys() 
2341         self
._field
_indices 
= indices
[1:] 
2342 ####        dbg('lookupField map:', indent=1) 
2343 ##        for i in range(len(self._mask)): 
2344 ####            dbg('pos %d:' % i, self._lookupField[i]) 
2347         # Verify that all field indices specified are valid for mask: 
2348         for index 
in self
._fields
.keys(): 
2349             if index 
not in [-1] + self
._lookupField
.values(): 
2350                 raise IndexError('field %d is not a valid field for mask "%s"' % (index
, self
._mask
)) 
2353     def _calcTemplate(self
, reset_fillchar
, reset_default
): 
2355         Subroutine for processing current fillchars and default values for 
2356         whole control and individual fields, constructing the resulting 
2357         overall template, and adjusting the current value as necessary. 
2360         if self
._ctrl
_constraints
._defaultValue
: 
2363             for field 
in self
._fields
.values(): 
2364                 if field
._defaultValue 
and not reset_default
: 
2366 ##        dbg('default set?', default_set) 
2368         # Determine overall new template for control, and keep track of previous 
2369         # values, so that current control value can be modified as appropriate: 
2370         if self
.controlInitialized
: curvalue 
= list(self
._GetValue
()) 
2371         else:                       curvalue 
= None 
2373         if hasattr(self
, '_fillChar'): old_fillchars 
= self
._fillChar
 
2374         else:                          old_fillchars 
= None 
2376         if hasattr(self
, '_template'): old_template 
= self
._template
 
2377         else:                          old_template 
= None 
2384         for field 
in self
._fields
.values(): 
2385             field
._template 
= "" 
2387         for pos 
in range(len(self
._mask
)): 
2388 ####            dbg('pos:', pos) 
2389             field 
= self
._FindField
(pos
) 
2390 ####            dbg('field:', field._index) 
2391             start
, end 
= field
._extent
 
2393             if pos 
== 0 and self
._signOk
: 
2394                 self
._template 
= ' ' # always make 1st 1st position blank, regardless of fillchar 
2395             elif self
._isFloat 
and pos 
== self
._decimalpos
: 
2396                 self
._template 
+= self
._decimalChar
 
2397             elif self
._isMaskChar
(pos
): 
2398                 if field
._fillChar 
!= self
._ctrl
_constraints
._fillChar 
and not reset_fillchar
: 
2399                     fillChar 
= field
._fillChar
 
2401                     fillChar 
= self
._ctrl
_constraints
._fillChar
 
2402                 self
._fillChar
[pos
] = fillChar
 
2404                 # Replace any current old fillchar with new one in current value; 
2405                 # if action required, set reset_value flag so we can take that action 
2406                 # after we're all done 
2407                 if self
.controlInitialized 
and old_fillchars 
and old_fillchars
.has_key(pos
) and curvalue
: 
2408                     if curvalue
[pos
] == old_fillchars
[pos
] and old_fillchars
[pos
] != fillChar
: 
2410                         curvalue
[pos
] = fillChar
 
2412                 if not field
._defaultValue 
and not self
._ctrl
_constraints
._defaultValue
: 
2413 ####                    dbg('no default value') 
2414                     self
._template 
+= fillChar
 
2415                     field
._template 
+= fillChar
 
2417                 elif field
._defaultValue 
and not reset_default
: 
2418 ####                    dbg('len(field._defaultValue):', len(field._defaultValue)) 
2419 ####                    dbg('pos-start:', pos-start) 
2420                     if len(field
._defaultValue
) > pos
-start
: 
2421 ####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) 
2422                         self
._template 
+= field
._defaultValue
[pos
-start
] 
2423                         field
._template 
+= field
._defaultValue
[pos
-start
] 
2425 ####                        dbg('field default not long enough; using fillChar') 
2426                         self
._template 
+= fillChar
 
2427                         field
._template 
+= fillChar
 
2429                     if len(self
._ctrl
_constraints
._defaultValue
) > pos
: 
2430 ####                        dbg('using control default') 
2431                         self
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2432                         field
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2434 ####                        dbg('ctrl default not long enough; using fillChar') 
2435                         self
._template 
+= fillChar
 
2436                         field
._template 
+= fillChar
 
2437 ####                dbg('field[%d]._template now "%s"' % (field._index, field._template)) 
2438 ####                dbg('self._template now "%s"' % self._template) 
2440                 self
._template 
+= self
._mask
[pos
] 
2442         self
._fields
[-1]._template 
= self
._template     
# (for consistency) 
2444         if curvalue
:    # had an old value, put new one back together 
2445             newvalue 
= string
.join(curvalue
, "") 
2450             self
._defaultValue 
= self
._template
 
2451 ##            dbg('self._defaultValue:', self._defaultValue) 
2452             if not self
.IsEmpty(self
._defaultValue
) and not self
.IsValid(self
._defaultValue
): 
2454                 raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self
._defaultValue
, self
.name
)) 
2456             # if no fillchar change, but old value == old template, replace it: 
2457             if newvalue 
== old_template
: 
2458                 newvalue 
= self
._template
 
2461             self
._defaultValue 
= None 
2464 ##            dbg('resetting value to: "%s"' % newvalue) 
2465             pos 
= self
._GetInsertionPoint
() 
2466             sel_start
, sel_to 
= self
._GetSelection
() 
2467             self
._SetValue
(newvalue
) 
2468             self
._SetInsertionPoint
(pos
) 
2469             self
._SetSelection
(sel_start
, sel_to
) 
2472     def _propagateConstraints(self
, **reset_args
): 
2474         Subroutine for propagating changes to control-level constraints and 
2475         formatting to the individual fields as appropriate. 
2477         parent_codes 
= self
._ctrl
_constraints
._formatcodes
 
2478         parent_includes 
= self
._ctrl
_constraints
._includeChars
 
2479         parent_excludes 
= self
._ctrl
_constraints
._excludeChars
 
2480         for i 
in self
._field
_indices
: 
2481             field 
= self
._fields
[i
] 
2483             if len(self
._field
_indices
) == 1: 
2484                 inherit_args
['formatcodes'] = parent_codes
 
2485                 inherit_args
['includeChars'] = parent_includes
 
2486                 inherit_args
['excludeChars'] = parent_excludes
 
2488                 field_codes 
= current_codes 
= field
._GetParameter
('formatcodes') 
2489                 for c 
in parent_codes
: 
2490                     if c 
not in field_codes
: field_codes 
+= c
 
2491                 if field_codes 
!= current_codes
: 
2492                     inherit_args
['formatcodes'] = field_codes
 
2494                 include_chars 
= current_includes 
= field
._GetParameter
('includeChars') 
2495                 for c 
in parent_includes
: 
2496                     if not c 
in include_chars
: include_chars 
+= c
 
2497                 if include_chars 
!= current_includes
: 
2498                     inherit_args
['includeChars'] = include_chars
 
2500                 exclude_chars 
= current_excludes 
= field
._GetParameter
('excludeChars') 
2501                 for c 
in parent_excludes
: 
2502                     if not c 
in exclude_chars
: exclude_chars 
+= c
 
2503                 if exclude_chars 
!= current_excludes
: 
2504                     inherit_args
['excludeChars'] = exclude_chars
 
2506             if reset_args
.has_key('defaultValue') and reset_args
['defaultValue']: 
2507                 inherit_args
['defaultValue'] = ""   # (reset for field) 
2509             for param 
in Field
.propagating_params
: 
2510 ####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) 
2511 ####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) 
2512                 if reset_args
.has_key(param
): 
2513                     inherit_args
[param
] = self
.GetCtrlParameter(param
) 
2514 ####                    dbg('inherit_args[%s]' % param, inherit_args[param]) 
2517                 field
._SetParameters
(**inherit_args
) 
2518                 field
._ValidateParameters
(**inherit_args
) 
2521     def _validateChoices(self
): 
2523         Subroutine that validates that all choices for given fields are at 
2524         least of the necessary length, and that they all would be valid pastes 
2525         if pasted into their respective fields. 
2527         for field 
in self
._fields
.values(): 
2529                 index 
= field
._index
 
2530                 if len(self
._field
_indices
) == 1 and index 
== 0 and field
._choices 
== self
._ctrl
_constraints
._choices
: 
2531 ##                    dbg('skipping (duplicate) choice validation of field 0') 
2533 ####                dbg('checking for choices for field', field._index) 
2534                 start
, end 
= field
._extent
 
2535                 field_length 
= end 
- start
 
2536 ####                dbg('start, end, length:', start, end, field_length) 
2537                 for choice 
in field
._choices
: 
2538 ####                    dbg('testing "%s"' % choice) 
2539                     valid_paste
, ignore
, replace_to 
= self
._validatePaste
(choice
, start
, end
) 
2542                         raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice
, index
, self
.name
)) 
2543                     elif replace_to 
> end
: 
2545                         raise ValueError('"%s" will not fit into field %d of control "%s"' (choice
, index
, self
.name
)) 
2546 ####                    dbg(choice, 'valid in field', index) 
2549     def _configure(self
, mask
, **reset_args
): 
2551         This function sets flags for automatic styling options.  It is 
2552         called whenever a control or field-level parameter is set/changed. 
2554         This routine does the bulk of the interdependent parameter processing, determining 
2555         the field extents of the mask if changed, resetting parameters as appropriate, 
2556         determining the overall template value for the control, etc. 
2558         reset_args is supplied if called from control's .SetCtrlParameters() 
2559         routine, and indicates which if any parameters which can be 
2560         overridden by individual fields have been reset by request for the 
2565 ##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) 
2567         # Preprocess specified mask to expand {n} syntax, handle escaped 
2568         # mask characters, etc and build the resulting positionally keyed 
2569         # dictionary for which positions are mask vs. template characters: 
2570         self
._mask
, self
.ismasked 
= self
._processMask
(mask
) 
2571         self
._masklength 
= len(self
._mask
) 
2572 ####        dbg('processed mask:', self._mask) 
2574         # Preserve original mask specified, for subsequent reprocessing 
2575         # if parameters change. 
2576 ##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) 
2577         self
._previous
_mask 
= mask    
# save unexpanded mask for next time 
2578             # Set expanded mask and extent of field -1 to width of entire control: 
2579         self
._ctrl
_constraints
._SetParameters
(mask 
= self
._mask
, extent
=(0,self
._masklength
)) 
2581         # Go parse mask to determine where each field is, construct field 
2582         # instances as necessary, configure them with those extents, and 
2583         # build lookup table mapping each position for control to its corresponding 
2585 ####        dbg('calculating field extents') 
2587         self
._calcFieldExtents
() 
2590         # Go process defaultValues and fillchars to construct the overall 
2591         # template, and adjust the current value as necessary: 
2592         reset_fillchar 
= reset_args
.has_key('fillChar') and reset_args
['fillChar'] 
2593         reset_default 
= reset_args
.has_key('defaultValue') and reset_args
['defaultValue'] 
2595 ####        dbg('calculating template') 
2596         self
._calcTemplate
(reset_fillchar
, reset_default
) 
2598         # Propagate control-level formatting and character constraints to each 
2599         # field if they don't already have them; if only one field, propagate 
2600         # control-level validation constraints to field as well: 
2601 ####        dbg('propagating constraints') 
2602         self
._propagateConstraints
(**reset_args
) 
2605         if self
._isFloat 
and self
._fields
[0]._groupChar 
== self
._decimalChar
: 
2606             raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % 
2607                                  (self
._fields
[0]._groupChar
, self
._decimalChar
) ) 
2609 ####        dbg('fields:', indent=1) 
2610 ##        for i in [-1] + self._field_indices: 
2611 ####            dbg('field %d:' % i, self._fields[i].__dict__) 
2614         # Set up special parameters for numeric control, if appropriate: 
2616             self
._signpos 
= 0   # assume it starts here, but it will move around on floats 
2617             signkeys 
= ['-', '+', ' '] 
2619                 signkeys 
+= ['(', ')'] 
2620             for key 
in signkeys
: 
2622                 if not self
._keyhandlers
.has_key(keycode
): 
2623                     self
._SetKeyHandler
(key
, self
._OnChangeSign
) 
2624         elif self
._isInt 
or self
._isFloat
: 
2625             signkeys 
= ['-', '+', ' ', '(', ')'] 
2626             for key 
in signkeys
: 
2628                 if self
._keyhandlers
.has_key(keycode
) and self
._keyhandlers
[keycode
] == self
._OnChangeSign
: 
2629                     self
._SetKeyHandler
(key
, None) 
2633         if self
._isFloat 
or self
._isInt
: 
2634             if self
.controlInitialized
: 
2635                 value 
= self
._GetValue
() 
2636 ####                dbg('value: "%s"' % value, 'len(value):', len(value), 
2637 ##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) 
2638                 if len(value
) < len(self
._ctrl
_constraints
._mask
): 
2640                     if self
._useParens 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find('(') == -1: 
2642                     if self
._signOk 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find(')') == -1: 
2643                         newvalue 
= ' ' + newvalue
 
2644                     if len(newvalue
) < len(self
._ctrl
_constraints
._mask
): 
2645                         if self
._ctrl
_constraints
._alignRight
: 
2646                             newvalue 
= newvalue
.rjust(len(self
._ctrl
_constraints
._mask
)) 
2648                             newvalue 
= newvalue
.ljust(len(self
._ctrl
_constraints
._mask
)) 
2649 ##                    dbg('old value: "%s"' % value) 
2650 ##                    dbg('new value: "%s"' % newvalue) 
2652                         self
._SetValue
(newvalue
) 
2653                     except Exception, e
: 
2654 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2655                         self
._SetInitialValue
() 
2657                 elif len(value
) > len(self
._ctrl
_constraints
._mask
): 
2659                     if not self
._useParens 
and newvalue
[-1] == ' ': 
2660                         newvalue 
= newvalue
[:-1] 
2661                     if not self
._signOk 
and len(newvalue
) > len(self
._ctrl
_constraints
._mask
): 
2662                         newvalue 
= newvalue
[1:] 
2663                     if not self
._signOk
: 
2664                         newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(newvalue
) 
2666 ##                    dbg('old value: "%s"' % value) 
2667 ##                    dbg('new value: "%s"' % newvalue) 
2669                         self
._SetValue
(newvalue
) 
2670                     except Exception, e
: 
2671 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2672                         self
._SetInitialValue
() 
2673                 elif not self
._signOk 
and ('(' in value 
or '-' in value
): 
2674                     newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(value
) 
2675 ##                    dbg('old value: "%s"' % value) 
2676 ##                    dbg('new value: "%s"' % newvalue) 
2678                         self
._SetValue
(newvalue
) 
2680 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2681                         self
._SetInitialValue
() 
2683             # Replace up/down arrow default handling: 
2684             # make down act like tab, up act like shift-tab: 
2686 ####            dbg('Registering numeric navigation and control handlers (if not already set)') 
2687             if not self
._keyhandlers
.has_key(wx
.WXK_DOWN
): 
2688                 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnChangeField
) 
2689             if not self
._keyhandlers
.has_key(wx
.WXK_UP
): 
2690                 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnUpNumeric
)  # (adds "shift" to up arrow, and calls _OnChangeField) 
2692             # On ., truncate contents right of cursor to decimal point (if any) 
2693             # leaves cursor after decimal point if floating point, otherwise at 0. 
2694             if not self
._keyhandlers
.has_key(ord(self
._decimalChar
)) or self
._keyhandlers
[ord(self
._decimalChar
)] != self
._OnDecimalPoint
: 
2695                 self
._SetKeyHandler
(self
._decimalChar
, self
._OnDecimalPoint
) 
2697             if not self
._keyhandlers
.has_key(ord(self
._shiftDecimalChar
)) or self
._keyhandlers
[ord(self
._shiftDecimalChar
)] != self
._OnChangeField
: 
2698                 self
._SetKeyHandler
(self
._shiftDecimalChar
, self
._OnChangeField
)   # (Shift-'.' == '>' on US keyboards) 
2700             # Allow selective insert of groupchar in numbers: 
2701             if not self
._keyhandlers
.has_key(ord(self
._fields
[0]._groupChar
)) or self
._keyhandlers
[ord(self
._fields
[0]._groupChar
)] != self
._OnGroupChar
: 
2702                 self
._SetKeyHandler
(self
._fields
[0]._groupChar
, self
._OnGroupChar
) 
2704 ##        dbg(indent=0, suspend=0) 
2707     def _SetInitialValue(self
, value
=""): 
2709         fills the control with the generated or supplied default value. 
2710         It will also set/reset the font if necessary and apply 
2711         formatting to the control at this time. 
2713 ##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) 
2715             self
._prevValue 
= self
._curValue 
= self
._template
 
2716             # don't apply external validation rules in this case, as template may 
2717             # not coincide with "legal" value... 
2719                 self
._SetValue
(self
._curValue
)  # note the use of "raw" ._SetValue()... 
2720             except Exception, e
: 
2721 ##                dbg('exception thrown:', e, indent=0) 
2724             # Otherwise apply validation as appropriate to passed value: 
2725 ####            dbg('value = "%s", length:' % value, len(value)) 
2726             self
._prevValue 
= self
._curValue 
= value
 
2728                 self
.SetValue(value
)            # use public (validating) .SetValue() 
2729             except Exception, e
: 
2730 ##                dbg('exception thrown:', e, indent=0) 
2734         # Set value/type-specific formatting 
2735         self
._applyFormatting
() 
2739     def _calcSize(self
, size
=None): 
2740         """ Calculate automatic size if allowed; must be called after the base control is instantiated""" 
2741 ####        dbg('MaskedEditMixin::_calcSize', indent=1) 
2742         cont 
= (size 
is None or size 
== wx
.DefaultSize
) 
2744         if cont 
and self
._autofit
: 
2745             sizing_text 
= 'M' * self
._masklength
 
2746             if wx
.Platform 
!= "__WXMSW__":   # give it a little extra space 
2748             if wx
.Platform 
== "__WXMAC__":   # give it even a little more... 
2750 ####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) 
2751             w
, h 
= self
.GetTextExtent(sizing_text
) 
2752             size 
= (w
+4, self
.GetSize().height
) 
2753 ####            dbg('size:', size, indent=0) 
2758         """ Set the control's font typeface -- pass the font name as str.""" 
2759 ####        dbg('MaskedEditMixin::_setFont', indent=1) 
2760         if not self
._useFixedWidthFont
: 
2761             self
._font 
= wx
.SystemSettings_GetFont(wx
.SYS_DEFAULT_GUI_FONT
) 
2763             font 
= self
.GetFont()   # get size, weight, etc from current font 
2765             # Set to teletype font (guaranteed to be mappable to all wxWindows 
2767             self
._font 
= wx
.Font( font
.GetPointSize(), wx
.TELETYPE
, font
.GetStyle(), 
2768                                  font
.GetWeight(), font
.GetUnderlined()) 
2769 ####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) 
2771         self
.SetFont(self
._font
) 
2775     def _OnTextChange(self
, event
): 
2777         Handler for EVT_TEXT event. 
2778         self._Change() is provided for subclasses, and may return False to 
2779         skip this method logic.  This function returns True if the event 
2780         detected was a legitimate event, or False if it was a "bogus" 
2781         EVT_TEXT event.  (NOTE: There is currently an issue with calling 
2782         .SetValue from within the EVT_CHAR handler that causes duplicate 
2783         EVT_TEXT events for the same change.) 
2785         newvalue 
= self
._GetValue
() 
2786 ##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) 
2788         if self
._ignoreChange
:      # ie. if an "intermediate text change event" 
2792         ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue 
2793         ## call is generating two (2) EVT_TEXT events.  On certain platforms, 
2794         ## (eg. linux/GTK) the 1st is an empty string value. 
2795         ## This is the only mechanism I can find to mask this problem: 
2796         if newvalue 
== self
._curValue 
or len(newvalue
) == 0: 
2797 ##            dbg('ignoring bogus text change event', indent=0) 
2800 ##            dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue))) 
2802                 if self
._signOk 
and self
._isNeg 
and newvalue
.find('-') == -1 and newvalue
.find('(') == -1: 
2803 ##                    dbg('clearing self._isNeg') 
2805                     text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
() 
2806                 self
._CheckValid
()  # Recolor control as appropriate 
2807 ##            dbg('calling event.Skip()') 
2810         self
._prevValue 
= self
._curValue    
# save for undo 
2811         self
._curValue 
= newvalue           
# Save last seen value for next iteration 
2816     def _OnKeyDown(self
, event
): 
2818         This function allows the control to capture Ctrl-events like Ctrl-tab, 
2819         that are not normally seen by the "cooked" EVT_CHAR routine. 
2821         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2822         key    
= event
.GetKeyCode() 
2823         if key 
in self
._nav 
and event
.ControlDown(): 
2824             # then this is the only place we will likely see these events; 
2826 ##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') 
2829         # else allow regular EVT_CHAR key processing 
2833     def _OnChar(self
, event
): 
2835         This is the engine of MaskedEdit controls.  It examines each keystroke, 
2836         decides if it's allowed, where it should go or what action to take. 
2838 ##        dbg('MaskedEditMixin::_OnChar', indent=1) 
2840         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2841         key 
= event
.GetKeyCode() 
2842         orig_pos 
= self
._GetInsertionPoint
() 
2843         orig_value 
= self
._GetValue
() 
2844 ##        dbg('keycode = ', key) 
2845 ##        dbg('current pos = ', orig_pos) 
2846 ##        dbg('current selection = ', self._GetSelection()) 
2848         if not self
._Keypress
(key
): 
2852         # If no format string for this control, or the control is marked as "read-only", 
2853         # skip the rest of the special processing, and just "do the standard thing:" 
2854         if not self
._mask 
or not self
._IsEditable
(): 
2859         # Process navigation and control keys first, with 
2860         # position/selection unadulterated: 
2861         if key 
in self
._nav 
+ self
._control
: 
2862             if self
._keyhandlers
.has_key(key
): 
2863                 keep_processing 
= self
._keyhandlers
[key
](event
) 
2864                 if self
._GetValue
() != orig_value
: 
2865                     self
.modified 
= True 
2866                 if not keep_processing
: 
2869                 self
._applyFormatting
() 
2873         # Else... adjust the position as necessary for next input key, 
2874         # and determine resulting selection: 
2875         pos 
= self
._adjustPos
( orig_pos
, key 
)    ## get insertion position, adjusted as needed 
2876         sel_start
, sel_to 
= self
._GetSelection
()                ## check for a range of selected text 
2877 ##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) 
2879         keep_processing 
= True 
2880         # Capture user past end of format field 
2881         if pos 
> len(self
.maskdict
): 
2882 ##            dbg("field length exceeded:",pos) 
2883             keep_processing 
= False 
2886             if self
._isMaskChar
(pos
):  ## Get string of allowed characters for validation 
2887                 okchars 
= self
._getAllowedChars
(pos
) 
2889 ##                dbg('Not a valid position: pos = ', pos,"chars=",maskchars) 
2892         key 
= self
._adjustKey
(pos
, key
)     # apply formatting constraints to key: 
2894         if self
._keyhandlers
.has_key(key
): 
2895             # there's an override for default behavior; use override function instead 
2896 ##            dbg('using supplied key handler:', self._keyhandlers[key]) 
2897             keep_processing 
= self
._keyhandlers
[key
](event
) 
2898             if self
._GetValue
() != orig_value
: 
2899                 self
.modified 
= True 
2900             if not keep_processing
: 
2903             # else skip default processing, but do final formatting 
2904         if key 
< wx
.WXK_SPACE 
or key 
> 255: 
2905 ##            dbg('key < WXK_SPACE or key > 255') 
2906             event
.Skip()                # non alphanumeric 
2907             keep_processing 
= False 
2909             field 
= self
._FindField
(pos
) 
2911 ##            dbg("key ='%s'" % chr(key)) 
2913 ##                dbg('okSpaces?', field._okSpaces) 
2916             char 
= chr(key
) # (must work if we got this far) 
2918             if 'unicode' in wx
.PlatformInfo
: 
2919                 char 
= char
.decode(self
._defaultEncoding
) 
2921                 if type(field
._excludeChars
) != types
.UnicodeType
: 
2922                     excludes 
+= field
._excludeChars
.decode(self
._defaultEncoding
) 
2923                 if type(self
._ctrl
_constraints
) != types
.UnicodeType
: 
2924                     excludes 
+= self
._ctrl
_constraints
._excludeChars
.decode(self
._defaultEncoding
) 
2926                 excludes 
= field
._excludeChars 
+ self
._ctrl
_constraints
._excludeChars
 
2928             if char 
in excludes
: 
2929                 keep_processing 
= False 
2931             if keep_processing 
and self
._isCharAllowed
( char
, pos
, checkRegex 
= True ): 
2932 ##                dbg("key allowed by mask") 
2933                 # insert key into candidate new value, but don't change control yet: 
2934                 oldstr 
= self
._GetValue
() 
2935                 newstr
, newpos
, new_select_to
, match_field
, match_index 
= self
._insertKey
( 
2936                                 char
, pos
, sel_start
, sel_to
, self
._GetValue
(), allowAutoSelect 
= True) 
2937 ##                dbg("str with '%s' inserted:" % char, '"%s"' % newstr) 
2938                 if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
2939 ##                    dbg('not valid; checking to see if adjusted string is:') 
2940                     keep_processing 
= False 
2941                     if self
._isFloat 
and newstr 
!= self
._template
: 
2942                         newstr 
= self
._adjustFloat
(newstr
) 
2943 ##                        dbg('adjusted str:', newstr) 
2944                         if self
.IsValid(newstr
): 
2946                             keep_processing 
= True 
2947                             wx
.CallAfter(self
._SetInsertionPoint
, self
._decimalpos
) 
2948                     if not keep_processing
: 
2949 ##                        dbg("key disallowed by validation") 
2950                         if not wx
.Validator_IsSilent() and orig_pos 
== pos
: 
2956                     # special case: adjust date value as necessary: 
2957                     if self
._isDate 
and newstr 
!= self
._template
: 
2958                         newstr 
= self
._adjustDate
(newstr
) 
2959 ##                    dbg('adjusted newstr:', newstr) 
2961                     if newstr 
!= orig_value
: 
2962                         self
.modified 
= True 
2964                     wx
.CallAfter(self
._SetValue
, newstr
) 
2966                     # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: 
2967                     if not self
.IsDefault() and self
._isDate 
and self
._4digityear
: 
2968                         year2dig 
= self
._dateExtent 
- 2 
2969                         if pos 
== year2dig 
and unadjusted
[year2dig
] != newstr
[year2dig
]: 
2972 ##                    dbg('queuing insertion point: (%d)' % newpos) 
2973                     wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
2975                     if match_field 
is not None: 
2976 ##                        dbg('matched field') 
2977                         self
._OnAutoSelect
(match_field
, match_index
) 
2979                     if new_select_to 
!= newpos
: 
2980 ##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) 
2981                         wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
2983                         newfield 
= self
._FindField
(newpos
) 
2984                         if newfield 
!= field 
and newfield
._selectOnFieldEntry
: 
2985 ##                            dbg('queuing insertion point: (%d)' % newfield._extent[0]) 
2986                             wx
.CallAfter(self
._SetInsertionPoint
, newfield
._extent
[0]) 
2987 ##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) 
2988                             wx
.CallAfter(self
._SetSelection
, newfield
._extent
[0], newfield
._extent
[1]) 
2990                             wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
2991                     keep_processing 
= False 
2993             elif keep_processing
: 
2994 ##                dbg('char not allowed') 
2995                 keep_processing 
= False 
2996                 if (not wx
.Validator_IsSilent()) and orig_pos 
== pos
: 
2999         self
._applyFormatting
() 
3001         # Move to next insertion point 
3002         if keep_processing 
and key 
not in self
._nav
: 
3003             pos 
= self
._GetInsertionPoint
() 
3004             next_entry 
= self
._findNextEntry
( pos 
) 
3005             if pos 
!= next_entry
: 
3006 ##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) 
3007                 wx
.CallAfter(self
._SetInsertionPoint
, next_entry 
) 
3009             if self
._isTemplateChar
(pos
): 
3010                 self
._AdjustField
(pos
) 
3014     def _FindFieldExtent(self
, pos
=None, getslice
=False, value
=None): 
3015         """ returns editable extent of field corresponding to 
3016         position pos, and, optionally, the contents of that field 
3017         in the control or the value specified. 
3018         Template chars are bound to the preceding field. 
3019         For masks beginning with template chars, these chars are ignored 
3020         when calculating the current field. 
3022         Eg: with template (###) ###-####, 
3023         >>> self._FindFieldExtent(pos=0) 
3025         >>> self._FindFieldExtent(pos=1) 
3027         >>> self._FindFieldExtent(pos=5) 
3029         >>> self._FindFieldExtent(pos=6) 
3031         >>> self._FindFieldExtent(pos=10) 
3035 ##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) 
3037         field 
= self
._FindField
(pos
) 
3040                 return None, None, "" 
3043         edit_start
, edit_end 
= field
._extent
 
3045             if value 
is None: value 
= self
._GetValue
() 
3046             slice = value
[edit_start
:edit_end
] 
3047 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) 
3049             return edit_start
, edit_end
, slice 
3051 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end) 
3053             return edit_start
, edit_end
 
3056     def _FindField(self
, pos
=None): 
3058         Returns the field instance in which pos resides. 
3059         Template chars are bound to the preceding field. 
3060         For masks beginning with template chars, these chars are ignored 
3061         when calculating the current field. 
3064 ####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) 
3065         if pos 
is None: pos 
= self
._GetInsertionPoint
() 
3066         elif pos 
< 0 or pos 
> self
._masklength
: 
3067             raise IndexError('position %s out of range of control' % str(pos
)) 
3069         if len(self
._fields
) == 0: 
3075         return self
._fields
[self
._lookupField
[pos
]] 
3078     def ClearValue(self
): 
3079         """ Blanks the current control value by replacing it with the default value.""" 
3080 ##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") 
3081         self
._SetValue
( self
._template 
) 
3082         self
._SetInsertionPoint
(0) 
3086     def _baseCtrlEventHandler(self
, event
): 
3088         This function is used whenever a key should be handled by the base control. 
3094     def _OnUpNumeric(self
, event
): 
3096         Makes up-arrow act like shift-tab should; ie. take you to start of 
3099 ##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1) 
3100         event
.m_shiftDown 
= 1 
3101 ##        dbg('event.ShiftDown()?', event.ShiftDown()) 
3102         self
._OnChangeField
(event
) 
3106     def _OnArrow(self
, event
): 
3108         Used in response to left/right navigation keys; makes these actions skip 
3109         over mask template chars. 
3111 ##        dbg("MaskedEditMixin::_OnArrow", indent=1) 
3112         pos 
= self
._GetInsertionPoint
() 
3113         keycode 
= event
.GetKeyCode() 
3114         sel_start
, sel_to 
= self
._GetSelection
() 
3115         entry_end 
= self
._goEnd
(getPosOnly
=True) 
3116         if keycode 
in (wx
.WXK_RIGHT
, wx
.WXK_DOWN
): 
3117             if( ( not self
._isTemplateChar
(pos
) and pos
+1 > entry_end
) 
3118                 or ( self
._isTemplateChar
(pos
) and pos 
>= entry_end
) ): 
3119 ##                dbg("can't advance", indent=0) 
3121             elif self
._isTemplateChar
(pos
): 
3122                 self
._AdjustField
(pos
) 
3123         elif keycode 
in (wx
.WXK_LEFT
,wx
.WXK_UP
) and sel_start 
== sel_to 
and pos 
> 0 and self
._isTemplateChar
(pos
-1): 
3124 ##            dbg('adjusting field') 
3125             self
._AdjustField
(pos
) 
3127         # treat as shifted up/down arrows as tab/reverse tab: 
3128         if event
.ShiftDown() and keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
): 
3129             # remove "shifting" and treat as (forward) tab: 
3130             event
.m_shiftDown 
= False 
3131             keep_processing 
= self
._OnChangeField
(event
) 
3133         elif self
._FindField
(pos
)._selectOnFieldEntry
: 
3134             if( keycode 
in (wx
.WXK_UP
, wx
.WXK_LEFT
) 
3136                 and self
._isTemplateChar
(sel_start
-1) 
3137                 and sel_start 
!= self
._masklength
 
3138                 and not self
._signOk 
and not self
._useParens
): 
3140                 # call _OnChangeField to handle "ctrl-shifted event" 
3141                 # (which moves to previous field and selects it.) 
3142                 event
.m_shiftDown 
= True 
3143                 event
.m_ControlDown 
= True 
3144                 keep_processing 
= self
._OnChangeField
(event
) 
3145             elif( keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
) 
3146                   and sel_to 
!= self
._masklength
 
3147                   and self
._isTemplateChar
(sel_to
)): 
3149                 # when changing field to the right, ensure don't accidentally go left instead 
3150                 event
.m_shiftDown 
= False 
3151                 keep_processing 
= self
._OnChangeField
(event
) 
3153                 # treat arrows as normal, allowing selection 
3155 ##                dbg('using base ctrl event processing') 
3158             if( (sel_to 
== self
._fields
[0]._extent
[0] and keycode 
== wx
.WXK_LEFT
) 
3159                 or (sel_to 
== self
._masklength 
and keycode 
== wx
.WXK_RIGHT
) ): 
3160                 if not wx
.Validator_IsSilent(): 
3163                 # treat arrows as normal, allowing selection 
3165 ##                dbg('using base event processing') 
3168         keep_processing 
= False 
3170         return keep_processing
 
3173     def _OnCtrl_S(self
, event
): 
3174         """ Default Ctrl-S handler; prints value information if demo enabled. """ 
3175 ##        dbg("MaskedEditMixin::_OnCtrl_S") 
3177             print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self
.GetValue(), self
.GetPlainValue()) 
3178             print "Valid? => " + str(self
.IsValid()) 
3179             print "Current field, start, end, value =", str( self
._FindFieldExtent
(getslice
=True)) 
3183     def _OnCtrl_X(self
, event
=None): 
3184         """ Handles ctrl-x keypress in control and Cut operation on context menu. 
3185             Should return False to skip other processing. """ 
3186 ##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1) 
3191     def _OnCtrl_C(self
, event
=None): 
3192         """ Handles ctrl-C keypress in control and Copy operation on context menu. 
3193             Uses base control handling. Should return False to skip other processing.""" 
3197     def _OnCtrl_V(self
, event
=None): 
3198         """ Handles ctrl-V keypress in control and Paste operation on context menu. 
3199             Should return False to skip other processing. """ 
3200 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1) 
3205     def _OnCtrl_Z(self
, event
=None): 
3206         """ Handles ctrl-Z keypress in control and Undo operation on context menu. 
3207             Should return False to skip other processing. """ 
3208 ##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) 
3213     def _OnCtrl_A(self
,event
=None): 
3214         """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ 
3215         end 
= self
._goEnd
(getPosOnly
=True) 
3216         if not event 
or (isinstance(event
, wx
.KeyEvent
) and event
.ShiftDown()): 
3217             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3218             wx
.CallAfter(self
._SetSelection
, 0, self
._masklength
) 
3220             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3221             wx
.CallAfter(self
._SetSelection
, 0, end
) 
3225     def _OnErase(self
, event
=None, just_return_value
=False): 
3226         """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" 
3227 ##        dbg("MaskedEditMixin::_OnErase", indent=1) 
3228         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
3230         if event 
is None:   # called as action routine from Cut() operation. 
3233             key 
= event
.GetKeyCode() 
3235         field 
= self
._FindField
(sel_to
) 
3236         start
, end 
= field
._extent
 
3237         value 
= self
._GetValue
() 
3238         oldstart 
= sel_start
 
3240         # If trying to erase beyond "legal" bounds, disallow operation: 
3241         if( (sel_to 
== 0 and key 
== wx
.WXK_BACK
) 
3242             or (self
._signOk 
and sel_to 
== 1 and value
[0] == ' ' and key 
== wx
.WXK_BACK
) 
3243             or (sel_to 
== self
._masklength 
and sel_start 
== sel_to 
and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) 
3244             or (self
._signOk 
and self
._useParens
 
3245                 and sel_start 
== sel_to
 
3246                 and sel_to 
== self
._masklength 
- 1 
3247                 and value
[sel_to
] == ' ' and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) ): 
3248             if not wx
.Validator_IsSilent(): 
3254         if( field
._insertRight                                  
# an insert-right field 
3255             and value
[start
:end
] != self
._template
[start
:end
]   # and field not empty 
3256             and sel_start 
>= start                              
# and selection starts in field 
3257             and ((sel_to 
== sel_start                           
# and no selection 
3258                   and sel_to 
== end                             
# and cursor at right edge 
3259                   and key 
in (wx
.WXK_BACK
, wx
.WXK_DELETE
))            # and either delete or backspace key 
3261                  (key 
== wx
.WXK_BACK                               
# backspacing 
3262                     and (sel_to 
== end                          
# and selection ends at right edge 
3263                          or sel_to 
< end 
and field
._allowInsert
)) ) ):  # or allow right insert at any point in field 
3265 ##            dbg('delete left') 
3266             # if backspace but left of cursor is empty, adjust cursor right before deleting 
3267             while( key 
== wx
.WXK_BACK
 
3268                    and sel_start 
== sel_to
 
3270                    and value
[start
:sel_start
] == self
._template
[start
:sel_start
]): 
3274 ##            dbg('sel_start, start:', sel_start, start) 
3276             if sel_start 
== sel_to
: 
3280             newfield 
= value
[start
:keep
] + value
[sel_to
:end
] 
3282             # handle sign char moving from outside field into the field: 
3283             move_sign_into_field 
= False 
3284             if not field
._padZero 
and self
._signOk 
and self
._isNeg 
and value
[0] in ('-', '('): 
3286                 newfield 
= signchar 
+ newfield
 
3287                 move_sign_into_field 
= True 
3288 ##            dbg('cut newfield: "%s"' % newfield) 
3290             # handle what should fill in from the left: 
3292             for i 
in range(start
, end 
- len(newfield
)): 
3295                 elif( self
._signOk 
and self
._isNeg 
and i 
== 1 
3296                       and ((self
._useParens 
and newfield
.find('(') == -1) 
3297                            or (not self
._useParens 
and newfield
.find('-') == -1)) ): 
3300                     left 
+= self
._template
[i
]   # this can produce strange results in combination with default values... 
3301             newfield 
= left 
+ newfield
 
3302 ##            dbg('filled newfield: "%s"' % newfield) 
3304             newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3306             # (handle sign located in "mask position" in front of field prior to delete) 
3307             if move_sign_into_field
: 
3308                 newstr 
= ' ' + newstr
[1:] 
3311             # handle erasure of (left) sign, moving selection accordingly... 
3312             if self
._signOk 
and sel_start 
== 0: 
3313                 newstr 
= value 
= ' ' + value
[1:] 
3316             if field
._allowInsert 
and sel_start 
>= start
: 
3317                 # selection (if any) falls within current insert-capable field: 
3318                 select_len 
= sel_to 
- sel_start
 
3319                 # determine where cursor should end up: 
3320                 if key 
== wx
.WXK_BACK
: 
3322                         newpos 
= sel_start 
-1 
3328                     if sel_to 
== sel_start
: 
3329                         erase_to 
= sel_to 
+ 1 
3333                 if self
._isTemplateChar
(newpos
) and select_len 
== 0: 
3335                         if value
[newpos
] in ('(', '-'): 
3336                             newpos 
+= 1     # don't move cusor 
3337                             newstr 
= ' ' + value
[newpos
:] 
3338                         elif value
[newpos
] == ')': 
3339                             # erase right sign, but don't move cursor; (matching left sign handled later) 
3340                             newstr 
= value
[:newpos
] + ' ' 
3342                             # no deletion; just move cursor 
3345                         # no deletion; just move cursor 
3348                     if erase_to 
> end
: erase_to 
= end
 
3349                     erase_len 
= erase_to 
- newpos
 
3351                     left 
= value
[start
:newpos
] 
3352 ##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) 
3353                     right 
= value
[erase_to
:end
] + self
._template
[end
-erase_len
:end
] 
3355                     if field
._alignRight
: 
3356                         rstripped 
= right
.rstrip() 
3357                         if rstripped 
!= right
: 
3358                             pos_adjust 
= len(right
) - len(rstripped
) 
3361                     if not field
._insertRight 
and value
[-1] == ')' and end 
== self
._masklength 
- 1: 
3362                         # need to shift ) into the field: 
3363                         right 
= right
[:-1] + ')' 
3364                         value 
= value
[:-1] + ' ' 
3366                     newfield 
= left
+right
 
3368                         newfield 
= newfield
.rjust(end
-start
) 
3369                         newpos 
+= pos_adjust
 
3370 ##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) 
3371                     newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3376                 if sel_start 
== sel_to
: 
3377 ##                    dbg("current sel_start, sel_to:", sel_start, sel_to) 
3378                     if key 
== wx
.WXK_BACK
: 
3379                         sel_start
, sel_to 
= sel_to
-1, sel_to
-1 
3380 ##                        dbg("new sel_start, sel_to:", sel_start, sel_to) 
3382                     if field
._padZero 
and not value
[start
:sel_to
].replace('0', '').replace(' ','').replace(field
._fillChar
, ''): 
3383                         # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: 
3386                         newchar 
= self
._template
[sel_to
] ## get an original template character to "clear" the current char 
3387 ##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) 
3389                     if self
._isTemplateChar
(sel_to
): 
3390                         if sel_to 
== 0 and self
._signOk 
and value
[sel_to
] == '-':   # erasing "template" sign char 
3391                             newstr 
= ' ' + value
[1:] 
3393                         elif self
._signOk 
and self
._useParens 
and (value
[sel_to
] == ')' or value
[sel_to
] == '('): 
3394                             # allow "change sign" by removing both parens: 
3395                             newstr 
= value
[:self
._signpos
] + ' ' + value
[self
._signpos
+1:-1] + ' ' 
3400                         if field
._insertRight 
and sel_start 
== sel_to
: 
3401                             # force non-insert-right behavior, by selecting char to be replaced: 
3403                         newstr
, ignore 
= self
._insertKey
(newchar
, sel_start
, sel_start
, sel_to
, value
) 
3407                     newstr 
= self
._eraseSelection
(value
, sel_start
, sel_to
) 
3409                 pos 
= sel_start  
# put cursor back at beginning of selection 
3411         if self
._signOk 
and self
._useParens
: 
3412             # account for resultant unbalanced parentheses: 
3413             left_signpos 
= newstr
.find('(') 
3414             right_signpos 
= newstr
.find(')') 
3416             if left_signpos 
== -1 and right_signpos 
!= -1: 
3417                 # erased left-sign marker; get rid of right sign marker: 
3418                 newstr 
= newstr
[:right_signpos
] + ' ' + newstr
[right_signpos
+1:] 
3420             elif left_signpos 
!= -1 and right_signpos 
== -1: 
3421                 # erased right-sign marker; get rid of left-sign marker: 
3422                 newstr 
= newstr
[:left_signpos
] + ' ' + newstr
[left_signpos
+1:] 
3424 ##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) 
3425 ##        dbg("newstr:'%s'" % newstr, 'pos:', pos) 
3427         # if erasure results in an invalid field, disallow it: 
3428 ##        dbg('field._validRequired?', field._validRequired) 
3429 ##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) 
3430         if field
._validRequired 
and not field
.IsValid(newstr
[start
:end
]): 
3431             if not wx
.Validator_IsSilent(): 
3436         # if erasure results in an invalid value, disallow it: 
3437         if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3438             if not wx
.Validator_IsSilent(): 
3443         if just_return_value
: 
3448 ##        dbg('setting value (later) to', newstr) 
3449         wx
.CallAfter(self
._SetValue
, newstr
) 
3450 ##        dbg('setting insertion point (later) to', pos) 
3451         wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3454             self
.modified 
= True 
3458     def _OnEnd(self
,event
): 
3459         """ Handles End keypress in control. Should return False to skip other processing. """ 
3460 ##        dbg("MaskedEditMixin::_OnEnd", indent=1) 
3461         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3462         if not event
.ControlDown(): 
3463             end 
= self
._masklength  
# go to end of control 
3464             if self
._signOk 
and self
._useParens
: 
3465                 end 
= end 
- 1       # account for reserved char at end 
3467             end_of_input 
= self
._goEnd
(getPosOnly
=True) 
3468             sel_start
, sel_to 
= self
._GetSelection
() 
3469             if sel_to 
< pos
: sel_to 
= pos
 
3470             field 
= self
._FindField
(sel_to
) 
3471             field_end 
= self
._FindField
(end_of_input
) 
3473             # pick different end point if either: 
3474             # - cursor not in same field 
3475             # - or at or past last input already 
3476             # - or current selection = end of current field: 
3477 ####            dbg('field != field_end?', field != field_end) 
3478 ####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input) 
3479             if field 
!= field_end 
or sel_to 
>= end_of_input
: 
3480                 edit_start
, edit_end 
= field
._extent
 
3481 ####                dbg('edit_end:', edit_end) 
3482 ####                dbg('sel_to:', sel_to) 
3483 ####                dbg('sel_to == edit_end?', sel_to == edit_end) 
3484 ####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) 
3486                 if sel_to 
== edit_end 
and field
._index 
< self
._field
_indices
[-1]: 
3487                     edit_start
, edit_end 
= self
._FindFieldExtent
(self
._findNextEntry
(edit_end
))  # go to end of next field: 
3489 ##                    dbg('end moved to', end) 
3491                 elif sel_to 
== edit_end 
and field
._index 
== self
._field
_indices
[-1]: 
3492                     # already at edit end of last field; select to end of control: 
3493                     end 
= self
._masklength
 
3494 ##                    dbg('end moved to', end) 
3496                     end 
= edit_end  
# select to end of current field 
3497 ##                    dbg('end moved to ', end) 
3499                 # select to current end of input 
3503 ####        dbg('pos:', pos, 'end:', end) 
3505         if event
.ShiftDown(): 
3506             if not event
.ControlDown(): 
3507 ##                dbg("shift-end; select to end of control") 
3510 ##                dbg("shift-ctrl-end; select to end of non-whitespace") 
3512             wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3513             wx
.CallAfter(self
._SetSelection
, pos
, end
) 
3515             if not event
.ControlDown(): 
3516 ##                dbg('go to end of control:') 
3518             wx
.CallAfter(self
._SetInsertionPoint
, end
) 
3519             wx
.CallAfter(self
._SetSelection
, end
, end
) 
3525     def _OnReturn(self
, event
): 
3527          Swallows the return, issues a Navigate event instead, since 
3528          masked controls are "single line" by defn. 
3530 ##         dbg('MaskedEditMixin::OnReturn') 
3535     def _OnHome(self
,event
): 
3536         """ Handles Home keypress in control. Should return False to skip other processing.""" 
3537 ##        dbg("MaskedEditMixin::_OnHome", indent=1) 
3538         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3539         sel_start
, sel_to 
= self
._GetSelection
() 
3541         # There are 5 cases here: 
3543         # 1) shift: select from start of control to end of current 
3545         if event
.ShiftDown() and not event
.ControlDown(): 
3546 ##            dbg("shift-home; select to start of control") 
3550         # 2) no shift, no control: move cursor to beginning of control. 
3551         elif not event
.ControlDown(): 
3552 ##            dbg("home; move to start of control") 
3556         # 3) No shift, control: move cursor back to beginning of field; if 
3557         #    there already, go to beginning of previous field. 
3558         # 4) shift, control, start of selection not at beginning of control: 
3559         #    move sel_start back to start of field; if already there, go to 
3560         #    start of previous field. 
3561         elif( event
.ControlDown() 
3562               and (not event
.ShiftDown() 
3563                    or (event
.ShiftDown() and sel_start 
> 0) ) ): 
3564             if len(self
._field
_indices
) > 1: 
3565                 field 
= self
._FindField
(sel_start
) 
3566                 start
, ignore 
= field
._extent
 
3567                 if sel_start 
== start 
and field
._index 
!= self
._field
_indices
[0]:  # go to start of previous field: 
3568                     start
, ignore 
= self
._FindFieldExtent
(sel_start
-1) 
3569                 elif sel_start 
== start
: 
3570                     start 
= 0   # go to literal beginning if edit start 
3577             if not event
.ShiftDown(): 
3578 ##                dbg("ctrl-home; move to beginning of field") 
3581 ##                dbg("shift-ctrl-home; select to beginning of field") 
3585         # 5) shift, control, start of selection at beginning of control: 
3586         #    unselect by moving sel_to backward to beginning of current field; 
3587         #    if already there, move to start of previous field. 
3589             if len(self
._field
_indices
) > 1: 
3590                 # find end of previous field: 
3591                 field 
= self
._FindField
(sel_to
) 
3592                 if sel_to 
> start 
and field
._index 
!= self
._field
_indices
[0]: 
3593                     ignore
, end 
= self
._FindFieldExtent
(field
._extent
[0]-1) 
3599                 end_of_field 
= False 
3600 ##            dbg("shift-ctrl-home; unselect to beginning of field") 
3602 ##        dbg('queuing new sel_start, sel_to:', (start, end)) 
3603         wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3604         wx
.CallAfter(self
._SetSelection
, start
, end
) 
3609     def _OnChangeField(self
, event
): 
3611         Primarily handles TAB events, but can be used for any key that 
3612         designer wants to change fields within a masked edit control. 
3614 ##        dbg('MaskedEditMixin::_OnChangeField', indent = 1) 
3615         # determine end of current field: 
3616         pos 
= self
._GetInsertionPoint
() 
3617 ##        dbg('current pos:', pos) 
3618         sel_start
, sel_to 
= self
._GetSelection
() 
3620         if self
._masklength 
< 0:   # no fields; process tab normally 
3621             self
._AdjustField
(pos
) 
3622             if event
.GetKeyCode() == wx
.WXK_TAB
: 
3623 ##                dbg('tab to next ctrl') 
3624                 # As of 2.5.2, you don't call event.Skip() to do 
3625                 # this, but instead force explicit navigation, if 
3626                 # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3633         if event
.ShiftDown(): 
3637             # NOTE: doesn't yet work with SHIFT-tab under wx; the control 
3638             # never sees this event! (But I've coded for it should it ever work, 
3639             # and it *does* work for '.' in IpAddrCtrl.) 
3640             field 
= self
._FindField
(pos
) 
3641             index 
= field
._index
 
3642             field_start 
= field
._extent
[0] 
3643             if pos 
< field_start
: 
3644 ##                dbg('cursor before 1st field; cannot change to a previous field') 
3645                 if not wx
.Validator_IsSilent(): 
3649             if event
.ControlDown(): 
3650 ##                dbg('queuing select to beginning of field:', field_start, pos) 
3651                 wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3652                 wx
.CallAfter(self
._SetSelection
, field_start
, pos
) 
3657                   # We're already in the 1st field; process shift-tab normally: 
3658                 self
._AdjustField
(pos
) 
3659                 if event
.GetKeyCode() == wx
.WXK_TAB
: 
3660 ##                    dbg('tab to previous ctrl') 
3661                     # As of 2.5.2, you don't call event.Skip() to do 
3662                     # this, but instead force explicit navigation, if 
3663                     # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3664                     self
.Navigate(False) 
3666 ##                    dbg('position at beginning') 
3667                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3671                 # find beginning of previous field: 
3672                 begin_prev 
= self
._FindField
(field_start
-1)._extent
[0] 
3673                 self
._AdjustField
(pos
) 
3674 ##                dbg('repositioning to', begin_prev) 
3675                 wx
.CallAfter(self
._SetInsertionPoint
, begin_prev
) 
3676                 if self
._FindField
(begin_prev
)._selectOnFieldEntry
: 
3677                     edit_start
, edit_end 
= self
._FindFieldExtent
(begin_prev
) 
3678 ##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) 
3679                     wx
.CallAfter(self
._SetInsertionPoint
, edit_start
) 
3680                     wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3686             field 
= self
._FindField
(sel_to
) 
3687             field_start
, field_end 
= field
._extent
 
3688             if event
.ControlDown(): 
3689 ##                dbg('queuing select to end of field:', pos, field_end) 
3690                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3691                 wx
.CallAfter(self
._SetSelection
, pos
, field_end
) 
3695                 if pos 
< field_start
: 
3696 ##                    dbg('cursor before 1st field; go to start of field') 
3697                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3698                     if field
._selectOnFieldEntry
: 
3699                         wx
.CallAfter(self
._SetSelection
, field_start
, field_end
) 
3701                         wx
.CallAfter(self
._SetSelection
, field_start
, field_start
) 
3704 ##                dbg('end of current field:', field_end) 
3705 ##                dbg('go to next field') 
3706                 if field_end 
== self
._fields
[self
._field
_indices
[-1]]._extent
[1]: 
3707                     self
._AdjustField
(pos
) 
3708                     if event
.GetKeyCode() == wx
.WXK_TAB
: 
3709 ##                        dbg('tab to next ctrl') 
3710                         # As of 2.5.2, you don't call event.Skip() to do 
3711                         # this, but instead force explicit navigation, if 
3712                         # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3715 ##                        dbg('position at end') 
3716                         wx
.CallAfter(self
._SetInsertionPoint
, field_end
) 
3720                     # we have to find the start of the next field 
3721                     next_pos 
= self
._findNextEntry
(field_end
) 
3722                     if next_pos 
== field_end
: 
3723 ##                        dbg('already in last field') 
3724                         self
._AdjustField
(pos
) 
3725                         if event
.GetKeyCode() == wx
.WXK_TAB
: 
3726 ##                            dbg('tab to next ctrl') 
3727                             # As of 2.5.2, you don't call event.Skip() to do 
3728                             # this, but instead force explicit navigation, if 
3729                             # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3735                         self
._AdjustField
( pos 
) 
3737                         # move cursor to appropriate point in the next field and select as necessary: 
3738                         field 
= self
._FindField
(next_pos
) 
3739                         edit_start
, edit_end 
= field
._extent
 
3740                         if field
._selectOnFieldEntry
: 
3741 ##                            dbg('move to ', next_pos) 
3742                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3743                             edit_start
, edit_end 
= self
._FindFieldExtent
(next_pos
) 
3744 ##                            dbg('queuing select', edit_start, edit_end) 
3745                             wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3747                             if field
._insertRight
: 
3748                                 next_pos 
= field
._extent
[1] 
3749 ##                            dbg('move to ', next_pos) 
3750                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3755     def _OnDecimalPoint(self
, event
): 
3756 ##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) 
3758         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3760         if self
._isFloat
:       ## handle float value, move to decimal place 
3761 ##            dbg('key == Decimal tab; decimal pos:', self._decimalpos) 
3762             value 
= self
._GetValue
() 
3763             if pos 
< self
._decimalpos
: 
3764                 clipped_text 
= value
[0:pos
] + self
._decimalChar 
+ value
[self
._decimalpos
+1:] 
3765 ##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3766                 newstr 
= self
._adjustFloat
(clipped_text
) 
3768                 newstr 
= self
._adjustFloat
(value
) 
3769             wx
.CallAfter(self
._SetValue
, newstr
) 
3770             fraction 
= self
._fields
[1] 
3771             start
, end 
= fraction
._extent
 
3772             wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3773             if fraction
._selectOnFieldEntry
: 
3774 ##                dbg('queuing selection after decimal point to:', (start, end)) 
3775                 wx
.CallAfter(self
._SetSelection
, start
, end
) 
3777                 wx
.CallAfter(self
._SetSelection
, start
, start
) 
3778             keep_processing 
= False 
3780         if self
._isInt
:      ## handle integer value, truncate from current position 
3781 ##            dbg('key == Integer decimal event') 
3782             value 
= self
._GetValue
() 
3783             clipped_text 
= value
[0:pos
] 
3784 ##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3785             newstr 
= self
._adjustInt
(clipped_text
) 
3786 ##            dbg('newstr: "%s"' % newstr) 
3787             wx
.CallAfter(self
._SetValue
, newstr
) 
3788             newpos 
= len(newstr
.rstrip()) 
3789             if newstr
.find(')') != -1: 
3790                 newpos 
-= 1     # (don't move past right paren) 
3791             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3792             wx
.CallAfter(self
._SetSelection
, newpos
, newpos
) 
3793             keep_processing 
= False 
3797     def _OnChangeSign(self
, event
): 
3798 ##        dbg('MaskedEditMixin::_OnChangeSign', indent=1) 
3799         key 
= event
.GetKeyCode() 
3800         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), key
) 
3801         value 
= self
._eraseSelection
() 
3802         integer 
= self
._fields
[0] 
3803         start
, end 
= integer
._extent
 
3804         sel_start
, sel_to 
= self
._GetSelection
() 
3806 ####        dbg('adjusted pos:', pos) 
3807         if chr(key
) in ('-','+','(', ')') or (chr(key
) == " " and pos 
== self
._signpos
): 
3808             cursign 
= self
._isNeg
 
3809 ##            dbg('cursign:', cursign) 
3810             if chr(key
) in ('-','(', ')'): 
3811                 if sel_start 
<= self
._signpos
: 
3814                     self
._isNeg 
= (not self
._isNeg
)   ## flip value 
3817 ##            dbg('isNeg?', self._isNeg) 
3819             text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
(candidate
=value
) 
3820 ##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) 
3824             if self
._isNeg 
and self
._signpos 
is not None and self
._signpos 
!= -1: 
3825                 if self
._useParens 
and self
._right
_signpos 
is not None: 
3826                     text 
= text
[:self
._signpos
] + '(' + text
[self
._signpos
+1:self
._right
_signpos
] + ')' + text
[self
._right
_signpos
+1:] 
3828                     text 
= text
[:self
._signpos
] + '-' + text
[self
._signpos
+1:] 
3830 ####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) 
3832                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:self
._right
_signpos
] + ' ' + text
[self
._right
_signpos
+1:] 
3834                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:] 
3835 ##                dbg('clearing self._isNeg') 
3838             wx
.CallAfter(self
._SetValue
, text
) 
3839             wx
.CallAfter(self
._applyFormatting
) 
3840 ##            dbg('pos:', pos, 'signpos:', self._signpos) 
3841             if pos 
== self
._signpos 
or integer
.IsEmpty(text
[start
:end
]): 
3842                 wx
.CallAfter(self
._SetInsertionPoint
, self
._signpos
+1) 
3844                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3846             keep_processing 
= False 
3848             keep_processing 
= True 
3850         return keep_processing
 
3853     def _OnGroupChar(self
, event
): 
3855         This handler is only registered if the mask is a numeric mask. 
3856         It allows the insertion of ',' or '.' if appropriate. 
3858 ##        dbg('MaskedEditMixin::_OnGroupChar', indent=1) 
3859         keep_processing 
= True 
3860         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3861         sel_start
, sel_to 
= self
._GetSelection
() 
3862         groupchar 
= self
._fields
[0]._groupChar
 
3863         if not self
._isCharAllowed
(groupchar
, pos
, checkRegex
=True): 
3864             keep_processing 
= False 
3865             if not wx
.Validator_IsSilent(): 
3869             newstr
, newpos 
= self
._insertKey
(groupchar
, pos
, sel_start
, sel_to
, self
._GetValue
() ) 
3870 ##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) 
3871             if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3872                 keep_processing 
= False 
3873                 if not wx
.Validator_IsSilent(): 
3877             wx
.CallAfter(self
._SetValue
, newstr
) 
3878             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3879         keep_processing 
= False 
3881         return keep_processing
 
3884     def _findNextEntry(self
,pos
, adjustInsert
=True): 
3885         """ Find the insertion point for the next valid entry character position.""" 
3886         if self
._isTemplateChar
(pos
):   # if changing fields, pay attn to flag 
3887             adjustInsert 
= adjustInsert
 
3888         else:                           # else within a field; flag not relevant 
3889             adjustInsert 
= False 
3891         while self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3894         # if changing fields, and we've been told to adjust insert point, 
3895         # look at new field; if empty and right-insert field, 
3896         # adjust to right edge: 
3897         if adjustInsert 
and pos 
< self
._masklength
: 
3898             field 
= self
._FindField
(pos
) 
3899             start
, end 
= field
._extent
 
3900             slice = self
._GetValue
()[start
:end
] 
3901             if field
._insertRight 
and field
.IsEmpty(slice): 
3906     def _findNextTemplateChar(self
, pos
): 
3907         """ Find the position of the next non-editable character in the mask.""" 
3908         while not self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3913     def _OnAutoCompleteField(self
, event
): 
3914 ##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) 
3915         pos 
= self
._GetInsertionPoint
() 
3916         field 
= self
._FindField
(pos
) 
3917         edit_start
, edit_end
, slice = self
._FindFieldExtent
(pos
, getslice
=True) 
3920         keycode 
= event
.GetKeyCode() 
3922         if field
._fillChar 
!= ' ': 
3923             text 
= slice.replace(field
._fillChar
, '') 
3927         keep_processing 
= True  # (assume True to start) 
3928 ##        dbg('field._hasList?', field._hasList) 
3930 ##            dbg('choices:', field._choices) 
3931 ##            dbg('compareChoices:', field._compareChoices) 
3932             choices
, choice_required 
= field
._compareChoices
, field
._choiceRequired
 
3933             if keycode 
in (wx
.WXK_PRIOR
, wx
.WXK_UP
): 
3937             match_index
, partial_match 
= self
._autoComplete
(direction
, choices
, text
, compareNoCase
=field
._compareNoCase
, current_index 
= field
._autoCompleteIndex
) 
3938             if( match_index 
is None 
3939                 and (keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3940                      or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown() ) ) ): 
3941                 # Select the 1st thing from the list: 
3944             if( match_index 
is not None 
3945                 and ( keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3946                       or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown()) 
3947                       or (keycode 
== wx
.WXK_DOWN 
and partial_match
) ) ): 
3949                 # We're allowed to auto-complete: 
3950 ##                dbg('match found') 
3951                 value 
= self
._GetValue
() 
3952                 newvalue 
= value
[:edit_start
] + field
._choices
[match_index
] + value
[edit_end
:] 
3953 ##                dbg('setting value to "%s"' % newvalue) 
3954                 self
._SetValue
(newvalue
) 
3955                 self
._SetInsertionPoint
(min(edit_end
, len(newvalue
.rstrip()))) 
3956                 self
._OnAutoSelect
(field
, match_index
) 
3957                 self
._CheckValid
()  # recolor as appopriate 
3960         if keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
): 
3961             # treat as left right arrow if unshifted, tab/shift tab if shifted. 
3962             if event
.ShiftDown(): 
3963                 if keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
): 
3964                     # remove "shifting" and treat as (forward) tab: 
3965                     event
.m_shiftDown 
= False 
3966                 keep_processing 
= self
._OnChangeField
(event
) 
3968                 keep_processing 
= self
._OnArrow
(event
) 
3969         # else some other key; keep processing the key 
3971 ##        dbg('keep processing?', keep_processing, indent=0) 
3972         return keep_processing
 
3975     def _OnAutoSelect(self
, field
, match_index 
= None): 
3977         Function called if autoselect feature is enabled and entire control 
3980 ##        dbg('MaskedEditMixin::OnAutoSelect', field._index) 
3981         if match_index 
is not None: 
3982             field
._autoCompleteIndex 
= match_index
 
3985     def _autoComplete(self
, direction
, choices
, value
, compareNoCase
, current_index
): 
3987         This function gets called in response to Auto-complete events. 
3988         It attempts to find a match to the specified value against the 
3989         list of choices; if exact match, the index of then next 
3990         appropriate value in the list, based on the given direction. 
3991         If not an exact match, it will return the index of the 1st value from 
3992         the choice list for which the partial value can be extended to match. 
3993         If no match found, it will return None. 
3994         The function returns a 2-tuple, with the 2nd element being a boolean 
3995         that indicates if partial match was necessary. 
3997 ##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) 
3999 ##            dbg('nothing to match against', indent=0) 
4000             return (None, False) 
4002         partial_match 
= False 
4005             value 
= value
.lower() 
4007         last_index 
= len(choices
) - 1 
4008         if value 
in choices
: 
4009 ##            dbg('"%s" in', choices) 
4010             if current_index 
is not None and choices
[current_index
] == value
: 
4011                 index 
= current_index
 
4013                 index 
= choices
.index(value
) 
4015 ##            dbg('matched "%s" (%d)' % (choices[index], index)) 
4017 ##                dbg('going to previous') 
4018                 if index 
== 0: index 
= len(choices
) - 1 
4021                 if index 
== len(choices
) - 1: index 
= 0 
4023 ##            dbg('change value to "%s" (%d)' % (choices[index], index)) 
4026             partial_match 
= True 
4027             value 
= value
.strip() 
4028 ##            dbg('no match; try to auto-complete:') 
4030 ##            dbg('searching for "%s"' % value) 
4031             if current_index 
is None: 
4032                 indices 
= range(len(choices
)) 
4037                     indices 
= range(current_index 
+1, len(choices
)) + range(current_index
+1) 
4038 ##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) 
4040                     indices 
= range(current_index
-1, -1, -1) + range(len(choices
)-1, current_index
-1, -1) 
4041 ##                    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) 
4042 ####            dbg('indices:', indices) 
4043             for index 
in indices
: 
4044                 choice 
= choices
[index
] 
4045                 if choice
.find(value
, 0) == 0: 
4046 ##                    dbg('match found:', choice) 
4050 ##                    dbg('choice: "%s" - no match' % choice) 
4052             if match 
is not None: 
4053 ##                dbg('matched', match) 
4056 ##                dbg('no match found') 
4059         return (match
, partial_match
) 
4062     def _AdjustField(self
, pos
): 
4064         This function gets called by default whenever the cursor leaves a field. 
4065         The pos argument given is the char position before leaving that field. 
4066         By default, floating point, integer and date values are adjusted to be 
4067         legal in this function.  Derived classes may override this function 
4068         to modify the value of the control in a different way when changing fields. 
4070         NOTE: these change the value immediately, and restore the cursor to 
4071         the passed location, so that any subsequent code can then move it 
4072         based on the operation being performed. 
4074         newvalue 
= value 
= self
._GetValue
() 
4075         field 
= self
._FindField
(pos
) 
4076         start
, end
, slice = self
._FindFieldExtent
(getslice
=True) 
4077         newfield 
= field
._AdjustField
(slice) 
4078         newvalue 
= value
[:start
] + newfield 
+ value
[end
:] 
4080         if self
._isFloat 
and newvalue 
!= self
._template
: 
4081             newvalue 
= self
._adjustFloat
(newvalue
) 
4083         if self
._ctrl
_constraints
._isInt 
and value 
!= self
._template
: 
4084             newvalue 
= self
._adjustInt
(value
) 
4086         if self
._isDate 
and value 
!= self
._template
: 
4087             newvalue 
= self
._adjustDate
(value
, fixcentury
=True) 
4088             if self
._4digityear
: 
4089                 year2dig 
= self
._dateExtent 
- 2 
4090                 if pos 
== year2dig 
and value
[year2dig
] != newvalue
[year2dig
]: 
4093         if newvalue 
!= value
: 
4094 ##            dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue)) 
4095             self
._SetValue
(newvalue
) 
4096             self
._SetInsertionPoint
(pos
) 
4099     def _adjustKey(self
, pos
, key
): 
4100         """ Apply control formatting to the key (e.g. convert to upper etc). """ 
4101         field 
= self
._FindField
(pos
) 
4102         if field
._forceupper 
and key 
in range(97,123): 
4103             key 
= ord( chr(key
).upper()) 
4105         if field
._forcelower 
and key 
in range(97,123): 
4106             key 
= ord( chr(key
).lower()) 
4111     def _adjustPos(self
, pos
, key
): 
4113         Checks the current insertion point position and adjusts it if 
4114         necessary to skip over non-editable characters. 
4116 ##        dbg('_adjustPos', pos, key, indent=1) 
4117         sel_start
, sel_to 
= self
._GetSelection
() 
4118         # If a numeric or decimal mask, and negatives allowed, reserve the 
4119         # first space for sign, and last one if using parens. 
4121             and ((pos 
== self
._signpos 
and key 
in (ord('-'), ord('+'), ord(' ')) ) 
4122                  or (self
._useParens 
and pos 
== self
._masklength 
-1))): 
4123 ##            dbg('adjusted pos:', pos, indent=0) 
4126         if key 
not in self
._nav
: 
4127             field 
= self
._FindField
(pos
) 
4129 ##            dbg('field._insertRight?', field._insertRight) 
4130 ##            if self._signOk: dbg('self._signpos:', self._signpos) 
4131             if field
._insertRight
:              # if allow right-insert 
4132                 start
, end 
= field
._extent
 
4133                 slice = self
._GetValue
()[start
:end
].strip() 
4134                 field_len 
= end 
- start
 
4135                 if pos 
== end
:                      # if cursor at right edge of field 
4136                     # if not filled or supposed to stay in field, keep current position 
4137 ####                    dbg('pos==end') 
4138 ####                    dbg('len (slice):', len(slice)) 
4139 ####                    dbg('field_len?', field_len) 
4140 ####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) 
4141 ####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) 
4142                     if len(slice) == field_len 
and field
._moveOnFieldFull
: 
4143                         # move cursor to next field: 
4144                         pos 
= self
._findNextEntry
(pos
) 
4145                         self
._SetInsertionPoint
(pos
) 
4147                             self
._SetSelection
(pos
, sel_to
)     # restore selection 
4149                             self
._SetSelection
(pos
, pos
)        # remove selection 
4150                     else: # leave cursor alone 
4153                     # if at start of control, move to right edge 
4154                     if (sel_to 
== sel_start
 
4155                         and (self
._isTemplateChar
(pos
) or (pos 
== start 
and len(slice)+ 1 < field_len
)) 
4157                         pos 
= end                   
# move to right edge 
4158 ##                    elif sel_start <= start and sel_to == end: 
4159 ##                        # select to right edge of field - 1 (to replace char) 
4161 ##                        self._SetInsertionPoint(pos) 
4162 ##                        # restore selection 
4163 ##                        self._SetSelection(sel_start, pos) 
4165                     # if selected to beginning and signed, and not changing sign explicitly: 
4166                     elif self
._signOk 
and sel_start 
== 0 and key 
not in (ord('-'), ord('+'), ord(' ')): 
4167                         # adjust to past reserved sign position: 
4168                         pos 
= self
._fields
[0]._extent
[0] 
4169 ##                        dbg('adjusting field to ', pos) 
4170                         self
._SetInsertionPoint
(pos
) 
4171                         # but keep original selection, to allow replacement of any sign:  
4172                         self
._SetSelection
(0, sel_to
) 
4174                         pass    # leave position/selection alone 
4176             # else make sure the user is not trying to type over a template character 
4177             # If they are, move them to the next valid entry position 
4178             elif self
._isTemplateChar
(pos
): 
4179                 if( not field
._moveOnFieldFull
 
4180                       and (not self
._signOk
 
4182                                and field
._index 
== 0 
4183                                and pos 
> 0) ) ):      # don't move to next field without explicit cursor movement 
4186                     # find next valid position 
4187                     pos 
= self
._findNextEntry
(pos
) 
4188                     self
._SetInsertionPoint
(pos
) 
4189                     if pos 
< sel_to
:    # restore selection 
4190                         self
._SetSelection
(pos
, sel_to
) 
4192                         self
._SetSelection
(pos
, pos
) 
4193 ##        dbg('adjusted pos:', pos, indent=0) 
4197     def _adjustFloat(self
, candidate
=None): 
4199         'Fixes' an floating point control. Collapses spaces, right-justifies, etc. 
4201 ##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) 
4202         lenInt
,lenFraction  
= [len(s
) for s 
in self
._mask
.split('.')]  ## Get integer, fraction lengths 
4204         if candidate 
is None: value 
= self
._GetValue
() 
4205         else: value 
= candidate
 
4206 ##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) 
4207         intStr
, fracStr 
= value
.split(self
._decimalChar
) 
4209         intStr 
= self
._fields
[0]._AdjustField
(intStr
) 
4210 ##        dbg('adjusted intStr: "%s"' % intStr) 
4211         lenInt 
= len(intStr
) 
4212         fracStr 
= fracStr 
+ ('0'*(lenFraction
-len(fracStr
)))  # add trailing spaces to decimal 
4214 ##        dbg('intStr "%(intStr)s"' % locals()) 
4215 ##        dbg('lenInt:', lenInt) 
4217         intStr 
= string
.rjust( intStr
[-lenInt
:], lenInt
) 
4218 ##        dbg('right-justifed intStr = "%(intStr)s"' % locals()) 
4219         newvalue 
= intStr 
+ self
._decimalChar 
+ fracStr
 
4222             if len(newvalue
) < self
._masklength
: 
4223                 newvalue 
= ' ' + newvalue
 
4224             signedvalue 
= self
._getSignedValue
(newvalue
)[0] 
4225             if signedvalue 
is not None: newvalue 
= signedvalue
 
4227         # Finally, align string with decimal position, left-padding with 
4229         newdecpos 
= newvalue
.find(self
._decimalChar
) 
4230         if newdecpos 
< self
._decimalpos
: 
4231             padlen 
= self
._decimalpos 
- newdecpos
 
4232             newvalue 
= string
.join([' ' * padlen
] + [newvalue
] ,'') 
4234         if self
._signOk 
and self
._useParens
: 
4235             if newvalue
.find('(') != -1: 
4236                 newvalue 
= newvalue
[:-1] + ')' 
4238                 newvalue 
= newvalue
[:-1] + ' ' 
4240 ##        dbg('newvalue = "%s"' % newvalue) 
4241         if candidate 
is None: 
4242             wx
.CallAfter(self
._SetValue
, newvalue
) 
4247     def _adjustInt(self
, candidate
=None): 
4248         """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" 
4249 ##        dbg("MaskedEditMixin::_adjustInt", candidate) 
4250         lenInt 
= self
._masklength
 
4251         if candidate 
is None: value 
= self
._GetValue
() 
4252         else: value 
= candidate
 
4254         intStr 
= self
._fields
[0]._AdjustField
(value
) 
4255         intStr 
= intStr
.strip() # drop extra spaces 
4256 ##        dbg('adjusted field: "%s"' % intStr) 
4258         if self
._isNeg 
and intStr
.find('-') == -1 and intStr
.find('(') == -1: 
4260                 intStr 
= '(' + intStr 
+ ')' 
4262                 intStr 
= '-' + intStr
 
4263         elif self
._isNeg 
and intStr
.find('-') != -1 and self
._useParens
: 
4264             intStr 
= intStr
.replace('-', '(') 
4266         if( self
._signOk 
and ((self
._useParens 
and intStr
.find('(') == -1) 
4267                                 or (not self
._useParens 
and intStr
.find('-') == -1))): 
4268             intStr 
= ' ' + intStr
 
4270                 intStr 
+= ' '   # space for right paren position 
4272         elif self
._signOk 
and self
._useParens 
and intStr
.find('(') != -1 and intStr
.find(')') == -1: 
4273             # ensure closing right paren: 
4276         if self
._fields
[0]._alignRight
:     ## Only if right-alignment is enabled 
4277             intStr 
= intStr
.rjust( lenInt 
) 
4279             intStr 
= intStr
.ljust( lenInt 
) 
4281         if candidate 
is None: 
4282             wx
.CallAfter(self
._SetValue
, intStr 
) 
4286     def _adjustDate(self
, candidate
=None, fixcentury
=False, force4digit_year
=False): 
4288         'Fixes' a date control, expanding the year if it can. 
4289         Applies various self-formatting options. 
4291 ##        dbg("MaskedEditMixin::_adjustDate", indent=1) 
4292         if candidate 
is None: text    
= self
._GetValue
() 
4293         else: text 
= candidate
 
4294 ##        dbg('text=', text) 
4295         if self
._datestyle 
== "YMD": 
4300 ##        dbg('getYear: "%s"' % _getYear(text, self._datestyle)) 
4301         year    
= string
.replace( _getYear( text
, self
._datestyle
),self
._fields
[year_field
]._fillChar
,"")  # drop extra fillChars 
4302         month   
= _getMonth( text
, self
._datestyle
) 
4303         day     
= _getDay( text
, self
._datestyle
) 
4304 ##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) 
4307         yearstart 
= self
._dateExtent 
- 4 
4311                  or (self
._GetInsertionPoint
() > yearstart
+1 and text
[yearstart
+2] == ' ') 
4312                  or (self
._GetInsertionPoint
() > yearstart
+2 and text
[yearstart
+3] == ' ') ) ): 
4313             ## user entered less than four digits and changing fields or past point where we could 
4314             ## enter another digit: 
4318 ##                dbg('bad year=', year) 
4319                 year 
= text
[yearstart
:self
._dateExtent
] 
4321         if len(year
) < 4 and yearVal
: 
4323                 # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the 
4325                 now 
= wx
.DateTime_Now() 
4326                 century 
= (now
.GetYear() /100) * 100        # "this century" 
4327                 twodig_year 
= now
.GetYear() - century       
# "this year" (2 digits) 
4328                 # if separation between today's 2-digit year and typed value > 50, 
4329                 #      assume last century, 
4330                 # else assume this century. 
4332                 # Eg: if 2003 and yearVal == 30, => 2030 
4333                 #     if 2055 and yearVal == 80, => 2080 
4334                 #     if 2010 and yearVal == 96, => 1996 
4336                 if abs(yearVal 
- twodig_year
) > 50: 
4337                     yearVal 
= (century 
- 100) + yearVal
 
4339                     yearVal 
= century 
+ yearVal
 
4340                 year 
= str( yearVal 
) 
4341             else:   # pad with 0's to make a 4-digit year 
4342                 year 
= "%04d" % yearVal
 
4343             if self
._4digityear 
or force4digit_year
: 
4344                 text 
= _makeDate(year
, month
, day
, self
._datestyle
, text
) + text
[self
._dateExtent
:] 
4345 ##        dbg('newdate: "%s"' % text, indent=0) 
4349     def _goEnd(self
, getPosOnly
=False): 
4350         """ Moves the insertion point to the end of user-entry """ 
4351 ##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) 
4352         text 
= self
._GetValue
() 
4353 ####        dbg('text: "%s"' % text) 
4355         if len(text
.rstrip()): 
4356             for i 
in range( min( self
._masklength
-1, len(text
.rstrip())), -1, -1): 
4357 ####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) 
4358                 if self
._isMaskChar
(i
): 
4360 ####                    dbg("text[%d]: '%s'" % (i, char)) 
4366             pos 
= self
._goHome
(getPosOnly
=True) 
4368             pos 
= min(i
,self
._masklength
) 
4370         field 
= self
._FindField
(pos
) 
4371         start
, end 
= field
._extent
 
4372         if field
._insertRight 
and pos 
< end
: 
4374 ##        dbg('next pos:', pos) 
4379             self
._SetInsertionPoint
(pos
) 
4382     def _goHome(self
, getPosOnly
=False): 
4383         """ Moves the insertion point to the beginning of user-entry """ 
4384 ##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) 
4385         text 
= self
._GetValue
() 
4386         for i 
in range(self
._masklength
): 
4387             if self
._isMaskChar
(i
): 
4394             self
._SetInsertionPoint
(max(i
,0)) 
4398     def _getAllowedChars(self
, pos
): 
4399         """ Returns a string of all allowed user input characters for the provided 
4400             mask character plus control options 
4402         maskChar 
= self
.maskdict
[pos
] 
4403         okchars 
= self
.maskchardict
[maskChar
]    ## entry, get mask approved characters 
4405         # convert okchars to unicode if required; will force subsequent appendings to 
4406         # result in unicode strings 
4407         if 'unicode' in wx
.PlatformInfo 
and type(okchars
) != types
.UnicodeType
: 
4408             okchars 
= okchars
.decode(self
._defaultEncoding
) 
4410         field 
= self
._FindField
(pos
) 
4411         if okchars 
and field
._okSpaces
:          ## Allow spaces? 
4413         if okchars 
and field
._includeChars
:      ## any additional included characters? 
4414             okchars 
+= field
._includeChars
 
4415 ####        dbg('okchars[%d]:' % pos, okchars) 
4419     def _isMaskChar(self
, pos
): 
4420         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) 
4422         if pos 
< self
._masklength
: 
4423             return self
.ismasked
[pos
] 
4428     def _isTemplateChar(self
,Pos
): 
4429         """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) 
4431         if Pos 
< self
._masklength
: 
4432             return not self
._isMaskChar
(Pos
) 
4437     def _isCharAllowed(self
, char
, pos
, checkRegex
=False, allowAutoSelect
=True, ignoreInsertRight
=False): 
4438         """ Returns True if character is allowed at the specific position, otherwise False.""" 
4439 ##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1) 
4440         field 
= self
._FindField
(pos
) 
4441         right_insert 
= False 
4443         if self
.controlInitialized
: 
4444             sel_start
, sel_to 
= self
._GetSelection
() 
4446             sel_start
, sel_to 
= pos
, pos
 
4448         if (field
._insertRight 
or self
._ctrl
_constraints
._insertRight
) and not ignoreInsertRight
: 
4449             start
, end 
= field
._extent
 
4450             field_len 
= end 
- start
 
4451             if self
.controlInitialized
: 
4452                 value 
= self
._GetValue
() 
4453                 fstr 
= value
[start
:end
].strip() 
4455                     while fstr 
and fstr
[0] == '0': 
4457                 input_len 
= len(fstr
) 
4458                 if self
._signOk 
and '-' in fstr 
or '(' in fstr
: 
4459                     input_len 
-= 1  # sign can move out of field, so don't consider it in length 
4461                 value 
= self
._template
 
4462                 input_len 
= 0   # can't get the current "value", so use 0 
4465             # if entire field is selected or position is at end and field is not full, 
4466             # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar 
4467             # or the field is a singleton integer field and is currently 0 and we're at the end: 
4468             if( (sel_start
, sel_to
) == field
._extent
 
4469                 or (pos 
== end 
and ((input_len 
< field_len
) 
4471                                          and input_len 
== field_len
 
4473                                          and value
[end
-1] == '0' 
4477 ##                dbg('pos = end - 1 = ', pos, 'right_insert? 1') 
4479             elif( field
._allowInsert 
and sel_start 
== sel_to
 
4480                   and (sel_to 
== end 
or (sel_to 
< self
._masklength 
and value
[sel_start
] != field
._fillChar
)) 
4481                   and input_len 
< field_len 
): 
4482                 pos 
= sel_to 
- 1    # where character will go 
4483 ##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') 
4485             # else leave pos alone... 
4487 ##                dbg('pos stays ', pos, 'right_insert? 0') 
4490         if self
._isTemplateChar
( pos 
):  ## if a template character, return empty 
4491 ##            dbg('%d is a template character; returning False' % pos, indent=0) 
4494         if self
._isMaskChar
( pos 
): 
4495             okChars  
= self
._getAllowedChars
(pos
) 
4497             if self
._fields
[0]._groupdigits 
and (self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
)): 
4498                 okChars 
+= self
._fields
[0]._groupChar
 
4501                 if self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
): 
4505                 elif self
._useParens 
and (self
._isInt 
or (self
._isFloat 
and pos 
> self
._decimalpos
)): 
4508 ####            dbg('%s in %s?' % (char, okChars), char in okChars) 
4509             approved 
= char 
in okChars
 
4511             if approved 
and checkRegex
: 
4512 ##                dbg("checking appropriate regex's") 
4513                 value 
= self
._eraseSelection
(self
._GetValue
()) 
4515                     # move the position to the right side of the insertion: 
4520                     newvalue
, ignore
, ignore
, ignore
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
, allowAutoSelect
=True) 
4522                     newvalue
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
) 
4523 ##                dbg('newvalue: "%s"' % newvalue) 
4525                 fields 
= [self
._FindField
(pos
)] + [self
._ctrl
_constraints
] 
4526                 for field 
in fields
:    # includes fields[-1] == "ctrl_constraints" 
4527                     if field
._regexMask 
and field
._filter
: 
4528 ##                        dbg('checking vs. regex') 
4529                         start
, end 
= field
._extent
 
4530                         slice = newvalue
[start
:end
] 
4531                         approved 
= (re
.match( field
._filter
, slice) is not None) 
4532 ##                        dbg('approved?', approved) 
4533                     if not approved
: break 
4537 ##            dbg('%d is a !???! character; returning False', indent=0) 
4541     def _applyFormatting(self
): 
4542         """ Apply formatting depending on the control's state. 
4543             Need to find a way to call this whenever the value changes, in case the control's 
4544             value has been changed or set programatically. 
4547 ##        dbg('MaskedEditMixin::_applyFormatting', indent=1) 
4549         # Handle negative numbers 
4551             text
, signpos
, right_signpos 
= self
._getSignedValue
() 
4552 ##            dbg('text: "%s", signpos:' % text, signpos) 
4553             if text 
and signpos 
!= self
._signpos
: 
4554                 self
._signpos 
= signpos
 
4555             if not text 
or text
[signpos
] not in ('-','('): 
4557 ##                dbg('no valid sign found; new sign:', self._isNeg) 
4558             elif text 
and self
._valid 
and not self
._isNeg 
and text
[signpos
] in ('-', '('): 
4559 ##                dbg('setting _isNeg to True') 
4561 ##            dbg('self._isNeg:', self._isNeg) 
4563         if self
._signOk 
and self
._isNeg
: 
4564             fc 
= self
._signedForegroundColour
 
4566             fc 
= self
._foregroundColour
 
4568         if hasattr(fc
, '_name'): 
4572 ##        dbg('setting foreground to', c) 
4573         self
.SetForegroundColour(fc
) 
4578                 bc 
= self
._emptyBackgroundColour
 
4580                 bc 
= self
._validBackgroundColour
 
4583             bc 
= self
._invalidBackgroundColour
 
4584         if hasattr(bc
, '_name'): 
4588 ##        dbg('setting background to', c) 
4589         self
.SetBackgroundColour(bc
) 
4591 ##        dbg(indent=0, suspend=0) 
4594     def _getAbsValue(self
, candidate
=None): 
4595         """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). 
4597 ##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) 
4598         if candidate 
is None: text 
= self
._GetValue
() 
4599         else: text 
= candidate
 
4600         right_signpos 
= text
.find(')') 
4603             if self
._ctrl
_constraints
._alignRight 
and self
._fields
[0]._fillChar 
== ' ': 
4604                 signpos 
= text
.find('-') 
4606 ##                    dbg('no - found; searching for (') 
4607                     signpos 
= text
.find('(') 
4609 ##                    dbg('- found at', signpos) 
4613 ##                    dbg('signpos still -1') 
4614 ##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) 
4615                     if len(text
) < self
._masklength
: 
4617                     if len(text
) < self
._masklength
: 
4619                     if len(text
) > self
._masklength 
and text
[-1] in (')', ' '): 
4622 ##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) 
4623 ##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) 
4624                         signpos 
= len(text
) - (len(text
.lstrip()) + 1) 
4626                         if self
._useParens 
and not text
.strip(): 
4627                             signpos 
-= 1    # empty value; use penultimate space 
4628 ##                dbg('signpos:', signpos) 
4630                     text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4635                     text 
= self
._template
[0] + text
[1:] 
4639             if right_signpos 
!= -1: 
4641                     text 
= text
[:right_signpos
] + ' ' + text
[right_signpos
+1:] 
4642                 elif len(text
) > self
._masklength
: 
4643                     text 
= text
[:right_signpos
] + text
[right_signpos
+1:] 
4647             elif self
._useParens 
and self
._signOk
: 
4648                 # figure out where it ought to go: 
4649                 right_signpos 
= self
._masklength 
- 1     # initial guess 
4650                 if not self
._ctrl
_constraints
._alignRight
: 
4651 ##                    dbg('not right-aligned') 
4652                     if len(text
.strip()) == 0: 
4653                         right_signpos 
= signpos 
+ 1 
4654                     elif len(text
.strip()) < self
._masklength
: 
4655                         right_signpos 
= len(text
.rstrip()) 
4656 ##                dbg('right_signpos:', right_signpos) 
4658             groupchar 
= self
._fields
[0]._groupChar
 
4660                 value 
= long(text
.replace(groupchar
,'').replace('(','-').replace(')','').replace(' ', '')) 
4662 ##                dbg('invalid number', indent=0) 
4663                 return None, signpos
, right_signpos
 
4667                 groupchar 
= self
._fields
[0]._groupChar
 
4668                 value 
= float(text
.replace(groupchar
,'').replace(self
._decimalChar
, '.').replace('(', '-').replace(')','').replace(' ', '')) 
4669 ##                dbg('value:', value) 
4673             if value 
< 0 and value 
is not None: 
4674                 signpos 
= text
.find('-') 
4676                     signpos 
= text
.find('(') 
4678                 text 
= text
[:signpos
] + self
._template
[signpos
] + text
[signpos
+1:] 
4680                 # look forwards up to the decimal point for the 1st non-digit 
4681 ##                dbg('decimal pos:', self._decimalpos) 
4682 ##                dbg('text: "%s"' % text) 
4684                     signpos 
= self
._decimalpos 
- (len(text
[:self
._decimalpos
].lstrip()) + 1) 
4685                     # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET 
4686                     if len(text
) >= signpos
+1 and  text
[signpos
+1] in ('-','('): 
4690 ##                dbg('signpos:', signpos) 
4694                     right_signpos 
= self
._masklength 
- 1 
4695                     text 
= text
[:right_signpos
] + ' ' 
4696                     if text
[signpos
] == '(': 
4697                         text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4699                     right_signpos 
= text
.find(')') 
4700                     if right_signpos 
!= -1: 
4705 ##                dbg('invalid number') 
4708 ##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) 
4710         return text
, signpos
, right_signpos
 
4713     def _getSignedValue(self
, candidate
=None): 
4714         """ Return a signed value by adding a "-" prefix if the value 
4715             is set to negative, or a space if positive. 
4717 ##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) 
4718         if candidate 
is None: text 
= self
._GetValue
() 
4719         else: text 
= candidate
 
4722         abstext
, signpos
, right_signpos 
= self
._getAbsValue
(text
) 
4726                 return abstext
, signpos
, right_signpos
 
4728             if self
._isNeg 
or text
[signpos
] in ('-', '('): 
4735             if abstext
[signpos
] not in string
.digits
: 
4736                 text 
= abstext
[:signpos
] + sign 
+ abstext
[signpos
+1:] 
4738                 # this can happen if value passed is too big; sign assumed to be 
4739                 # in position 0, but if already filled with a digit, prepend sign... 
4740                 text 
= sign 
+ abstext
 
4741             if self
._useParens 
and text
.find('(') != -1: 
4742                 text 
= text
[:right_signpos
] + ')' + text
[right_signpos
+1:] 
4745 ##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) 
4747         return text
, signpos
, right_signpos
 
4750     def GetPlainValue(self
, candidate
=None): 
4751         """ Returns control's value stripped of the template text. 
4752             plainvalue = MaskedEditMixin.GetPlainValue() 
4754 ##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) 
4756         if candidate 
is None: text 
= self
._GetValue
() 
4757         else: text 
= candidate
 
4760 ##            dbg('returned ""', indent=0) 
4764             for idx 
in range( min(len(self
._template
), len(text
)) ): 
4765                 if self
._mask
[idx
] in maskchars
: 
4768             if self
._isFloat 
or self
._isInt
: 
4769 ##                dbg('plain so far: "%s"' % plain) 
4770                 plain 
= plain
.replace('(', '-').replace(')', ' ') 
4771 ##                dbg('plain after sign regularization: "%s"' % plain) 
4773                 if self
._signOk 
and self
._isNeg 
and plain
.count('-') == 0: 
4774                     # must be in reserved position; add to "plain value" 
4775                     plain 
= '-' + plain
.strip() 
4777                 if self
._fields
[0]._alignRight
: 
4778                     lpad 
= plain
.count(',') 
4779                     plain 
= ' ' * lpad 
+ plain
.replace(',','') 
4781                     plain 
= plain
.replace(',','') 
4782 ##                dbg('plain after pad and group:"%s"' % plain) 
4784 ##            dbg('returned "%s"' % plain.rstrip(), indent=0) 
4785             return plain
.rstrip() 
4788     def IsEmpty(self
, value
=None): 
4790         Returns True if control is equal to an empty value. 
4791         (Empty means all editable positions in the template == fillChar.) 
4793         if value 
is None: value 
= self
._GetValue
() 
4794         if value 
== self
._template 
and not self
._defaultValue
: 
4795 ####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") 
4796             return True     # (all mask chars == fillChar by defn) 
4797         elif value 
== self
._template
: 
4799             for pos 
in range(len(self
._template
)): 
4800 ####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) 
4801 ####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) 
4802                 if self
._isMaskChar
(pos
) and value
[pos
] not in (' ', self
._fillChar
[pos
]): 
4804 ####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) 
4807 ####            dbg("IsEmpty? 0 (value doesn't match template)") 
4811     def IsDefault(self
, value
=None): 
4813         Returns True if the value specified (or the value of the control if not specified) 
4814         is equal to the default value. 
4816         if value 
is None: value 
= self
._GetValue
() 
4817         return value 
== self
._template
 
4820     def IsValid(self
, value
=None): 
4821         """ Indicates whether the value specified (or the current value of the control 
4822         if not specified) is considered valid.""" 
4823 ####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) 
4824         if value 
is None: value 
= self
._GetValue
() 
4825         ret 
= self
._CheckValid
(value
) 
4830     def _eraseSelection(self
, value
=None, sel_start
=None, sel_to
=None): 
4831         """ Used to blank the selection when inserting a new character. """ 
4832 ##        dbg("MaskedEditMixin::_eraseSelection", indent=1) 
4833         if value 
is None: value 
= self
._GetValue
() 
4834         if sel_start 
is None or sel_to 
is None: 
4835             sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
4836 ##        dbg('value: "%s"' % value) 
4837 ##        dbg("current sel_start, sel_to:", sel_start, sel_to) 
4839         newvalue 
= list(value
) 
4840         for i 
in range(sel_start
, sel_to
): 
4841             if self
._signOk 
and newvalue
[i
] in ('-', '(', ')'): 
4842 ##                dbg('found sign (%s) at' % newvalue[i], i) 
4844                 # balance parentheses: 
4845                 if newvalue
[i
] == '(': 
4846                     right_signpos 
= value
.find(')') 
4847                     if right_signpos 
!= -1: 
4848                         newvalue
[right_signpos
] = ' ' 
4850                 elif newvalue
[i
] == ')': 
4851                     left_signpos 
= value
.find('(') 
4852                     if left_signpos 
!= -1: 
4853                         newvalue
[left_signpos
] = ' ' 
4857             elif self
._isMaskChar
(i
): 
4858                 field 
= self
._FindField
(i
) 
4862                     newvalue
[i
] = self
._template
[i
] 
4864         value 
= string
.join(newvalue
,"") 
4865 ##        dbg('new value: "%s"' % value) 
4870     def _insertKey(self
, char
, pos
, sel_start
, sel_to
, value
, allowAutoSelect
=False): 
4871         """ Handles replacement of the character at the current insertion point.""" 
4872 ##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) 
4874         text 
= self
._eraseSelection
(value
) 
4875         field 
= self
._FindField
(pos
) 
4876         start
, end 
= field
._extent
 
4880         # if >= 2 chars selected in a right-insert field, do appropriate erase on field, 
4881         # then set selection to end, and do usual right insert. 
4882         if sel_start 
!= sel_to 
and sel_to 
>= sel_start
+2: 
4883             field 
= self
._FindField
(sel_start
) 
4884             if( field
._insertRight                          
# if right-insert 
4885                 and field
._allowInsert                      
# and allow insert at any point in field 
4886                 and field 
== self
._FindField
(sel_to
) ):     # and selection all in same field 
4887                 text 
= self
._OnErase
(just_return_value
=True)    # remove selection before insert 
4888 ##                dbg('text after (left)erase: "%s"' % text) 
4889                 pos 
= sel_start 
= sel_to
 
4891         if pos 
!= sel_start 
and sel_start 
== sel_to
: 
4892             # adjustpos must have moved the position; make selection match: 
4893             sel_start 
= sel_to 
= pos
 
4895 ##        dbg('field._insertRight?', field._insertRight) 
4896 ##        dbg('field._allowInsert?', field._allowInsert) 
4897 ##        dbg('sel_start, end', sel_start, end) 
4899 ##            dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar) 
4902         if( field
._insertRight                                  
# field allows right insert 
4903             and ((sel_start
, sel_to
) == field
._extent           
# and whole field selected 
4904                  or (sel_start 
== sel_to                        
# or nothing selected 
4905                      and (sel_start 
== end                      
# and cursor at right edge 
4906                           or (field
._allowInsert                
# or field allows right-insert 
4907                               and sel_start 
< end               
# next to other char in field: 
4908                               and text
[sel_start
] != field
._fillChar
) ) ) ) ): 
4909 ##            dbg('insertRight') 
4910             fstr 
= text
[start
:end
] 
4911             erasable_chars 
= [field
._fillChar
, ' '] 
4913             # if zero padding field, or a single digit, and currently a value of 0, allow erasure of 0: 
4914             if field
._padZero 
or (field
._isInt 
and (end 
- start 
== 1) and fstr
[0] == '0'): 
4915                 erasable_chars
.append('0') 
4918 ####            dbg("fstr[0]:'%s'" % fstr[0]) 
4919 ####            dbg('field_index:', field._index) 
4920 ####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) 
4921 ####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", self._signOk and field._index == 0 and fstr[0] in ('-','(')) 
4922             if fstr
[0] in erasable_chars 
or (self
._signOk 
and field
._index 
== 0 and fstr
[0] in ('-','(')): 
4924 ####                dbg('value:      "%s"' % text) 
4925 ####                dbg('fstr:       "%s"' % fstr) 
4926 ####                dbg("erased:     '%s'" % erased) 
4927                 field_sel_start 
= sel_start 
- start
 
4928                 field_sel_to 
= sel_to 
- start
 
4929 ##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start]) 
4930 ##                dbg('right fstr: "%s"' % fstr[field_sel_to:end]) 
4931                 fstr 
= fstr
[1:field_sel_start
] + char 
+ fstr
[field_sel_to
:end
] 
4932             if field
._alignRight 
and sel_start 
!= sel_to
: 
4933                 field_len 
= end 
- start
 
4934 ##                pos += (field_len - len(fstr))    # move cursor right by deleted amount 
4936 ##                dbg('setting pos to:', pos) 
4938                     fstr 
= '0' * (field_len 
- len(fstr
)) + fstr
 
4940                     fstr 
= fstr
.rjust(field_len
)   # adjust the field accordingly 
4941 ##            dbg('field str: "%s"' % fstr) 
4943             newtext 
= text
[:start
] + fstr 
+ text
[end
:] 
4944             if erased 
in ('-', '(') and self
._signOk
: 
4945                 newtext 
= erased 
+ newtext
[1:] 
4946 ##            dbg('newtext: "%s"' % newtext) 
4948             if self
._signOk 
and field
._index 
== 0: 
4949                 start 
-= 1             # account for sign position 
4951 ####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull) 
4952 ####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) 
4953             if( field
._moveOnFieldFull 
and pos 
== end
 
4954                 and len(fstr
.lstrip()) == end
-start
):   # if field now full 
4955                 newpos 
= self
._findNextEntry
(end
)       #   go to next field 
4957                 newpos 
= pos                            
# else keep cursor at current position 
4960 ##            dbg('not newtext') 
4962 ##                dbg('newpos:', newpos) 
4964             if self
._signOk 
and self
._useParens
: 
4965                 old_right_signpos 
= text
.find(')') 
4967             if field
._allowInsert 
and not field
._insertRight 
and sel_to 
<= end 
and sel_start 
>= start
: 
4968 ##                dbg('inserting within a left-insert-capable field') 
4969                 field_len 
= end 
- start
 
4970                 before 
= text
[start
:sel_start
] 
4971                 after 
= text
[sel_to
:end
].strip() 
4972 ####                dbg("current field:'%s'" % text[start:end]) 
4973 ####                dbg("before:'%s'" % before, "after:'%s'" % after) 
4974                 new_len 
= len(before
) + len(after
) + 1 # (for inserted char) 
4975 ####                dbg('new_len:', new_len) 
4977                 if new_len 
< field_len
: 
4978                     retained 
= after 
+ self
._template
[end
-(field_len
-new_len
):end
] 
4979                 elif new_len 
> end
-start
: 
4980                     retained 
= after
[1:] 
4984                 left 
= text
[0:start
] + before
 
4985 ####                dbg("left:'%s'" % left, "retained:'%s'" % retained) 
4986                 right   
= retained 
+ text
[end
:] 
4989                 right   
= text
[pos
+1:] 
4991             if 'unicode' in wx
.PlatformInfo 
and type(char
) != types
.UnicodeType
: 
4992                 # convert the keyboard constant to a unicode value, to 
4993                 # ensure it can be concatenated into the control value: 
4994                 char 
= char
.decode(self
._defaultEncoding
) 
4996             newtext 
= left 
+ char 
+ right
 
4997 ####            dbg('left:    "%s"' % left) 
4998 ####            dbg('right:   "%s"' % right) 
4999 ####            dbg('newtext: "%s"' % newtext) 
5001             if self
._signOk 
and self
._useParens
: 
5002                 # Balance parentheses: 
5003                 left_signpos 
= newtext
.find('(') 
5005                 if left_signpos 
== -1:     # erased '('; remove ')' 
5006                     right_signpos 
= newtext
.find(')') 
5007                     if right_signpos 
!= -1: 
5008                         newtext 
= newtext
[:right_signpos
] + ' ' + newtext
[right_signpos
+1:] 
5010                 elif old_right_signpos 
!= -1: 
5011                     right_signpos 
= newtext
.find(')') 
5013                     if right_signpos 
== -1: # just replaced right-paren 
5014                         if newtext
[pos
] == ' ': # we just erased '); erase '(' 
5015                             newtext 
= newtext
[:left_signpos
] + ' ' + newtext
[left_signpos
+1:] 
5016                         else:   # replaced with digit; move ') over 
5017                             if self
._ctrl
_constraints
._alignRight 
or self
._isFloat
: 
5018                                 newtext 
= newtext
[:-1] + ')' 
5020                                 rstripped_text 
= newtext
.rstrip() 
5021                                 right_signpos 
= len(rstripped_text
) 
5022 ##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) 
5023                                 newtext 
= newtext
[:right_signpos
] + ')' + newtext
[right_signpos
+1:] 
5025             if( field
._insertRight                                  
# if insert-right field (but we didn't start at right edge) 
5026                 and field
._moveOnFieldFull                          
# and should move cursor when full 
5027                 and len(newtext
[start
:end
].strip()) == end
-start
):  # and field now full 
5028                 newpos 
= self
._findNextEntry
(end
)                   #   go to next field 
5029 ##                dbg('newpos = nextentry =', newpos) 
5031 ##                dbg('pos:', pos, 'newpos:', pos+1) 
5036             new_select_to 
= newpos     
# (default return values) 
5040             if field
._autoSelect
: 
5041                 match_index
, partial_match 
= self
._autoComplete
(1,  # (always forward) 
5042                                                                 field
._compareChoices
, 
5044                                                                 compareNoCase
=field
._compareNoCase
, 
5045                                                                 current_index 
= field
._autoCompleteIndex
-1) 
5046                 if match_index 
is not None and partial_match
: 
5047                     matched_str 
= newtext
[start
:end
] 
5048                     newtext 
= newtext
[:start
] + field
._choices
[match_index
] + newtext
[end
:] 
5051                     if field
._insertRight
: 
5052                         # adjust position to just after partial match in field 
5053                         newpos 
= end 
- (len(field
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5055             elif self
._ctrl
_constraints
._autoSelect
: 
5056                 match_index
, partial_match 
= self
._autoComplete
( 
5057                                         1,  # (always forward) 
5058                                         self
._ctrl
_constraints
._compareChoices
, 
5060                                         self
._ctrl
_constraints
._compareNoCase
, 
5061                                         current_index 
= self
._ctrl
_constraints
._autoCompleteIndex 
- 1) 
5062                 if match_index 
is not None and partial_match
: 
5063                     matched_str 
= newtext
 
5064                     newtext 
= self
._ctrl
_constraints
._choices
[match_index
] 
5065                     edit_end 
= self
._ctrl
_constraints
._extent
[1] 
5066                     new_select_to 
= min(edit_end
, len(newtext
.rstrip())) 
5067                     match_field 
= self
._ctrl
_constraints
 
5068                     if self
._ctrl
_constraints
._insertRight
: 
5069                         # adjust position to just after partial match in control: 
5070                         newpos 
= self
._masklength 
- (len(self
._ctrl
_constraints
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5072 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) 
5074             return newtext
, newpos
, new_select_to
, match_field
, match_index
 
5076 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos) 
5078             return newtext
, newpos
 
5081     def _OnFocus(self
,event
): 
5083         This event handler is currently necessary to work around new default 
5084         behavior as of wxPython2.3.3; 
5085         The TAB key auto selects the entire contents of the wx.TextCtrl *after* 
5086         the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection 
5087         *here*, because it hasn't happened yet.  So to prevent this behavior, and 
5088         preserve the correct selection when the focus event is not due to tab, 
5089         we need to pull the following trick: 
5091 ##        dbg('MaskedEditMixin::_OnFocus') 
5092         if self
.IsBeingDeleted() or self
.GetParent().IsBeingDeleted(): 
5094         wx
.CallAfter(self
._fixSelection
) 
5099     def _CheckValid(self
, candidate
=None): 
5101         This is the default validation checking routine; It verifies that the 
5102         current value of the control is a "valid value," and has the side 
5103         effect of coloring the control appropriately. 
5106 ##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) 
5107         oldValid 
= self
._valid
 
5108         if candidate 
is None: value 
= self
._GetValue
() 
5109         else: value 
= candidate
 
5110 ##        dbg('value: "%s"' % value) 
5112         valid 
= True    # assume True 
5114         if not self
.IsDefault(value
) and self
._isDate
:                    ## Date type validation 
5115             valid 
= self
._validateDate
(value
) 
5116 ##            dbg("valid date?", valid) 
5118         elif not self
.IsDefault(value
) and self
._isTime
: 
5119             valid 
= self
._validateTime
(value
) 
5120 ##            dbg("valid time?", valid) 
5122         elif not self
.IsDefault(value
) and (self
._isInt 
or self
._isFloat
):  ## Numeric type 
5123             valid 
= self
._validateNumeric
(value
) 
5124 ##            dbg("valid Number?", valid) 
5126         if valid
:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault() 
5127             ## valid so far; ensure also allowed by any list or regex provided: 
5128             valid 
= self
._validateGeneric
(value
) 
5129 ##            dbg("valid value?", valid) 
5131 ##        dbg('valid?', valid) 
5135             self
._applyFormatting
() 
5136             if self
._valid 
!= oldValid
: 
5137 ##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) 
5138 ##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) 
5140 ##        dbg(indent=0, suspend=0) 
5144     def _validateGeneric(self
, candidate
=None): 
5145         """ Validate the current value using the provided list or Regex filter (if any). 
5147         if candidate 
is None: 
5148             text 
= self
._GetValue
() 
5152         valid 
= True    # assume True 
5153         for i 
in [-1] + self
._field
_indices
:   # process global constraints first: 
5154             field 
= self
._fields
[i
] 
5155             start
, end 
= field
._extent
 
5156             slice = text
[start
:end
] 
5157             valid 
= field
.IsValid(slice) 
5164     def _validateNumeric(self
, candidate
=None): 
5165         """ Validate that the value is within the specified range (if specified.)""" 
5166         if candidate 
is None: value 
= self
._GetValue
() 
5167         else: value 
= candidate
 
5169             groupchar 
= self
._fields
[0]._groupChar
 
5171                 number 
= float(value
.replace(groupchar
, '').replace(self
._decimalChar
, '.').replace('(', '-').replace(')', '')) 
5173                 number 
= long( value
.replace(groupchar
, '').replace('(', '-').replace(')', '')) 
5175                     if self
._fields
[0]._alignRight
: 
5176                         require_digit_at 
= self
._fields
[0]._extent
[1]-1 
5178                         require_digit_at 
= self
._fields
[0]._extent
[0] 
5179 ##                    dbg('require_digit_at:', require_digit_at) 
5180 ##                    dbg("value[rda]: '%s'" % value[require_digit_at]) 
5181                     if value
[require_digit_at
] not in list(string
.digits
): 
5185 ##            dbg('number:', number) 
5186             if self
._ctrl
_constraints
._hasRange
: 
5187                 valid 
= self
._ctrl
_constraints
._rangeLow 
<= number 
<= self
._ctrl
_constraints
._rangeHigh
 
5190             groupcharpos 
= value
.rfind(groupchar
) 
5191             if groupcharpos 
!= -1:  # group char present 
5192 ##                dbg('groupchar found at', groupcharpos) 
5193                 if self
._isFloat 
and groupcharpos 
> self
._decimalpos
: 
5194                     # 1st one found on right-hand side is past decimal point 
5195 ##                    dbg('groupchar in fraction; illegal') 
5198                     integer 
= value
[:self
._decimalpos
].strip() 
5200                     integer 
= value
.strip() 
5201 ##                dbg("integer:'%s'" % integer) 
5202                 if integer
[0] in ('-', '('): 
5203                     integer 
= integer
[1:] 
5204                 if integer
[-1] == ')': 
5205                     integer 
= integer
[:-1] 
5207                 parts 
= integer
.split(groupchar
) 
5208 ##                dbg('parts:', parts) 
5209                 for i 
in range(len(parts
)): 
5210                     if i 
== 0 and abs(int(parts
[0])) > 999: 
5211 ##                        dbg('group 0 too long; illegal') 
5214                     elif i 
> 0 and (len(parts
[i
]) != 3 or ' ' in parts
[i
]): 
5215 ##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i])) 
5219 ##            dbg('value not a valid number') 
5224     def _validateDate(self
, candidate
=None): 
5225         """ Validate the current date value using the provided Regex filter. 
5226             Generally used for character types.BufferType 
5228 ##        dbg('MaskedEditMixin::_validateDate', indent=1) 
5229         if candidate 
is None: value 
= self
._GetValue
() 
5230         else: value 
= candidate
 
5231 ##        dbg('value = "%s"' % value) 
5232         text 
= self
._adjustDate
(value
, force4digit_year
=True)     ## Fix the date up before validating it 
5233 ##        dbg('text =', text) 
5234         valid 
= True   # assume True until proven otherwise 
5237             # replace fillChar in each field with space: 
5238             datestr 
= text
[0:self
._dateExtent
] 
5240                 field 
= self
._fields
[i
] 
5241                 start
, end 
= field
._extent
 
5242                 fstr 
= datestr
[start
:end
] 
5243                 fstr
.replace(field
._fillChar
, ' ') 
5244                 datestr 
= datestr
[:start
] + fstr 
+ datestr
[end
:] 
5246             year
, month
, day 
= _getDateParts( datestr
, self
._datestyle
) 
5248 ##            dbg('self._dateExtent:', self._dateExtent) 
5249             if self
._dateExtent 
== 11: 
5250                 month 
= charmonths_dict
[month
.lower()] 
5254 ##            dbg('year, month, day:', year, month, day) 
5257 ##            dbg('cannot convert string to integer parts') 
5260 ##            dbg('cannot convert string to integer month') 
5264             # use wxDateTime to unambiguously try to parse the date: 
5265             # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, 
5266             # rather than 1-12, so handle accordingly: 
5272 ##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) 
5273                     dateHandler 
= wx
.DateTimeFromDMY(day
,month
,year
) 
5277 ##                    dbg('cannot convert string to valid date') 
5283                 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5284                 # so we eliminate them here: 
5285                 timeStr     
= text
[self
._dateExtent
+1:].strip()         ## time portion of the string 
5287 ##                    dbg('timeStr: "%s"' % timeStr) 
5289                         checkTime    
= dateHandler
.ParseTime(timeStr
) 
5290                         valid 
= checkTime 
== len(timeStr
) 
5294 ##                        dbg('cannot convert string to valid time') 
5296 ##        if valid: dbg('valid date') 
5301     def _validateTime(self
, candidate
=None): 
5302         """ Validate the current time value using the provided Regex filter. 
5303             Generally used for character types.BufferType 
5305 ##        dbg('MaskedEditMixin::_validateTime', indent=1) 
5306         # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5307         # so we eliminate them here: 
5308         if candidate 
is None: value 
= self
._GetValue
().strip() 
5309         else: value 
= candidate
.strip() 
5310 ##        dbg('value = "%s"' % value) 
5311         valid 
= True   # assume True until proven otherwise 
5313         dateHandler 
= wx
.DateTime_Today() 
5315             checkTime    
= dateHandler
.ParseTime(value
) 
5316 ##            dbg('checkTime:', checkTime, 'len(value)', len(value)) 
5317             valid 
= checkTime 
== len(value
) 
5322 ##            dbg('cannot convert string to valid time') 
5324 ##        if valid: dbg('valid time') 
5329     def _OnKillFocus(self
,event
): 
5330         """ Handler for EVT_KILL_FOCUS event. 
5332 ##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) 
5333         if self
.IsBeingDeleted() or self
.GetParent().IsBeingDeleted(): 
5335         if self
._mask 
and self
._IsEditable
(): 
5336             self
._AdjustField
(self
._GetInsertionPoint
()) 
5337             self
._CheckValid
()   ## Call valid handler 
5339         self
._LostFocus
()    ## Provided for subclass use 
5344     def _fixSelection(self
): 
5346         This gets called after the TAB traversal selection is made, if the 
5347         focus event was due to this, but before the EVT_LEFT_* events if 
5348         the focus shift was due to a mouse event. 
5350         The trouble is that, a priori, there's no explicit notification of 
5351         why the focus event we received.  However, the whole reason we need to 
5352         do this is because the default behavior on TAB traveral in a wx.TextCtrl is 
5353         now to select the entire contents of the window, something we don't want. 
5354         So we can *now* test the selection range, and if it's "the whole text" 
5355         we can assume the cause, change the insertion point to the start of 
5356         the control, and deselect. 
5358 ##        dbg('MaskedEditMixin::_fixSelection', indent=1) 
5359         # can get here if called with wx.CallAfter after underlying  
5360         # control has been destroyed on close, but after focus 
5362         if not self 
or not self
._mask 
or not self
._IsEditable
(): 
5366         sel_start
, sel_to 
= self
._GetSelection
() 
5367 ##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) 
5369         if( sel_start 
== 0 and sel_to 
>= len( self
._mask 
)   #(can be greater in numeric controls because of reserved space) 
5370             and (not self
._ctrl
_constraints
._autoSelect 
or self
.IsEmpty() or self
.IsDefault() ) ): 
5371             # This isn't normally allowed, and so assume we got here by the new 
5372             # "tab traversal" behavior, so we need to reset the selection 
5373             # and insertion point: 
5374 ##            dbg('entire text selected; resetting selection to start of control') 
5376             field 
= self
._FindField
(self
._GetInsertionPoint
()) 
5377             edit_start
, edit_end 
= field
._extent
 
5378             if field
._selectOnFieldEntry
: 
5379                 if self
._isFloat 
or self
._isInt 
and field 
== self
._fields
[0]: 
5381                 self
._SetInsertionPoint
(edit_start
) 
5382                 self
._SetSelection
(edit_start
, edit_end
) 
5384             elif field
._insertRight
: 
5385                 self
._SetInsertionPoint
(edit_end
) 
5386                 self
._SetSelection
(edit_end
, edit_end
) 
5388         elif (self
._isFloat 
or self
._isInt
): 
5390             text
, signpos
, right_signpos 
= self
._getAbsValue
() 
5391             if text 
is None or text 
== self
._template
: 
5392                 integer 
= self
._fields
[0] 
5393                 edit_start
, edit_end 
= integer
._extent
 
5395                 if integer
._selectOnFieldEntry
: 
5396 ##                    dbg('select on field entry:') 
5397                     self
._SetInsertionPoint
(0) 
5398                     self
._SetSelection
(0, edit_end
) 
5400                 elif integer
._insertRight
: 
5401 ##                    dbg('moving insertion point to end') 
5402                     self
._SetInsertionPoint
(edit_end
) 
5403                     self
._SetSelection
(edit_end
, edit_end
) 
5405 ##                    dbg('numeric ctrl is empty; start at beginning after sign') 
5406                     self
._SetInsertionPoint
(signpos
+1)   ## Move past minus sign space if signed 
5407                     self
._SetSelection
(signpos
+1, signpos
+1) 
5409         elif sel_start 
> self
._goEnd
(getPosOnly
=True): 
5410 ##            dbg('cursor beyond the end of the user input; go to end of it') 
5413 ##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) 
5418     def _Keypress(self
,key
): 
5419         """ Method provided to override OnChar routine. Return False to force 
5420             a skip of the 'normal' OnChar process. Called before class OnChar. 
5425     def _LostFocus(self
): 
5426         """ Method provided for subclasses. _LostFocus() is called after 
5427             the class processes its EVT_KILL_FOCUS event code. 
5432     def _OnDoubleClick(self
, event
): 
5433         """ selects field under cursor on dclick.""" 
5434         pos 
= self
._GetInsertionPoint
() 
5435         field 
= self
._FindField
(pos
) 
5436         start
, end 
= field
._extent
 
5437         self
._SetInsertionPoint
(start
) 
5438         self
._SetSelection
(start
, end
) 
5442         """ Method provided for subclasses. Called by internal EVT_TEXT 
5443             handler. Return False to override the class handler, True otherwise. 
5450         Used to override the default Cut() method in base controls, instead 
5451         copying the selection to the clipboard and then blanking the selection, 
5452         leaving only the mask in the selected area behind. 
5453         Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the 
5454         derived control because the mixin functions can't override a method of 
5457 ##        dbg("MaskedEditMixin::_Cut", indent=1) 
5458         value 
= self
._GetValue
() 
5459 ##        dbg('current value: "%s"' % value) 
5460         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
5461 ##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) 
5462         do 
= wx
.TextDataObject() 
5463         do
.SetText(value
[sel_start
:sel_to
].strip()) 
5464         wx
.TheClipboard
.Open() 
5465         wx
.TheClipboard
.SetData(do
) 
5466         wx
.TheClipboard
.Close() 
5468         if sel_to 
- sel_start 
!= 0: 
5473 # WS Note: overriding Copy is no longer necessary given that you 
5474 # can no longer select beyond the last non-empty char in the control. 
5476 ##    def _Copy( self ): 
5478 ##        Override the wx.TextCtrl's .Copy function, with our own 
5479 ##        that does validation.  Need to strip trailing spaces. 
5481 ##        sel_start, sel_to = self._GetSelection() 
5482 ##        select_len = sel_to - sel_start 
5483 ##        textval = wx.TextCtrl._GetValue(self) 
5485 ##        do = wx.TextDataObject() 
5486 ##        do.SetText(textval[sel_start:sel_to].strip()) 
5487 ##        wx.TheClipboard.Open() 
5488 ##        wx.TheClipboard.SetData(do) 
5489 ##        wx.TheClipboard.Close() 
5492     def _getClipboardContents( self 
): 
5493         """ Subroutine for getting the current contents of the clipboard. 
5495         do 
= wx
.TextDataObject() 
5496         wx
.TheClipboard
.Open() 
5497         success 
= wx
.TheClipboard
.GetData(do
) 
5498         wx
.TheClipboard
.Close() 
5503             # Remove leading and trailing spaces before evaluating contents 
5504             return do
.GetText().strip() 
5507     def _validatePaste(self
, paste_text
, sel_start
, sel_to
, raise_on_invalid
=False): 
5509         Used by paste routine and field choice validation to see 
5510         if a given slice of paste text is legal for the area in question: 
5511         returns validity, replacement text, and extent of paste in 
5515 ##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) 
5516         select_length 
= sel_to 
- sel_start
 
5517         maxlength 
= select_length
 
5518 ##        dbg('sel_to - sel_start:', maxlength) 
5520             maxlength 
= self
._masklength 
- sel_start
 
5524 ##        dbg('maxlength:', maxlength) 
5525         if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5526             paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5528         length_considered 
= len(paste_text
) 
5529         if length_considered 
> maxlength
: 
5530 ##            dbg('paste text will not fit into the %s:' % item, indent=0) 
5531             if raise_on_invalid
: 
5532 ##                dbg(indent=0, suspend=0) 
5533                 if item 
== 'control': 
5534                     raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5536                     raise ValueError('"%s" will not fit into the selection' % paste_text
) 
5538 ##                dbg(indent=0, suspend=0) 
5539                 return False, None, None 
5541         text 
= self
._template
 
5542 ##        dbg('length_considered:', length_considered) 
5545         replacement_text 
= "" 
5546         replace_to 
= sel_start
 
5548         while valid_paste 
and i 
< length_considered 
and replace_to 
< self
._masklength
: 
5549             if paste_text
[i
:] == self
._template
[replace_to
:length_considered
]: 
5550                 # remainder of paste matches template; skip char-by-char analysis 
5551 ##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) 
5552                 replacement_text 
+= paste_text
[i
:] 
5553                 replace_to 
= i 
= length_considered
 
5556             char 
= paste_text
[i
] 
5557             field 
= self
._FindField
(replace_to
) 
5558             if not field
._compareNoCase
: 
5559                 if field
._forceupper
:   char 
= char
.upper() 
5560                 elif field
._forcelower
: char 
= char
.lower() 
5562 ##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) 
5563 ##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) 
5564             if not self
._isTemplateChar
(replace_to
) and self
._isCharAllowed
( char
, replace_to
, allowAutoSelect
=False, ignoreInsertRight
=True): 
5565                 replacement_text 
+= char
 
5566 ##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) 
5567 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5570             elif( char 
== self
._template
[replace_to
] 
5571                   or (self
._signOk 
and 
5572                           ( (i 
== 0 and (char 
== '-' or (self
._useParens 
and char 
== '('))) 
5573                             or (i 
== self
._masklength 
- 1 and self
._useParens 
and char 
== ')') ) ) ): 
5574                 replacement_text 
+= char
 
5575 ##                dbg("'%(char)s' == template(%(replace_to)d)" % locals()) 
5576 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5580                 next_entry 
= self
._findNextEntry
(replace_to
, adjustInsert
=False) 
5581                 if next_entry 
== replace_to
: 
5584                     replacement_text 
+= self
._template
[replace_to
:next_entry
] 
5585 ##                    dbg("skipping template; next_entry =", next_entry) 
5586 ##                    dbg("replacement_text:", '"'+replacement_text+'"') 
5587                     replace_to 
= next_entry  
# so next_entry will be considered on next loop 
5589         if not valid_paste 
and raise_on_invalid
: 
5590 ##            dbg('raising exception', indent=0, suspend=0) 
5591             raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text
, self
.name
)) 
5593         elif i 
< len(paste_text
): 
5595             if raise_on_invalid
: 
5596 ##                dbg('raising exception', indent=0, suspend=0) 
5597                 raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5599 ##        dbg('valid_paste?', valid_paste) 
5601 ##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) 
5603 ##        dbg(indent=0, suspend=0) 
5604         return valid_paste
, replacement_text
, replace_to
 
5607     def _Paste( self
, value
=None, raise_on_invalid
=False, just_return_value
=False ): 
5609         Used to override the base control's .Paste() function, 
5610         with our own that does validation. 
5611         Note: _Paste must be called from a Paste() override in the 
5612         derived control because the mixin functions can't override a 
5613         method of a sibling class. 
5615 ##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) 
5617             paste_text 
= self
._getClipboardContents
() 
5621         if paste_text 
is not None: 
5623             if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5624                 paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5626 ##            dbg('paste text: "%s"' % paste_text) 
5627             # (conversion will raise ValueError if paste isn't legal) 
5628             sel_start
, sel_to 
= self
._GetSelection
() 
5629 ##            dbg('selection:', (sel_start, sel_to)) 
5631             # special case: handle allowInsert fields properly 
5632             field 
= self
._FindField
(sel_start
) 
5633             edit_start
, edit_end 
= field
._extent
 
5635             if field
._allowInsert 
and sel_to 
<= edit_end 
and (sel_start 
+ len(paste_text
) < edit_end 
or field
._insertRight
): 
5636                 if field
._insertRight
: 
5637                     # want to paste to the left; see if it will fit: 
5638                     left_text 
= self
._GetValue
()[edit_start
:sel_start
].lstrip() 
5639 ##                    dbg('len(left_text):', len(left_text)) 
5640 ##                    dbg('len(paste_text):', len(paste_text)) 
5641 ##                    dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
5642                     if sel_start 
- (len(left_text
) - (sel_to 
- sel_start
) + len(paste_text
)) >= edit_start
: 
5643                         # will fit! create effective paste text, and move cursor back to do so: 
5644                         paste_text 
= left_text 
+ paste_text
 
5645                         sel_start 
-= len(left_text
) 
5646                         paste_text 
= paste_text
.rjust(sel_to 
- sel_start
) 
5647 ##                        dbg('modified paste_text to be: "%s"' % paste_text) 
5648 ##                        dbg('modified selection to:', (sel_start, sel_to)) 
5650 ##                        dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
5653                     paste_text 
= paste_text 
+ self
._GetValue
()[sel_to
:edit_end
].rstrip() 
5654 ##                    dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text) 
5657                 new_pos 
= sel_start 
+ len(paste_text
)   # store for subsequent positioning 
5658 ##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) 
5659 ##                dbg('expanded selection to:', (sel_start, sel_to)) 
5661             # Another special case: paste won't fit, but it's a right-insert field where entire 
5662             # non-empty value is selected, and there's room if the selection is expanded leftward: 
5663             if( len(paste_text
) > sel_to 
- sel_start
 
5664                 and field
._insertRight
 
5665                 and sel_start 
> edit_start
 
5666                 and sel_to 
>= edit_end
 
5667                 and not self
._GetValue
()[edit_start
:sel_start
].strip() ): 
5668                 # text won't fit within selection, but left of selection is empty; 
5669                 # check to see if we can expand selection to accommodate the value: 
5670                 empty_space 
= sel_start 
- edit_start
 
5671                 amount_needed 
= len(paste_text
) - (sel_to 
- sel_start
) 
5672                 if amount_needed 
<= empty_space
: 
5673                     sel_start 
-= amount_needed
 
5674 ##                    dbg('expanded selection to:', (sel_start, sel_to)) 
5677             # another special case: deal with signed values properly: 
5679                 signedvalue
, signpos
, right_signpos 
= self
._getSignedValue
() 
5680                 paste_signpos 
= paste_text
.find('-') 
5681                 if paste_signpos 
== -1: 
5682                     paste_signpos 
= paste_text
.find('(') 
5684                 # if paste text will result in signed value: 
5685 ####                dbg('paste_signpos != -1?', paste_signpos != -1) 
5686 ####                dbg('sel_start:', sel_start, 'signpos:', signpos) 
5687 ####                dbg('field._insertRight?', field._insertRight) 
5688 ####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) 
5689                 if paste_signpos 
!= -1 and (sel_start 
<= signpos
 
5690                                             or (field
._insertRight 
and sel_start 
- len(paste_text
) <= signpos
)): 
5694                 # remove "sign" from paste text, so we can auto-adjust for sign type after paste: 
5695                 paste_text 
= paste_text
.replace('-', ' ').replace('(',' ').replace(')','') 
5696 ##                dbg('unsigned paste text: "%s"' % paste_text) 
5700             # another special case: deal with insert-right fields when selection is empty and 
5701             # cursor is at end of field: 
5702 ####            dbg('field._insertRight?', field._insertRight) 
5703 ####            dbg('sel_start == edit_end?', sel_start == edit_end) 
5704 ####            dbg('sel_start', sel_start, 'sel_to', sel_to) 
5705             if field
._insertRight 
and sel_start 
== edit_end 
and sel_start 
== sel_to
: 
5706                 sel_start 
-= len(paste_text
) 
5709 ##                dbg('adjusted selection:', (sel_start, sel_to)) 
5712                 valid_paste
, replacement_text
, replace_to 
= self
._validatePaste
(paste_text
, sel_start
, sel_to
, raise_on_invalid
) 
5714 ##                dbg('exception thrown', indent=0) 
5718 ##                dbg('paste text not legal for the selection or portion of the control following the cursor;') 
5719                 if not wx
.Validator_IsSilent(): 
5724             text 
= self
._eraseSelection
() 
5726             new_text 
= text
[:sel_start
] + replacement_text 
+ text
[replace_to
:] 
5728                 new_text 
= string
.ljust(new_text
,self
._masklength
) 
5730                 new_text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=new_text
) 
5733                         new_text 
= new_text
[:signpos
] + '(' + new_text
[signpos
+1:right_signpos
] + ')' + new_text
[right_signpos
+1:] 
5735                         new_text 
= new_text
[:signpos
] + '-' + new_text
[signpos
+1:] 
5739 ##            dbg("new_text:", '"'+new_text+'"') 
5741             if not just_return_value
: 
5742                 if new_text 
!= self
._GetValue
(): 
5743                     self
.modified 
= True 
5747                     wx
.CallAfter(self
._SetValue
, new_text
) 
5749                         new_pos 
= sel_start 
+ len(replacement_text
) 
5750                     wx
.CallAfter(self
._SetInsertionPoint
, new_pos
) 
5753                 return new_text
, replace_to
 
5754         elif just_return_value
: 
5756             return self
._GetValue
(), sel_to
 
5759     def _Undo(self
, value
=None, prev
=None, just_return_results
=False): 
5760         """ Provides an Undo() method in base controls. """ 
5761 ##        dbg("MaskedEditMixin::_Undo", indent=1) 
5763             value 
= self
._GetValue
() 
5765             prev 
= self
._prevValue
 
5766 ##        dbg('current value:  "%s"' % value) 
5767 ##        dbg('previous value: "%s"' % prev) 
5769 ##            dbg('no previous value', indent=0) 
5773             # Determine what to select: (relies on fixed-length strings) 
5774             # (This is a lot harder than it would first appear, because 
5775             # of mask chars that stay fixed, and so break up the "diff"...) 
5777             # Determine where they start to differ: 
5779             length 
= len(value
)     # (both are same length in masked control) 
5781             while( value
[:i
] == prev
[:i
] ): 
5786             # handle signed values carefully, so undo from signed to unsigned or vice-versa 
5789                 text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=prev
) 
5791                     if prev
[signpos
] == '(' and prev
[right_signpos
] == ')': 
5795                     # eliminate source of "far-end" undo difference if using balanced parens: 
5796                     value 
= value
.replace(')', ' ') 
5797                     prev 
= prev
.replace(')', ' ') 
5798                 elif prev
[signpos
] == '-': 
5803             # Determine where they stop differing in "undo" result: 
5804             sm 
= difflib
.SequenceMatcher(None, a
=value
, b
=prev
) 
5805             i
, j
, k 
= sm
.find_longest_match(sel_start
, length
, sel_start
, length
) 
5806 ##            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] ) 
5808             if k 
== 0:                              # no match found; select to end 
5811                 code_5tuples 
= sm
.get_opcodes() 
5812                 for op
, i1
, i2
, j1
, j2 
in code_5tuples
: 
5813 ##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) 
5817                 # look backward through operations needed to produce "previous" value; 
5818                 # first change wins: 
5819                 for next_op 
in range(len(code_5tuples
)-1, -1, -1): 
5820                     op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5821 ##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) 
5822                     field 
= self
._FindField
(i2
) 
5823                     if op 
== 'insert' and prev
[j1
:j2
] != self
._template
[j1
:j2
]: 
5824 ##                        dbg('insert found: selection =>', (j1, j2)) 
5829                     elif op 
== 'delete' and value
[i1
:i2
] != self
._template
[i1
:i2
]: 
5830                         edit_start
, edit_end 
= field
._extent
 
5831                         if field
._insertRight 
and (field
._allowInsert 
or i2 
== edit_end
): 
5837 ##                        dbg('delete found: selection =>', (sel_start, sel_to)) 
5840                     elif op 
== 'replace': 
5841                         if not prev
[i1
:i2
].strip() and field
._insertRight
: 
5842                             sel_start 
= sel_to 
= j2
 
5846 ##                        dbg('replace found: selection =>', (sel_start, sel_to)) 
5852                     # now go forwards, looking for earlier changes: 
5853 ##                    dbg('searching forward...') 
5854                     for next_op 
in range(len(code_5tuples
)): 
5855                         op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5856                         field 
= self
._FindField
(i1
) 
5859                         elif op 
== 'replace': 
5860                             if field
._insertRight
: 
5861                                 # if replace with spaces in an insert-right control, ignore "forward" replace 
5862                                 if not prev
[i1
:i2
].strip(): 
5865 ##                                    dbg('setting sel_start to', j1) 
5868 ##                                    dbg('setting sel_start to', i1) 
5871 ##                                dbg('setting sel_start to', i1) 
5873 ##                            dbg('saw replace; breaking') 
5875                         elif op 
== 'insert' and not value
[i1
:i2
]: 
5876 ##                            dbg('forward %s found' % op) 
5877                             if prev
[j1
:j2
].strip(): 
5878 ##                                dbg('item to insert non-empty; setting sel_start to', j1) 
5881                             elif not field
._insertRight
: 
5882 ##                                dbg('setting sel_start to inserted space:', j1) 
5885                         elif op 
== 'delete': 
5886 ##                            dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip())) 
5887                             if field
._insertRight
: 
5888                                 if value
[i1
:i2
].lstrip(): 
5889 ##                                    dbg('setting sel_start to ', j1) 
5891 ##                                    dbg('breaking loop') 
5896 ##                                dbg('saw delete; breaking') 
5899 ##                            dbg('unknown code!') 
5900                             # we've got what we need 
5905 ##                    dbg('no insert,delete or replace found (!)') 
5906                     # do "left-insert"-centric processing of difference based on l.c.s.: 
5907                     if i 
== j 
and j 
!= sel_start
:         # match starts after start of selection 
5908                         sel_to 
= sel_start 
+ (j
-sel_start
)  # select to start of match 
5910                         sel_to 
= j                          
# (change ends at j) 
5913             # There are several situations where the calculated difference is 
5914             # not what we want to select.  If changing sign, or just adding 
5915             # group characters, we really don't want to highlight the characters 
5916             # changed, but instead leave the cursor where it is. 
5917             # Also, there a situations in which the difference can be ambiguous; 
5920             # current value:    11234 
5921             # previous value:   1111234 
5923             # Where did the cursor actually lie and which 1s were selected on the delete 
5926             # Also, difflib can "get it wrong;" Consider: 
5928             # current value:    "       128.66" 
5929             # previous value:   "       121.86" 
5931             # difflib produces the following opcodes, which are sub-optimal: 
5932             #    equal value[0:9] (       12) prev[0:9] (       12) 
5933             #   insert value[9:9] () prev[9:11] (1.) 
5934             #    equal value[9:10] (8) prev[11:12] (8) 
5935             #   delete value[10:11] (.) prev[12:12] () 
5936             #    equal value[11:12] (6) prev[12:13] (6) 
5937             #   delete value[12:13] (6) prev[13:13] () 
5939             # This should have been: 
5940             #    equal value[0:9] (       12) prev[0:9] (       12) 
5941             #  replace value[9:11] (8.6) prev[9:11] (1.8) 
5942             #    equal value[12:13] (6) prev[12:13] (6) 
5944             # But it didn't figure this out! 
5946             # To get all this right, we use the previous selection recorded to help us... 
5948             if (sel_start
, sel_to
) != self
._prevSelection
: 
5949 ##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) 
5951                 prev_sel_start
, prev_sel_to 
= self
._prevSelection
 
5952                 field 
= self
._FindField
(sel_start
) 
5954                       and sel_start 
< self
._masklength
 
5955                       and (prev
[sel_start
] in ('-', '(', ')') 
5956                                      or value
[sel_start
] in ('-', '(', ')')) ): 
5957                     # change of sign; leave cursor alone... 
5958 ##                    dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')')) 
5959 ##                    dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')')) 
5960 ##                    dbg('setting selection to previous one') 
5961                     sel_start
, sel_to 
= self
._prevSelection
 
5963                 elif field
._groupdigits 
and (value
[sel_start
:sel_to
] == field
._groupChar
 
5964                                              or prev
[sel_start
:sel_to
] == field
._groupChar
): 
5965                     # do not highlight grouping changes 
5966 ##                    dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar) 
5967 ##                    dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar) 
5968 ##                    dbg('setting selection to previous one') 
5969                     sel_start
, sel_to 
= self
._prevSelection
 
5972                     calc_select_len 
= sel_to 
- sel_start
 
5973                     prev_select_len 
= prev_sel_to 
- prev_sel_start
 
5975 ##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) 
5976 ##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) 
5978                     if prev_select_len 
>= calc_select_len
: 
5979                         # old selection was bigger; trust it: 
5980 ##                        dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len) 
5981                         if not field
._insertRight
: 
5982 ##                            dbg('setting selection to previous one') 
5983                             sel_start
, sel_to 
= self
._prevSelection
 
5985                             sel_to 
= self
._prevSelection
[1] 
5986 ##                            dbg('setting selection to', (sel_start, sel_to)) 
5988                     elif( sel_to 
> prev_sel_to                  
# calculated select past last selection 
5989                           and prev_sel_to 
< len(self
._template
) # and prev_sel_to not at end of control 
5990                           and sel_to 
== len(self
._template
) ):  # and calculated selection goes to end of control 
5992                         i
, j
, k 
= sm
.find_longest_match(prev_sel_to
, length
, prev_sel_to
, length
) 
5993 ##                        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] ) 
5995                             # difflib must not have optimized opcodes properly; 
5999                         # look for possible ambiguous diff: 
6001                         # if last change resulted in no selection, test from resulting cursor position: 
6002                         if prev_sel_start 
== prev_sel_to
: 
6003                             calc_select_len 
= sel_to 
- sel_start
 
6004                             field 
= self
._FindField
(prev_sel_start
) 
6006                             # determine which way to search from last cursor position for ambiguous change: 
6007                             if field
._insertRight
: 
6008                                 test_sel_start 
= prev_sel_start
 
6009                                 test_sel_to 
= prev_sel_start 
+ calc_select_len
 
6011                                 test_sel_start 
= prev_sel_start 
- calc_select_len
 
6012                                 test_sel_to 
= prev_sel_start
 
6014                             test_sel_start
, test_sel_to 
= prev_sel_start
, prev_sel_to
 
6016 ##                        dbg('test selection:', (test_sel_start, test_sel_to)) 
6017 ##                        dbg('calc change: "%s"' % prev[sel_start:sel_to]) 
6018 ##                        dbg('test change: "%s"' % prev[test_sel_start:test_sel_to]) 
6020                         # if calculated selection spans characters, and same characters 
6021                         # "before" the previous insertion point are present there as well, 
6022                         # select the ones related to the last known selection instead. 
6023                         if( sel_start 
!= sel_to
 
6024                             and test_sel_to 
< len(self
._template
) 
6025                             and prev
[test_sel_start
:test_sel_to
] == prev
[sel_start
:sel_to
] ): 
6027                             sel_start
, sel_to 
= test_sel_start
, test_sel_to
 
6029                 # finally, make sure that the old and new values are 
6030                 # different where we say they're different: 
6031                 while( sel_to 
- 1 > 0 
6032                         and sel_to 
> sel_start
 
6033                         and value
[sel_to
-1:] == prev
[sel_to
-1:]): 
6035                 while( sel_start 
+ 1 < self
._masklength
 
6036                         and sel_start 
< sel_to
 
6037                         and value
[:sel_start
+1] == prev
[:sel_start
+1]): 
6040 ##            dbg('sel_start, sel_to:', sel_start, sel_to) 
6041 ##            dbg('previous value: "%s"' % prev) 
6043             if just_return_results
: 
6044                 return prev
, (sel_start
, sel_to
) 
6046             self
._SetValue
(prev
) 
6047             self
._SetInsertionPoint
(sel_start
) 
6048             self
._SetSelection
(sel_start
, sel_to
) 
6051 ##            dbg('no difference between previous value') 
6053             if just_return_results
: 
6054                 return prev
, self
._GetSelection
() 
6057     def _OnClear(self
, event
): 
6058         """ Provides an action for context menu delete operation """ 
6062     def _OnContextMenu(self
, event
): 
6063 ##        dbg('MaskedEditMixin::OnContextMenu()', indent=1) 
6065         menu
.Append(wx
.ID_UNDO
, "Undo", "") 
6066         menu
.AppendSeparator() 
6067         menu
.Append(wx
.ID_CUT
, "Cut", "") 
6068         menu
.Append(wx
.ID_COPY
, "Copy", "") 
6069         menu
.Append(wx
.ID_PASTE
, "Paste", "") 
6070         menu
.Append(wx
.ID_CLEAR
, "Delete", "") 
6071         menu
.AppendSeparator() 
6072         menu
.Append(wx
.ID_SELECTALL
, "Select All", "") 
6074         wx
.EVT_MENU(menu
, wx
.ID_UNDO
, self
._OnCtrl
_Z
) 
6075         wx
.EVT_MENU(menu
, wx
.ID_CUT
, self
._OnCtrl
_X
) 
6076         wx
.EVT_MENU(menu
, wx
.ID_COPY
, self
._OnCtrl
_C
) 
6077         wx
.EVT_MENU(menu
, wx
.ID_PASTE
, self
._OnCtrl
_V
) 
6078         wx
.EVT_MENU(menu
, wx
.ID_CLEAR
, self
._OnClear
) 
6079         wx
.EVT_MENU(menu
, wx
.ID_SELECTALL
, self
._OnCtrl
_A
) 
6081         # ## WSS: The base control apparently handles 
6082         # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE 
6083         # and wx.ID_CLEAR menu items even if the menu is one 
6084         # we created.  However, it doesn't do undo properly, 
6085         # so we're keeping track of previous values ourselves. 
6086         # Therefore, we have to override the default update for 
6087         # that item on the menu: 
6088         wx
.EVT_UPDATE_UI(self
, wx
.ID_UNDO
, self
._UndoUpdateUI
) 
6089         self
._contextMenu 
= menu
 
6091         self
.PopupMenu(menu
, event
.GetPosition()) 
6093         self
._contextMenu 
= None 
6096     def _UndoUpdateUI(self
, event
): 
6097         if self
._prevValue 
is None or self
._prevValue 
== self
._curValue
: 
6098             self
._contextMenu
.Enable(wx
.ID_UNDO
, False) 
6100             self
._contextMenu
.Enable(wx
.ID_UNDO
, True) 
6103     def _OnCtrlParametersChanged(self
): 
6105         Overridable function to allow derived classes to take action as a 
6106         result of parameter changes prior to possibly changing the value 
6111  ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6112 class MaskedEditAccessorsMixin
: 
6114     To avoid a ton of boiler-plate, and to automate the getter/setter generation 
6115     for each valid control parameter so we never forget to add the functions when 
6116     adding parameters, this class programmatically adds the masked edit mixin 
6117     parameters to itself. 
6118     (This makes it easier for Designers like Boa to deal with masked controls.) 
6120     To further complicate matters, this is done with an extra level of inheritance, 
6121     so that "general" classes like masked.TextCtrl can have all possible attributes, 
6122     while derived classes, like masked.TimeCtrl and masked.NumCtrl can prevent 
6123     exposure of those optional attributes of their base class that do not make 
6124     sense for their derivation. 
6126     Therefore, we define: 
6127         BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) 
6129         masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). 
6131     This allows us to then derive: 
6132         masked.NumCtrl( BaseMaskedTextCtrl ) 
6134     and not have to expose all the same accessor functions for the 
6135     derived control when they don't all make sense for it. 
6139     # Define the default set of attributes exposed by the most generic masked controls: 
6140     exposed_basectrl_params 
= MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys() 
6141     exposed_basectrl_params
.remove('index') 
6142     exposed_basectrl_params
.remove('extent') 
6143     exposed_basectrl_params
.remove('foregroundColour')   # (base class already has this) 
6145     for param 
in exposed_basectrl_params
: 
6146         propname 
= param
[0].upper() + param
[1:] 
6147         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
6148         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
6150         if param.find('Colour
') != -1: 
6151             # add non-british spellings, for backward-compatibility 
6152             propname.replace('Colour
', 'Color
') 
6154             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
6155             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
6160 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6161 ## these are helper subroutines: 
6163 def _movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): 
6164     """ addseparators = add separator character every three numerals if True 
6166     fmt0 = fmtstring.split('.') 
6169     val  = origvalue.split('.')[0].strip() 
6170     ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) 
6173     return (ret,len(fmt1)) 
6176 def _isDateType( fmtstring ): 
6177     """ Checks the mask and returns True if it fits an allowed 
6178         date or datetime format. 
6180     dateMasks = ("^##/##/####", 
6192     reString  = "|".join(dateMasks) 
6193     filter = re.compile( reString) 
6194     if re.match(filter,fmtstring): return True 
6197 def _isTimeType( fmtstring ): 
6198     """ Checks the mask and returns True if it fits an allowed 
6201     reTimeMask = "^##:##(:##)?( (AM|PM))?" 
6202     filter = re.compile( reTimeMask ) 
6203     if re.match(filter,fmtstring): return True 
6207 def _isFloatingPoint( fmtstring): 
6208     filter = re.compile("[ ]?[#]+\.[#]+\n") 
6209     if re.match(filter,fmtstring+"\n"): return True 
6213 def _isInteger( fmtstring ): 
6214     filter = re.compile("[#]+\n") 
6215     if re.match(filter,fmtstring+"\n"): return True 
6219 def _getDateParts( dateStr, dateFmt ): 
6220     if len(dateStr) > 11: clip = dateStr[0:11] 
6221     else:                 clip = dateStr 
6222     if clip[-2] not in string.digits: 
6223         clip = clip[:-1]    # (got part of time; drop it) 
6225     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6226     slices  = clip.split(dateSep) 
6227     if dateFmt == "MDY": 
6228         y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts 
6229     elif dateFmt == "DMY": 
6230         y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts 
6231     elif dateFmt == "YMD": 
6232         y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts 
6234         y,m,d = None, None, None 
6241 def _getDateSepChar(dateStr): 
6242     clip   = dateStr[0:10] 
6243     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6247 def _makeDate( year, month, day, dateFmt, dateStr): 
6248     sep    = _getDateSepChar( dateStr) 
6249     if dateFmt == "MDY": 
6250         return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts 
6251     elif dateFmt == "DMY": 
6252         return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts 
6253     elif dateFmt == "YMD": 
6254         return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts 
6259 def _getYear(dateStr,dateFmt): 
6260     parts = _getDateParts( dateStr, dateFmt) 
6263 def _getMonth(dateStr,dateFmt): 
6264     parts = _getDateParts( dateStr, dateFmt) 
6267 def _getDay(dateStr,dateFmt): 
6268     parts = _getDateParts( dateStr, dateFmt) 
6271 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6272 class __test(wx.PySimpleApp): 
6274             from wx.lib.rcsizer import RowColSizer 
6275             self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) 
6276             self.panel = wx.Panel( self.frame, -1) 
6277             self.sizer = RowColSizer() 
6282             id, id1 = wx.NewId(), wx.NewId() 
6283             self.command1  = wx.Button( self.panel, id, "&Close" ) 
6284             self.command2  = wx.Button( self.panel, id1, "&AutoFormats" ) 
6285             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6286             self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) 
6287             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) 
6288 ##            self.panel.SetDefaultItem(self.command1 ) 
6289             self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) 
6291             self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) 
6292             self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) 
6293             self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) 
6294             self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) 
6295             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) 
6296             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) 
6299             label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. 
6300 Note that all controls have been auto-sized by including F in the format code. 
6301 Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" 
6302             label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." 
6304             self.label1 = wx.StaticText( self.panel, -1, label) 
6305             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6306             self.label3 = wx.StaticText( self.panel, -1, "Mask Value") 
6307             self.label4 = wx.StaticText( self.panel, -1, "Format") 
6308             self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") 
6309             self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") 
6310             self.label7 = wx.StaticText( self.panel, -1, label2) 
6311             self.label7.SetForegroundColour("Blue") 
6312             self.label1.SetForegroundColour("Blue") 
6313             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6314             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6315             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6316             self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6317             self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6319             self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) 
6320             self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) 
6321             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6322             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6323             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6324             self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) 
6325             self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) 
6327             # The following list is of the controls for the demo. Feel free to play around with 
6330             #description        mask                    excl format     regexp                              range,list,initial 
6331            ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''), 
6332            ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''), 
6333            ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''), 
6334            ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''), 
6335            ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''), 
6336            ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'), 
6337            ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''), 
6338            ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'), 
6339            ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''), 
6340            ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '), 
6341            ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '), 
6342            ("Month selector",   "XXX",                  "", 'F',        "",                                 (), 
6343                 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), 
6344            ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (), 
6345                 ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") 
6348             for control in controls: 
6349                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6350                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6351                 self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) 
6352                 self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) 
6354                 if control in controls[:]:#-2]: 
6355                     newControl  = MaskedTextCtrl( self.panel, -1, "", 
6357                                                     excludeChars = control[2], 
6358                                                     formatcodes  = control[3], 
6360                                                     validRegex   = control[4], 
6361                                                     validRange   = control[5], 
6362                                                     choices      = control[6], 
6363                                                     defaultValue = control[7], 
6365                     if control[6]: newControl.SetCtrlParameters(choiceRequired = True) 
6367                     newControl = MaskedComboBox(  self.panel, -1, "", 
6368                                                     choices = control[7], 
6369                                                     choiceRequired  = True, 
6371                                                     formatcodes  = control[3], 
6372                                                     excludeChars = control[2], 
6374                                                     validRegex   = control[4], 
6375                                                     validRange   = control[5], 
6377                 self.editList.append( newControl ) 
6379                 self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) 
6382             self.sizer.AddGrowableCol(4) 
6384             self.panel.SetSizer(self.sizer) 
6385             self.panel.SetAutoLayout(1) 
6392         def onClick(self, event): 
6395         def onClickPage(self, event): 
6396             self.page2 = __test2(self.frame,-1,"") 
6397             self.page2.Show(True) 
6399         def _onCheck1(self,event): 
6400             """ Set required value on/off """ 
6401             value = event.IsChecked() 
6403                 for control in self.editList: 
6404                     control.SetCtrlParameters(emptyInvalid=True) 
6407                 for control in self.editList: 
6408                     control.SetCtrlParameters(emptyInvalid=False) 
6410             self.panel.Refresh() 
6412         def _onCheck2(self,event): 
6413             """ Highlight empty values""" 
6414             value = event.IsChecked() 
6416                 for control in self.editList: 
6417                     control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') 
6420                 for control in self.editList: 
6421                     control.SetCtrlParameters( emptyBackgroundColour = 'White') 
6423             self.panel.Refresh() 
6426 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6428 class __test2(wx.Frame): 
6429         def __init__(self, parent, id, caption): 
6430             wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) 
6431             from wx.lib.rcsizer import RowColSizer 
6432             self.panel = wx.Panel( self, -1) 
6433             self.sizer = RowColSizer() 
6439 All these controls have been created by passing a single parameter, the AutoFormat code. 
6440 The class contains an internal dictionary of types and formats (autoformats). 
6441 To see a great example of validations in action, try entering a bad email address, then tab out.""" 
6443             self.label1 = wx.StaticText( self.panel, -1, label) 
6444             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6445             self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") 
6446             self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") 
6447             self.label1.SetForegroundColour("Blue") 
6448             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6449             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6450             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6452             self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) 
6453             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6454             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6455             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6457             id, id1 = wx.NewId(), wx.NewId() 
6458             self.command1  = wx.Button( self.panel, id, "&Close") 
6459             self.command2  = wx.Button( self.panel, id1, "&Print Formats") 
6460             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) 
6461             self.panel.SetDefaultItem(self.command1) 
6462             self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) 
6464             # The following list is of the controls for the demo. Feel free to play around with 
6467            ("Phone No","USPHONEFULLEXT"), 
6468            ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), 
6469            ("US Date MMDDYYYY","USDATEMMDDYYYY/"), 
6470            ("Time (with seconds)","TIMEHHMMSS"), 
6471            ("Military Time\n(without seconds)","24HRTIMEHHMM"), 
6472            ("Social Sec#","USSOCIALSEC"), 
6473            ("Credit Card","CREDITCARD"), 
6474            ("Expiration MM/YY","EXPDATEMMYY"), 
6475            ("Percentage","PERCENT"), 
6476            ("Person's Age","AGE"), 
6477            ("US Zip Code","USZIP"), 
6478            ("US Zip+4","USZIPPLUS4"), 
6479            ("Email Address","EMAIL"), 
6480            ("IP Address", "(derived control IpAddrCtrl)") 
6483             for control in controls: 
6484                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6485                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6486                 if control in controls[:-1]: 
6487                     self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", 
6488                                                       autoformat  = control[1], 
6490                                 row=rowcount,col=2,flag=wx.ALL,border=5) 
6492                     self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), 
6493                                     row=rowcount,col=2,flag=wx.ALL,border=5) 
6496             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6497             self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) 
6498             self.sizer.AddGrowableCol(3) 
6500             self.panel.SetSizer(self.sizer) 
6501             self.panel.SetAutoLayout(1) 
6503         def onClick(self, event): 
6506         def onClickPrint(self, event): 
6507             for format in masktags.keys(): 
6508                 sep = "+------------------------+" 
6509                 print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) 
6511 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6513 if __name__ == "__main__": 
6519 ## =================================== 
6521 ## 1. WS: For some reason I don't understand, the control is generating two (2) 
6522 ##      EVT_TEXT events for every one (1) .SetValue() of the underlying control. 
6523 ##      I've been unsuccessful in determining why or in my efforts to make just one 
6524 ##      occur.  So, I've added a hack to save the last seen value from the 
6525 ##      control in the EVT_TEXT handler, and if *different*, call event.Skip() 
6526 ##      to propagate it down the event chain, and let the application see it. 
6528 ## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the 
6529 ##      behavior of the underlying control that I can't fix.  The problems are: 
6530 ##      a) The background coloring doesn't work in the text field of the control; 
6531 ##         instead, there's a only border around it that assumes the correct color. 
6532 ##      b) The control will not pass WXK_TAB to the event handler, no matter what 
6533 ##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to 
6534 ##         indicate that we want these events.  As a result, MaskedComboBox 
6535 ##         doesn't do the nice field-tabbing that MaskedTextCtrl does. 
6536 ##      c) Auto-complete had to be reimplemented for the control because programmatic 
6537 ##         setting of the value of the text field does not set up the auto complete 
6538 ##         the way that the control processing keystrokes does.  (But I think I've 
6539 ##         implemented a fairly decent approximation.)  Because of this the control 
6540 ##         also won't auto-complete on dropdown, and there's no event I can catch 
6541 ##         to work around this problem. 
6542 ##      d) There is no method provided for getting the selection; the hack I've 
6543 ##         implemented has its flaws, not the least of which is that due to the 
6544 ##         strategy that I'm using, the paste buffer is always replaced by the 
6545 ##         contents of the control's selection when in focus, on each keystroke; 
6546 ##         this makes it impossible to paste anything into a MaskedComboBox 
6547 ##         at the moment... :-( 
6548 ##      e) The other deficient behavior, likely induced by the workaround for (d), 
6549 ##         is that you can can't shift-left to select more than one character 
6553 ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their 
6554 ##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in 
6555 ##      wxWindows, shift-tab won't take you backwards through the fields of 
6556 ##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will 
6557 ##      work like shift-tab and tab ought to. 
6561 ## =============================## 
6562 ##  1. Add Popup list for auto-completable fields that simulates combobox on individual 
6563 ##     fields.  Example: City validates against list of cities, or zip vs zip code list. 
6564 ##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" 
6566 ##  3. Fix shift-left selection for MaskedComboBox. 
6567 ##  5. Transform notion of "decimal control" to be less "entire control"-centric, 
6568 ##     so that monetary symbols can be included and still have the appropriate 
6569 ##     semantics.  (Big job, as currently written, but would make control even 
6570 ##     more useful for business applications.) 
6574 ## ==================== 
6576 ##  1. Now ignores kill focus events when being destroyed. 
6577 ##  2. Added missing call to set insertion point on changing fields. 
6578 ##  3. Modified SetKeyHandler() to accept None as means of removing one. 
6579 ##  4. Fixed keyhandler processing for group and decimal character changes. 
6580 ##  5. Fixed a problem that prevented input into the integer digit of a  
6581 ##     integerwidth=1 numctrl, if the current value was 0. 
6582 ##  6. Fixed logic involving processing of "_signOk" flag, to remove default 
6583 ##     sign key handlers if false, so that SetAllowNegative(False) in the 
6584 ##     NumCtrl works properly. 
6585 ##  7. Fixed selection logic for numeric controls so that if selectOnFieldEntry 
6586 ##     is true, and the integer portion of an integer format control is selected 
6587 ##     and the sign position is selected, the sign keys will always result in a 
6588 ##     negative value, rather than toggling the previous sign. 
6592 ##  1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail. 
6593 ##  2. Added proper support for unicode version of wxPython 
6594 ##  3. Added * as mask char meaning "all ansi chars" (ordinals 32-255). 
6595 ##  4. Converted doc strings to use reST format, for ePyDoc documentation. 
6596 ##  5. Renamed helper functions, classes, etc. not intended to be visible in public 
6597 ##     interface to code. 
6600 ##  1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead 
6601 ##     shifts the text to the left accordingly. 
6602 ##  2. Fixed _SetValue() to place cursor after last character inserted, rather than end of 
6604 ##  3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes 
6605 ##     (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping 
6607 ##  4. Fixed autoselect behavior to work similarly to (2) above, so that combobox 
6608 ##     selection will only select the non-empty text, as per request. 
6609 ##  5. Fixed tabbing to work with 2.5.2 semantics. 
6610 ##  6. Fixed size calculation to handle changing fonts 
6613 ##  1. Reorganized masked controls into separate package, renamed things accordingly 
6614 ##  2. Split actual controls out of this file into their own files. 
6616 ##  (Reported) bugs fixed: 
6617 ##   1. Crash ensues if you attempt to change the mask of a read-only 
6618 ##      MaskedComboBox after initial construction. 
6619 ##   2. Changed strategy of defining Get/Set property functions so that 
6620 ##      these are now generated dynamically at runtime, rather than as 
6621 ##      part of the class definition.  (This makes it possible to have 
6622 ##      more general base classes that have many more options for configuration 
6623 ##      without requiring that derivations support the same options.) 
6624 ##   3. Fixed IsModified for _Paste() and _OnErase(). 
6627 ##   1. Fixed "attribute function inheritance," since base control is more 
6628 ##      generic than subsequent derivations, not all property functions of a 
6629 ##      generic control should be exposed in those derivations.  New strategy 
6630 ##      uses base control classes (eg. BaseMaskedTextCtrl) that should be 
6631 ##      used to derive new class types, and mixed with their own mixins to 
6632 ##      only expose those attributes from the generic masked controls that 
6633 ##      make sense for the derivation.  (This makes Boa happier.) 
6634 ##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less 
6638 ##  (Reported) bugs fixed: 
6639 ##   1. Right-click menu allowed "cut" operation that destroyed mask 
6640 ##      (was implemented by base control) 
6641 ##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all 
6642 ##      got converted to lower case. 
6643 ##   3. MaskedComboBox selection didn't deal with spaces in values 
6644 ##      properly when autocompleting, and didn't have a concept of "next" 
6645 ##      match for handling choice list duplicates. 
6646 ##   4. Size of MaskedComboBox was always default. 
6647 ##   5. Email address regexp allowed some "non-standard" things, and wasn't 
6649 ##   6. Couldn't easily reset MaskedComboBox contents programmatically. 
6650 ##   7. Couldn't set emptyInvalid during construction. 
6651 ##   8. Under some versions of wxPython, readonly comboboxes can apparently 
6652 ##      return a GetInsertionPoint() result (655535), causing masked control 
6654 ##   9. Specifying an empty mask caused the controls to traceback. 
6655 ##  10. Can't specify float ranges for validRange. 
6656 ##  11. '.' from within a the static portion of a restricted IP address 
6657 ##      destroyed the mask from that point rightward; tab when cursor is 
6658 ##      before 1st field takes cursor past that field. 
6661 ##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) 
6662 ##  13. Added auto-select option on char input for masked controls with 
6664 ##  14. Added '>' formatcode, allowing insert within a given or each field 
6665 ##      as appropriate, rather than requiring "overwrite".  This makes single 
6666 ##      field controls that just have validation rules (eg. EMAIL) much more 
6667 ##      friendly.  The same flag controls left shift when deleting vs just 
6668 ##      blanking the value, and for right-insert fields, allows right-insert 
6669 ##      at any non-blank (non-sign) position in the field. 
6670 ##  15. Added option to use to indicate negative values for numeric controls. 
6671 ##  16. Improved OnFocus handling of numeric controls. 
6672 ##  17. Enhanced Home/End processing to allow operation on a field level, 
6674 ##  18. Added individual Get/Set functions for control parameters, for 
6675 ##      simplified integration with Boa Constructor. 
6676 ##  19. Standardized "Colour" parameter names to match wxPython, with 
6677 ##      non-british spellings still supported for backward-compatibility. 
6678 ##  20. Added '&' mask specification character for punctuation only (no letters 
6680 ##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide 
6681 ##      unified interface to the masked edit subclasses. 
6685 ##   1. Made it possible to configure grouping, decimal and shift-decimal characters, 
6686 ##      to make controls more usable internationally. 
6687 ##   2. Added code to smart "adjust" value strings presented to .SetValue() 
6688 ##      for right-aligned numeric format controls if they are shorter than 
6689 ##      than the control width,  prepending the missing portion, prepending control 
6690 ##      template left substring for the missing characters, so that setting 
6691 ##      numeric values is easier. 
6692 ##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved 
6693 ##      for b-c), as this makes more sense. 
6696 ##   1. Fixed .SetValue() to replace the current value, rather than the current 
6697 ##      selection. Also changed it to generate ValueError if presented with 
6698 ##      either a value which doesn't follow the format or won't fit.  Also made 
6699 ##      set value adjust numeric and date controls as if user entered the value. 
6700 ##      Expanded doc explaining how SetValue() works. 
6701 ##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to 
6702 ##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. 
6703 ##   3. Made all date autoformats automatically pick implied "datestyle". 
6704 ##   4. Added IsModified override, since base wx.TextCtrl never reports modified if 
6705 ##      .SetValue used to change the value, which is what the masked edit controls 
6707 ##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when 
6708 ##      using tab to "leave field" and auto-adjust. 
6709 ##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, 
6710 ##      and bug in ._Paste() that didn't account for signs in signed masks either. 
6711 ##   7. Fixed issues with _adjustPos for right-insert fields causing improper 
6712 ##      selection/replacement of values 
6713 ##   8. Fixed _OnHome handler to properly handle extending current selection to 
6714 ##      beginning of control. 
6715 ##   9. Exposed all (valid) autoformats to demo, binding descriptions to 
6717 ##  10. Fixed a couple of bugs in email regexp. 
6718 ##  11. Made maskchardict an instance var, to make mask chars to be more 
6719 ##      amenable to international use. 
6720 ##  12. Clarified meaning of '-' formatcode in doc. 
6721 ##  13. Fixed a couple of coding bugs being flagged by Python2.1. 
6722 ##  14. Fixed several issues with sign positioning, erasure and validity 
6723 ##      checking for "numeric" masked controls. 
6724 ##  15. Added validation to IpAddrCtrl.SetValue(). 
6727 ##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) 
6728 ##      vs. literal font facename, and use wxTELETYPE as the font family 
6730 ##   2. Switched to use of dbg module vs. locally defined version. 
6731 ##   3. Revamped entire control structure to use Field classes to hold constraint 
6732 ##      and formatting data, to make code more hierarchical, allow for more 
6733 ##      sophisticated masked edit construction. 
6734 ##   4. Better strategy for managing options, and better validation on keywords. 
6735 ##   5. Added 'V' format code, which requires that in order for a character 
6736 ##      to be accepted, it must result in a string that passes the validRegex. 
6737 ##   6. Added 'S' format code which means "select entire field when navigating 
6739 ##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) 
6740 ##   8. Added '<' format code to allow fields to require explicit cursor movement 
6742 ##   9. Added validFunc option to other validation mechanisms, that allows derived 
6743 ##      classes to add dynamic validation constraints to the control. 
6744 ##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also 
6745 ##      fixed failure to obey case conversion codes when pasting. 
6746 ##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... 
6747 ##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' 
6748 ##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, 
6749 ##      individual field validation to prevent field values > 255, and require explicit 
6750 ##      tab/. to change fields. 
6751 ##  14. Added handler for left double-click to select field under cursor. 
6752 ##  15. Fixed handling for "Read-only" styles. 
6753 ##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor 
6754 ##      attribute, for more consistent and controllable coloring. 
6755 ##  17. Added retainFieldValidation parameter, allowing top-level constraints 
6756 ##      such as "validRequired" to be set independently of field-level equivalent. 
6757 ##      (needed in TimeCtrl for bounds constraints.) 
6758 ##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed 
6759 ##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, 
6761 ##  19. Fixed maskchar setting for upper/lowercase, to work in all locales. 
6765 ##   1. Decimal point behavior restored for decimal and integer type controls: 
6766 ##      decimal point now trucates the portion > 0. 
6767 ##   2. Return key now works like the tab character and moves to the next field, 
6768 ##      provided no default button is set for the form panel on which the control 
6770 ##   3. Support added in _FindField() for subclasses controls (like timecontrol) 
6771 ##      to determine where the current insertion point is within the mask (i.e. 
6772 ##      which sub-'field'). See method documentation for more info and examples. 
6773 ##   4. Added Field class and support for all constraints to be field-specific 
6774 ##      in addition to being globally settable for the control. 
6775 ##      Choices for each field are validated for length and pastability into 
6776 ##      the field in question, raising ValueError if not appropriate for the control. 
6777 ##      Also added selective additional validation based on individual field constraints. 
6778 ##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all 
6779 ##      auto-complete fields with choice lists, supplying the 1st entry in 
6780 ##      the choice list if the field is empty, and cycling through the list in 
6781 ##      the appropriate direction if already a match.  WXK_DOWN will also auto- 
6782 ##      complete if the field is partially completed and a match can be made. 
6783 ##      SHIFT-WXK_UP/DOWN will also take you to the next field after any 
6784 ##      auto-completion performed. 
6785 ##   5. Added autoCompleteKeycodes=[] parameters for allowing further 
6786 ##      customization of the control.  Any keycode supplied as a member 
6787 ##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If 
6788 ##      requireFieldChoice is set, then a valid value from each non-empty 
6789 ##      choice list will be required for the value of the control to validate. 
6790 ##   6. Fixed "auto-sizing" to be relative to the font actually used, rather 
6791 ##      than making assumptions about character width. 
6792 ##   7. Fixed GetMaskParameter(), which was non-functional in previous version. 
6793 ##   8. Fixed exceptions raised to provide info on which control had the error. 
6794 ##   9. Fixed bug in choice management of MaskedComboBox. 
6795 ##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of 
6796 ##     the form '# #'.  Modified control code for IpAddrCtrl so that '.' 
6797 ##     in the middle of a field clips the rest of that field, similar to 
6798 ##     decimal and integer controls. 
6802 ##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. 
6803 ##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). 
6804 ##   3. New support for selecting text within the control.(thanks Will Sadkin!) 
6805 ##      Shift-End and Shift-Home now select text as you would expect 
6806 ##      Control-Shift-End selects to the end of the mask string, even if value not entered. 
6807 ##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control. 
6808 ##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- 
6809 ##      for some reason I couldn't find the original email but thanks!!!) 
6810 ##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome, 
6811 ##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. 
6812 ##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). 
6813 ##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) 
6814 ##   7. New mechanism for replacing default behavior for any given key, using 
6815 ##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available 
6816 ##      for easier subclassing of the control. 
6817 ##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs 
6818 ##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut" 
6819 ##      semantics, erasing the selection, rather than erasing the entire control. 
6820 ##   9. Added option for an "default value" (ie. the template) for use when a single fillChar 
6821 ##      is not desired in every position.  Added IsDefault() function to mean "does the value 
6822 ##      equal the template?" and modified .IsEmpty() to mean "do all of the editable 
6823 ##      positions in the template == the fillChar?" 
6824 ##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, 
6826 ##  11. MaskedComboBox now adds the capability to validate from list of valid values. 
6827 ##      Example: City validates against list of cities, or zip vs zip code list. 
6828 ##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being 
6829 ##      passed to the next handler in the event chain, causing updates to the 
6830 ##      control to be invisible to the parent code. 
6831 ##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within 
6832 ##      the control, that auto-reformats as you move between cells. 
6833 ##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. 
6834 ##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} 
6835 ##  16. Fixed major bugs in date validation, due to the fact that 
6836 ##      wxDateTime.ParseDate is too liberal, and will accept any form that 
6837 ##      makes any kind of sense, regardless of the datestyle you specified 
6838 ##      for the control.  Unfortunately, the strategy used to fix it only 
6839 ##      works for versions of wxPython post 2.3.3.1, as a C++ assert box 
6840 ##      seems to show up on an invalid date otherwise, instead of a catchable 
6842 ##  17. Enhanced date adjustment to automatically adjust heuristic based on 
6843 ##      current year, making last century/this century determination on 
6844 ##      2-digit year based on distance between today's year and value; 
6845 ##      if > 50 year separation, assume last century (and don't assume last 
6846 ##      century is 20th.) 
6847 ##  18. Added autoformats and support for including HHMMSS as well as HHMM for 
6848 ##      date times, and added similar time, and militaray time autoformats. 
6849 ##  19. Enhanced tabbing logic so that tab takes you to the next field if the 
6850 ##      control is a multi-field control. 
6851 ##  20. Added stub method called whenever the control "changes fields", that 
6852 ##      can be overridden by subclasses (eg. IpAddrCtrl.) 
6853 ##  21. Changed a lot of code to be more functionally-oriented so side-effects 
6854 ##      aren't as problematic when maintaining code and/or adding features. 
6855 ##      Eg: IsValid() now does not have side-effects; it merely reflects the 
6856 ##      validity of the value of the control; to determine validity AND recolor 
6857 ##      the control, _CheckValid() should be used with a value argument of None. 
6858 ##      Similarly, made most reformatting function take an optional candidate value 
6859 ##      rather than just using the current value of the control, and only 
6860 ##      have them change the value of the control if a candidate is not specified. 
6861 ##      In this way, you can do validation *before* changing the control. 
6862 ##  22. Changed validRequired to mean "disallow chars that result in invalid 
6863 ##      value."  (Old meaning now represented by emptyInvalid.)  (This was 
6864 ##      possible once I'd made the changes in (19) above.) 
6865 ##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they 
6866 ##      can be set/modified/retrieved after construction.  Removed individual 
6867 ##      parameter setting functions, in favor of this mechanism, so that 
6868 ##      all adjustment of the control based on changing parameter values can 
6869 ##      be handled in one place with unified mechanism. 
6870 ##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability 
6871 ##      to type "grouping char" (ie. ',') and validate as appropriate. 
6872 ##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. 
6873 ##  26. Fixed assumption about "decimal or integer" masks so that they're only 
6874 ##      made iff there's no validRegex associated with the field.  (This 
6875 ##      is so things like zipcodes which look like integers can have more 
6876 ##      restrictive validation (ie. must be 5 digits.) 
6877 ##  27. Added a ton more doc strings to explain use and derivation requirements 
6878 ##      and did regularization of the naming conventions. 
6879 ##  28. Fixed a range bug in _adjustKey preventing z from being handled properly. 
6880 ##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to 
6881 ##      reformat the value and move the next field as appropriate. (shift-'.', 
6882 ##      ie. '>' moves to the previous field. 
6885 ##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending 
6887 ##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the 
6888 ##      year position, the control will expand the value to four digits, using numerals below 
6889 ##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). 
6890 ##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} 
6891 ##   3. revalid parameter renamed validRegex to conform to standard for all validation 
6892 ##      parameters (see 2 new ones below). 
6893 ##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types. 
6894 ##      Allows the developer to specify a valid low/high range of values. 
6895 ##   5. New optional init parameter = validList. Used for character types. Allows developer 
6896 ##      to send a list of values to the control to be used for specific validation. 
6897 ##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams. 
6898 ##   6. Date type fields now use wxDateTime's parser to validate the date and time. 
6899 ##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing 
6900 ##      me toward this solution! 
6901 ##   7. Date fields now automatically expand 2-digit years when it can. For example, 
6902 ##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year 
6903 ##      date is entered it will be expanded in any case when the user tabs out of the 
6905 ##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, 
6906 ##      SetSignedForeColor allow accessto override default class coloring behavior. 
6907 ##   9. Documentation updated and improved. 
6908 ##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. 
6909 ##      Two new options (checkboxes) - test highlight empty and disallow empty. 
6910 ##  11. Home and End now work more intuitively, moving to the first and last user-entry 
6911 ##      value, respectively. 
6912 ##  12. New class function: SetRequired(bool). Sets the control's entry required flag 
6913 ##      (i.e. disallow empty values if True). 
6916 ##   1. get_plainValue method renamed to GetPlainValue following the wxWindows 
6917 ##      StudlyCaps(tm) standard (thanks Paul Moore).  ;) 
6918 ##   2. New format code 'F' causes the control to auto-fit (auto-size) itself 
6919 ##      based on the length of the mask template. 
6920 ##   3. Class now supports "autoformat" codes. These can be passed to the class 
6921 ##      on instantiation using the parameter autoformat="code". If the code is in 
6922 ##      the dictionary, it will self set the mask, formatting, and validation string. 
6923 ##      I have included a number of samples, but I am hoping that someone out there 
6924 ##      can help me to define a whole bunch more. 
6925 ##   4. I have added a second page to the demo (as well as a second demo class, test2) 
6926 ##      to showcase how autoformats work. The way they self-format and self-size is, 
6927 ##      I must say, pretty cool. 
6928 ##   5. Comments added and some internal cosmetic revisions re: matching the code 
6929 ##      standards for class submission. 
6930 ##   6. Regex validation is now done in real time - field turns yellow immediately 
6931 ##      and stays yellow until the entered value is valid 
6932 ##   7. Cursor now skips over template characters in a more intuitive way (before the 
6934 ##   8. Change, Keypress and LostFocus methods added for convenience of subclasses. 
6935 ##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, 
6936 ##      and EVT_KILL_FOCUS, respectively. 
6937 ##   9. Decimal and numeric handlers have been rewritten and now work more intuitively. 
6940 ##   1. New .IsEmpty() method returns True if the control's value is equal to the 
6941 ##      blank template string 
6942 ##   2. Control now supports a new init parameter: revalid. Pass a regular expression 
6943 ##      that the value will have to match when the control loses focus. If invalid, 
6944 ##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next). 
6945 ##   3. Demo now shows revalid functionality. Try entering a partial value, such as a 
6946 ##      partial social security number. 
6947 ##   4. New .IsValid() value returns True if the control is empty, or if the value matches 
6948 ##      the revalid expression. If not, .IsValid() returns False. 
6949 ##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never 
6950 ##      presses the decimal point. 
6951 ##   6. Cursor now goes to the beginning of the field if the user clicks in an 
6952 ##      "empty" field intead of leaving the insertion point in the middle of the 
6954 ##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. 
6955 ##   8. New formatcodes init parameter replaces other init params and adds functions. 
6956 ##      String passed to control on init controls: 
6960 ##        R Show negative #s in red 
6962 ##        - Signed numerals 
6963 ##        0 Numeric fields get leading zeros 
6964 ##   9. Ctrl-X in any field clears the current value. 
6965 ##   10. Code refactored and made more modular (esp in OnChar method). Should be more 
6966 ##       easy to read and understand. 
6967 ##   11. Demo enhanced. 
6968 ##   12. Now has _doc_. 
6971 ##   1. GetPlainValue() now returns the value without the template characters; 
6972 ##      so, for example, a social security number (123-33-1212) would return as 
6973 ##      123331212; also removes white spaces from numeric/decimal values, so 
6974 ##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value. 
6975 ##   2. Press '.' in an integer style masked control and truncate any trailing digits. 
6976 ##   3. Code moderately refactored. Internal names improved for clarity. Additional 
6977 ##      internal documentation. 
6978 ##   4. Home and End keys now supported to move cursor to beginning or end of field. 
6979 ##   5. Un-signed integers and decimals now supported. 
6980 ##   6. Cosmetic improvements to the demo. 
6981 ##   7. Class renamed to MaskedTextCtrl. 
6982 ##   8. Can now specify include characters that will override the basic 
6983 ##      controls: for example, includeChars = "@." for email addresses 
6984 ##   9. Added mask character 'C' -> allow any upper or lowercase character 
6985 ##   10. .SetSignColor(str:color) sets the foreground color for negative values 
6986 ##       in signed controls (defaults to red) 
6987 ##   11. Overview documentation written. 
6990 ##   1. Tab now works properly when pressed in last position 
6991 ##   2. Decimal types now work (e.g. #####.##) 
6992 ##   3. Signed decimal or numeric values supported (i.e. negative numbers) 
6993 ##   4. Negative decimal or numeric values now can show in red. 
6994 ##   5. Can now specify an "exclude list" with the excludeChars parameter. 
6995 ##      See date/time formatted example - you can only enter A or P in the 
6996 ##      character mask space (i.e. AM/PM). 
6997 ##   6. Backspace now works properly, including clearing data from a selected 
6998 ##      region but leaving template characters intact. Also delete key. 
6999 ##   7. Left/right arrows now work properly. 
7000 ##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3