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         "Navigation" keys are assumed to change the cursor position, and 
 700         therefore don't cause automatic motion of the cursor as insertable 
 703         ._AddNavKeycode(keycode, handler=None) 
 704         ._AddNavKey(char, handler=None) 
 705                         Allows controls to specify other keys (and optional handlers) 
 706                         to be treated as navigational characters. (eg. '.' in IpAddrCtrl) 
 708         ._GetNavKeycodes()  Returns the current list of navigational keycodes. 
 710         ._SetNavKeycodes(key_func_tuples) 
 711                         Allows replacement of the current list of keycode 
 712                         processed as navigation keys, and bind associated 
 713                         optional keyhandlers. argument is a list of key/handler 
 714                         tuples.  Passing a value of None for the handler in a 
 715                         given tuple indicates that default processing for the key 
 718         ._FindField(pos) Returns the Field object associated with this position 
 721         ._FindFieldExtent(pos, getslice=False, value=None) 
 722                         Returns edit_start, edit_end of the field corresponding 
 723                         to the specified position within the control, and 
 724                         optionally also returns the current contents of that field. 
 725                         If value is specified, it will retrieve the slice the corresponding 
 726                         slice from that value, rather than the current value of the 
 730                         This is, the function that gets called for a given position 
 731                         whenever the cursor is adjusted to leave a given field. 
 732                         By default, it adjusts the year in date fields if mask is a date, 
 733                         It can be overridden by a derived class to 
 734                         adjust the value of the control at that time. 
 735                         (eg. IpAddrCtrl reformats the address in this way.) 
 737         ._Change()      Called by internal EVT_TEXT handler. Return False to force 
 738                         skip of the normal class change event. 
 739         ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force 
 740                         skip of the normal class keypress event. 
 741         ._LostFocus()   Called by internal EVT_KILL_FOCUS handler 
 744                         This is the default EVT_KEY_DOWN routine; it just checks for 
 745                         "navigation keys", and if event.ControlDown(), it fires the 
 746                         mixin's _OnChar() routine, as such events are not always seen 
 747                         by the "cooked" EVT_CHAR routine. 
 749         ._OnChar(event) This is the main EVT_CHAR handler for the 
 752     The following routines are used to handle standard actions 
 754         _OnArrow(event)         used for arrow navigation events 
 755         _OnCtrl_A(event)        'select all' 
 756         _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive) 
 757         _OnCtrl_S(event)        'save' (does nothing) 
 758         _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste 
 759         _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection 
 760         _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any) 
 762         _OnChangeField(event)   primarily used for tab events, but can be 
 763                                 used for other keys (eg. '.' in IpAddrCtrl) 
 765         _OnErase(event)         used for backspace and delete 
 769     The following routine provides a hook back to any class derivations, so that 
 770     they can react to parameter changes before any value is set/reset as a result of 
 771     those changes.  (eg. masked.ComboBox needs to detect when the choices list is 
 772     modified, either implicitly or explicitly, so it can reset the base control 
 773     to have the appropriate choice list *before* the initial value is reset to match.) 
 775         _OnCtrlParametersChanged() 
 779     For convenience, each class derived from MaskedEditMixin should 
 780     define an accessors mixin, so that it exposes only those parameters 
 781     that make sense for the derivation.  This is done with an intermediate 
 782     level of inheritance, ie: 
 784     class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): 
 786     class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): 
 787     class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): 
 788     class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): 
 789     class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): 
 790     class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): 
 794     Each accessors mixin defines Get/Set functions for the base class parameters 
 795     that are appropriate for that derivation. 
 796     This allows the base classes to be "more generic," exposing the widest 
 797     set of options, while not requiring derived classes to be so general. 
 808 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
 809 # be a good place to implement the 2.3 logger class 
 810 from wx
.tools
.dbg 
import Logger
 
 815 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 817 ## Constants for identifying control keys and classes of keys: 
 819 WXK_CTRL_A 
= (ord('A')+1) - ord('A')   ## These keys are not already defined in wx 
 820 WXK_CTRL_C 
= (ord('C')+1) - ord('A') 
 821 WXK_CTRL_S 
= (ord('S')+1) - ord('A') 
 822 WXK_CTRL_V 
= (ord('V')+1) - ord('A') 
 823 WXK_CTRL_X 
= (ord('X')+1) - ord('A') 
 824 WXK_CTRL_Z 
= (ord('Z')+1) - ord('A') 
 827     wx
.WXK_BACK
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
, wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_TAB
, 
 828     wx
.WXK_HOME
, wx
.WXK_END
, wx
.WXK_RETURN
, wx
.WXK_PRIOR
, wx
.WXK_NEXT
 
 832     wx
.WXK_BACK
, wx
.WXK_DELETE
, WXK_CTRL_A
, WXK_CTRL_C
, WXK_CTRL_S
, WXK_CTRL_V
, 
 833     WXK_CTRL_X
, WXK_CTRL_Z
 
 837 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 839 ## Constants for masking. This is where mask characters 
 841 ##  maskchars used to identify valid mask characters from all others 
 842 ##   #- allow numeric 0-9 only 
 843 ##   A- allow uppercase only. Combine with forceupper to force lowercase to upper 
 844 ##   a- allow lowercase only. Combine with forcelower to force upper to lowercase 
 845 ##   X- allow any character (string.letters, string.punctuation, string.digits) 
 846 ## Note: locale settings affect what "uppercase", lowercase, etc comprise. 
 848 maskchars 
= ("#","A","a","X","C","N",'*','&') 
 850 for i 
in xrange(32, 256): 
 853 months 
= '(01|02|03|04|05|06|07|08|09|10|11|12)' 
 854 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)' 
 855 charmonths_dict 
= {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 
 856                    'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} 
 858 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)' 
 859 hours  
= '(0\d| \d|1[012])' 
 860 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)' 
 861 minutes 
= """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ 
 862 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ 
 863 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ 
 866 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' 
 868 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(',') 
 870 state_names 
= ['Alabama','Alaska','Arizona','Arkansas', 
 871                'California','Colorado','Connecticut', 
 872                'Delaware','District of Columbia', 
 873                'Florida','Georgia','Hawaii', 
 874                'Idaho','Illinois','Indiana','Iowa', 
 875                'Kansas','Kentucky','Louisiana', 
 876                'Maine','Maryland','Massachusetts','Michigan', 
 877                'Minnesota','Mississippi','Missouri','Montana', 
 878                'Nebraska','Nevada','New Hampshire','New Jersey', 
 879                'New Mexico','New York','North Carolina','North Dakokta', 
 880                'Ohio','Oklahoma','Oregon', 
 881                'Pennsylvania','Puerto Rico','Rhode Island', 
 882                'South Carolina','South Dakota', 
 883                'Tennessee','Texas','Utah', 
 884                'Vermont','Virginia', 
 885                'Washington','West Virginia', 
 886                'Wisconsin','Wyoming'] 
 888 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 890 ## The following dictionary defines the current set of autoformats: 
 894            'mask': "(###) ###-#### x:###", 
 895            'formatcodes': 'F^->', 
 896            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 897            'description': "Phone Number w/opt. ext" 
 900            'mask': "###-###-#### x:###", 
 901            'formatcodes': 'F^->', 
 902            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 903            'description': "Phone Number\n (w/hyphens and opt. ext)" 
 906            'mask': "(###) ###-####", 
 907            'formatcodes': 'F^->', 
 908            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 909            'description': "Phone Number only" 
 912            'mask': "###-###-####", 
 913            'formatcodes': 'F^->', 
 914            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 915            'description': "Phone Number\n(w/hyphens)" 
 919            'formatcodes': 'F!V', 
 920            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(states
,'|'), 
 922            'choiceRequired': True, 
 923            'description': "US State Code" 
 926            'mask': "ACCCCCCCCCCCCCCCCCCC", 
 928            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(state_names
,'|'), 
 929            'choices': state_names
, 
 930            'choiceRequired': True, 
 931            'description': "US State Name" 
 934        "USDATETIMEMMDDYYYY/HHMMSS": { 
 935            'mask': "##/##/#### ##:##:## AM", 
 936            'excludeChars': am_pm_exclude
, 
 937            'formatcodes': 'DF!', 
 938            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 939            'description': "US Date + Time" 
 941        "USDATETIMEMMDDYYYY-HHMMSS": { 
 942            'mask': "##-##-#### ##:##:## AM", 
 943            'excludeChars': am_pm_exclude
, 
 944            'formatcodes': 'DF!', 
 945            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 946            'description': "US Date + Time\n(w/hypens)" 
 948        "USDATE24HRTIMEMMDDYYYY/HHMMSS": { 
 949            'mask': "##/##/#### ##:##:##", 
 951            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 952            'description': "US Date + 24Hr (Military) Time" 
 954        "USDATE24HRTIMEMMDDYYYY-HHMMSS": { 
 955            'mask': "##-##-#### ##:##:##", 
 957            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 958            'description': "US Date + 24Hr Time\n(w/hypens)" 
 960        "USDATETIMEMMDDYYYY/HHMM": { 
 961            'mask': "##/##/#### ##:## AM", 
 962            'excludeChars': am_pm_exclude
, 
 963            'formatcodes': 'DF!', 
 964            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 965            'description': "US Date + Time\n(without seconds)" 
 967        "USDATE24HRTIMEMMDDYYYY/HHMM": { 
 968            'mask': "##/##/#### ##:##", 
 970            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 971            'description': "US Date + 24Hr Time\n(without seconds)" 
 973        "USDATETIMEMMDDYYYY-HHMM": { 
 974            'mask': "##-##-#### ##:## AM", 
 975            'excludeChars': am_pm_exclude
, 
 976            'formatcodes': 'DF!', 
 977            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 978            'description': "US Date + Time\n(w/hypens and w/o secs)" 
 980        "USDATE24HRTIMEMMDDYYYY-HHMM": { 
 981            'mask': "##-##-#### ##:##", 
 983            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 984            'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" 
 987            'mask': "##/##/####", 
 989            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4}', 
 990            'description': "US Date\n(MMDDYYYY)" 
 995            'validRegex': '^' + months 
+ '/' + days 
+ '/\d\d', 
 996            'description': "US Date\n(MMDDYY)" 
 999            'mask': "##-##-####", 
1000            'formatcodes': 'DF', 
1001            'validRegex': '^' + months 
+ '-' + days 
+ '-' +'\d{4}', 
1002            'description': "MM-DD-YYYY" 
1005        "EUDATEYYYYMMDD/": { 
1006            'mask': "####/##/##", 
1007            'formatcodes': 'DF', 
1008            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days
, 
1009            'description': "YYYY/MM/DD" 
1011        "EUDATEYYYYMMDD.": { 
1012            'mask': "####.##.##", 
1013            'formatcodes': 'DF', 
1014            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days
, 
1015            'description': "YYYY.MM.DD" 
1017        "EUDATEDDMMYYYY/": { 
1018            'mask': "##/##/####", 
1019            'formatcodes': 'DF', 
1020            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4}', 
1021            'description': "DD/MM/YYYY" 
1023        "EUDATEDDMMYYYY.": { 
1024            'mask': "##.##.####", 
1025            'formatcodes': 'DF', 
1026            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4}', 
1027            'description': "DD.MM.YYYY" 
1029        "EUDATEDDMMMYYYY.": { 
1030            'mask': "##.CCC.####", 
1031            'formatcodes': 'DF', 
1032            'validRegex': '^' + days 
+ '.' + charmonths 
+ '.' + '\d{4}', 
1033            'description': "DD.Month.YYYY" 
1035        "EUDATEDDMMMYYYY/": { 
1036            'mask': "##/CCC/####", 
1037            'formatcodes': 'DF', 
1038            'validRegex': '^' + days 
+ '/' + charmonths 
+ '/' + '\d{4}', 
1039            'description': "DD/Month/YYYY" 
1042        "EUDATETIMEYYYYMMDD/HHMMSS": { 
1043            'mask': "####/##/## ##:##:## AM", 
1044            'excludeChars': am_pm_exclude
, 
1045            'formatcodes': 'DF!', 
1046            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1047            'description': "YYYY/MM/DD HH:MM:SS" 
1049        "EUDATETIMEYYYYMMDD.HHMMSS": { 
1050            'mask': "####.##.## ##:##:## AM", 
1051            'excludeChars': am_pm_exclude
, 
1052            'formatcodes': 'DF!', 
1053            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1054            'description': "YYYY.MM.DD HH:MM:SS" 
1056        "EUDATETIMEDDMMYYYY/HHMMSS": { 
1057            'mask': "##/##/#### ##:##:## AM", 
1058            'excludeChars': am_pm_exclude
, 
1059            'formatcodes': 'DF!', 
1060            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1061            'description': "DD/MM/YYYY HH:MM:SS" 
1063        "EUDATETIMEDDMMYYYY.HHMMSS": { 
1064            'mask': "##.##.#### ##:##:## AM", 
1065            'excludeChars': am_pm_exclude
, 
1066            'formatcodes': 'DF!', 
1067            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1068            'description': "DD.MM.YYYY HH:MM:SS" 
1071        "EUDATETIMEYYYYMMDD/HHMM": { 
1072            'mask': "####/##/## ##:## AM", 
1073            'excludeChars': am_pm_exclude
, 
1074            'formatcodes': 'DF!', 
1075            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1076            'description': "YYYY/MM/DD HH:MM" 
1078        "EUDATETIMEYYYYMMDD.HHMM": { 
1079            'mask': "####.##.## ##:## AM", 
1080            'excludeChars': am_pm_exclude
, 
1081            'formatcodes': 'DF!', 
1082            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1083            'description': "YYYY.MM.DD HH:MM" 
1085        "EUDATETIMEDDMMYYYY/HHMM": { 
1086            'mask': "##/##/#### ##:## AM", 
1087            'excludeChars': am_pm_exclude
, 
1088            'formatcodes': 'DF!', 
1089            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1090            'description': "DD/MM/YYYY HH:MM" 
1092        "EUDATETIMEDDMMYYYY.HHMM": { 
1093            'mask': "##.##.#### ##:## AM", 
1094            'excludeChars': am_pm_exclude
, 
1095            'formatcodes': 'DF!', 
1096            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1097            'description': "DD.MM.YYYY HH:MM" 
1100        "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { 
1101            'mask': "####/##/## ##:##:##", 
1102            'formatcodes': 'DF', 
1103            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1104            'description': "YYYY/MM/DD 24Hr Time" 
1106        "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { 
1107            'mask': "####.##.## ##:##:##", 
1108            'formatcodes': 'DF', 
1109            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1110            'description': "YYYY.MM.DD 24Hr Time" 
1112        "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { 
1113            'mask': "##/##/#### ##:##:##", 
1114            'formatcodes': 'DF', 
1115            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1116            'description': "DD/MM/YYYY 24Hr Time" 
1118        "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { 
1119            'mask': "##.##.#### ##:##:##", 
1120            'formatcodes': 'DF', 
1121            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1122            'description': "DD.MM.YYYY 24Hr Time" 
1124        "EUDATE24HRTIMEYYYYMMDD/HHMM": { 
1125            'mask': "####/##/## ##:##", 
1126            'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1127            'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" 
1129        "EUDATE24HRTIMEYYYYMMDD.HHMM": { 
1130            'mask': "####.##.## ##:##", 
1131            'formatcodes': 'DF', 
1132            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1133            'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" 
1135        "EUDATE24HRTIMEDDMMYYYY/HHMM": { 
1136            'mask': "##/##/#### ##:##", 
1137            'formatcodes': 'DF', 
1138            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1139            'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" 
1141        "EUDATE24HRTIMEDDMMYYYY.HHMM": { 
1142            'mask': "##.##.#### ##:##", 
1143            'formatcodes': 'DF', 
1144            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1145            'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" 
1149            'mask': "##:##:## AM", 
1150            'excludeChars': am_pm_exclude
, 
1151            'formatcodes': 'TF!', 
1152            'validRegex': '^' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1153            'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" 
1157            'excludeChars': am_pm_exclude
, 
1158            'formatcodes': 'TF!', 
1159            'validRegex': '^' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1160            'description': "HH:MM (A|P)M\n(see TimeCtrl)" 
1164            'formatcodes': 'TF', 
1165            'validRegex': '^' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1166            'description': "24Hr HH:MM:SS\n(see TimeCtrl)" 
1170            'formatcodes': 'TF', 
1171            'validRegex': '^' + milhours 
+ ':' + minutes
, 
1172            'description': "24Hr HH:MM\n(see TimeCtrl)" 
1175            'mask': "###-##-####", 
1177            'validRegex': "\d{3}-\d{2}-\d{4}", 
1178            'description': "Social Sec#" 
1181            'mask': "####-####-####-####", 
1183            'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", 
1184            'description': "Credit Card" 
1189            'validRegex': "^" + months 
+ "/\d\d", 
1190            'description': "Expiration MM/YY" 
1195            'validRegex': "^\d{5}", 
1196            'description': "US 5-digit zip code" 
1199            'mask': "#####-####", 
1201            'validRegex': "\d{5}-(\s{4}|\d{4})", 
1202            'description': "US zip+4 code" 
1207            'validRegex': "^0.\d\d", 
1208            'description': "Percentage" 
1213            'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]", 
1214            'description': "Age" 
1217            'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 
1218            'excludeChars': " \\/*&%$#!+='\"", 
1219            'formatcodes': "F>", 
1220            '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}\]) *$", 
1221            'description': "Email address" 
1224            'mask': "###.###.###.###", 
1225            'formatcodes': 'F_Sr', 
1226            '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}", 
1227            'description': "IP Address\n(see IpAddrCtrl)" 
1231 # build demo-friendly dictionary of descriptions of autoformats 
1233 for key
, value 
in masktags
.items(): 
1234     autoformats
.append((key
, value
['description'])) 
1237 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1241     This class manages the individual fields in a masked edit control. 
1242     Each field has a zero-based index, indicating its position in the 
1243     control, an extent, an associated mask, and a plethora of optional 
1244     parameters.  Fields can be instantiated and then associated with 
1245     parent masked controls, in order to provide field-specific configuration. 
1246     Alternatively, fields will be implicitly created by the parent control 
1247     if not provided at construction, at which point, the fields can then 
1248     manipulated by the controls .SetFieldParameters() method. 
1251               'index': None,                    ## which field of mask; set by parent control. 
1252               'mask': "",                       ## mask chars for this field 
1253               'extent': (),                     ## (edit start, edit_end) of field; set by parent control. 
1254               'formatcodes':  "",               ## codes indicating formatting options for the control 
1255               'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given 
1256               'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits 
1257               'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction 
1258               'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard 
1259               'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. 
1260               'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar 
1261               'excludeChars': "",               ## optional string of chars to exclude even if main mask type does 
1262               'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't 
1263               'validRegex':   "",               ## optional regular expression to use to validate the control 
1264               'validRange':   (),               ## Optional hi-low range for numerics 
1265               'choices':    [],                 ## Optional list for character expressions 
1266               'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list 
1267               'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search 
1268               'autoSelect': False,              ## Set to True to try auto-completion on each keystroke: 
1269               'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl 
1270               'validRequired': False,           ## Set to True to disallow input that results in an invalid value 
1271               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID 
1272               'description': "",                ## primarily for autoformats, but could be useful elsewhere 
1275     # This list contains all parameters that when set at the control level should 
1276     # propagate down to each field: 
1277     propagating_params 
= ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', 
1278                           'compareNoCase', 'emptyInvalid', 'validRequired') 
1280     def __init__(self
, **kwargs
): 
1282         This is the "constructor" for setting up parameters for fields. 
1283         a field_index of -1 is used to indicate "the entire control." 
1285 ####        dbg('Field::Field', indent=1) 
1286         # Validate legitimate set of parameters: 
1287         for key 
in kwargs
.keys(): 
1288             if key 
not in Field
.valid_params
.keys(): 
1290                 raise TypeError('invalid parameter "%s"' % (key
)) 
1292         # Set defaults for each parameter for this instance, and fully 
1293         # populate initial parameter list for configuration: 
1294         for key
, value 
in Field
.valid_params
.items(): 
1295             setattr(self
, '_' + key
, copy
.copy(value
)) 
1296             if not kwargs
.has_key(key
): 
1297                 kwargs
[key
] = copy
.copy(value
) 
1299         self
._autoCompleteIndex 
= -1 
1300         self
._SetParameters
(**kwargs
) 
1301         self
._ValidateParameters
(**kwargs
) 
1306     def _SetParameters(self
, **kwargs
): 
1308         This function can be used to set individual or multiple parameters for 
1309         a masked edit field parameter after construction. 
1312 ##        dbg('maskededit.Field::_SetParameters', indent=1) 
1313         # Validate keyword arguments: 
1314         for key 
in kwargs
.keys(): 
1315             if key 
not in Field
.valid_params
.keys(): 
1316 ##                dbg(indent=0, suspend=0) 
1317                 raise AttributeError('invalid keyword argument "%s"' % key
) 
1319 ##        if self._index is not None: dbg('field index:', self._index) 
1320 ##        dbg('parameters:', indent=1) 
1321         for key
, value 
in kwargs
.items(): 
1322 ##            dbg('%s:' % key, value) 
1327         old_fillChar 
= self
._fillChar   
# store so we can change choice lists accordingly if it changes 
1329         # First, Assign all parameters specified: 
1330         for key 
in Field
.valid_params
.keys(): 
1331             if kwargs
.has_key(key
): 
1332                 setattr(self
, '_' + key
, kwargs
[key
] ) 
1334         if kwargs
.has_key('formatcodes'):   # (set/changed) 
1335             self
._forceupper  
= '!' in self
._formatcodes
 
1336             self
._forcelower  
= '^' in self
._formatcodes
 
1337             self
._groupdigits 
= ',' in self
._formatcodes
 
1338             self
._okSpaces    
= '_' in self
._formatcodes
 
1339             self
._padZero     
= '0' in self
._formatcodes
 
1340             self
._autofit     
= 'F' in self
._formatcodes
 
1341             self
._insertRight 
= 'r' in self
._formatcodes
 
1342             self
._allowInsert 
= '>' in self
._formatcodes
 
1343             self
._alignRight  
= 'R' in self
._formatcodes 
or 'r' in self
._formatcodes
 
1344             self
._moveOnFieldFull 
= not '<' in self
._formatcodes
 
1345             self
._selectOnFieldEntry 
= 'S' in self
._formatcodes
 
1347             if kwargs
.has_key('groupChar'): 
1348                 self
._groupChar 
= kwargs
['groupChar'] 
1349             if kwargs
.has_key('decimalChar'): 
1350                 self
._decimalChar 
= kwargs
['decimalChar'] 
1351             if kwargs
.has_key('shiftDecimalChar'): 
1352                 self
._shiftDecimalChar 
= kwargs
['shiftDecimalChar'] 
1354         if kwargs
.has_key('formatcodes') or kwargs
.has_key('validRegex'): 
1355             self
._regexMask   
= 'V' in self
._formatcodes 
and self
._validRegex
 
1357         if kwargs
.has_key('fillChar'): 
1358             self
._old
_fillChar 
= old_fillChar
 
1359 ####            dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1361         if kwargs
.has_key('mask') or kwargs
.has_key('validRegex'):  # (set/changed) 
1362             self
._isInt 
= _isInteger(self
._mask
) 
1363 ##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) 
1365 ##        dbg(indent=0, suspend=0) 
1368     def _ValidateParameters(self
, **kwargs
): 
1370         This function can be used to validate individual or multiple parameters for 
1371         a masked edit field parameter after construction. 
1374 ##        dbg('maskededit.Field::_ValidateParameters', indent=1) 
1375 ##        if self._index is not None: dbg('field index:', self._index) 
1376 ####        dbg('parameters:', indent=1) 
1377 ##        for key, value in kwargs.items(): 
1378 ####            dbg('%s:' % key, value) 
1380 ####        dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1382         # Verify proper numeric format params: 
1383         if self
._groupdigits 
and self
._groupChar 
== self
._decimalChar
: 
1384 ##            dbg(indent=0, suspend=0) 
1385             raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self
._groupChar
, self
._decimalChar
)) 
1388         # Now go do validation, semantic and inter-dependency parameter processing: 
1389         if kwargs
.has_key('choices') or kwargs
.has_key('compareNoCase') or kwargs
.has_key('choiceRequired'): # (set/changed) 
1391             self
._compareChoices 
= [choice
.strip() for choice 
in self
._choices
] 
1393             if self
._compareNoCase 
and self
._choices
: 
1394                 self
._compareChoices 
= [item
.lower() for item 
in self
._compareChoices
] 
1396             if kwargs
.has_key('choices'): 
1397                 self
._autoCompleteIndex 
= -1 
1400         if kwargs
.has_key('validRegex'):    # (set/changed) 
1401             if self
._validRegex
: 
1403                     if self
._compareNoCase
: 
1404                         self
._filter 
= re
.compile(self
._validRegex
, re
.IGNORECASE
) 
1406                         self
._filter 
= re
.compile(self
._validRegex
) 
1408 ##                    dbg(indent=0, suspend=0) 
1409                     raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self
._index
), self
._validRegex
)) 
1413         if kwargs
.has_key('validRange'):    # (set/changed) 
1414             self
._hasRange  
= False 
1417             if self
._validRange
: 
1418                 if type(self
._validRange
) != types
.TupleType 
or len( self
._validRange 
)!= 2 or self
._validRange
[0] > self
._validRange
[1]: 
1419 ##                    dbg(indent=0, suspend=0) 
1420                     raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' 
1421                                     % (str(self
._index
), repr(self
._validRange
)) ) 
1423                 self
._hasRange  
= True 
1424                 self
._rangeLow  
= self
._validRange
[0] 
1425                 self
._rangeHigh 
= self
._validRange
[1] 
1427         if kwargs
.has_key('choices') or (len(self
._choices
) and len(self
._choices
[0]) != len(self
._mask
)):       # (set/changed) 
1428             self
._hasList   
= False 
1429             if self
._choices 
and type(self
._choices
) not in (types
.TupleType
, types
.ListType
): 
1430 ##                dbg(indent=0, suspend=0) 
1431                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1432             elif len( self
._choices
) > 0: 
1433                 for choice 
in self
._choices
: 
1434                     if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
1435 ##                        dbg(indent=0, suspend=0) 
1436                         raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1438                 length 
= len(self
._mask
) 
1439 ##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) 
1440                 if len(self
._choices
) and length
: 
1441                     if len(self
._choices
[0]) > length
: 
1442                         # changed mask without respecifying choices; readjust the width as appropriate: 
1443                         self
._choices 
= [choice
.strip() for choice 
in self
._choices
] 
1444                     if self
._alignRight
: 
1445                         self
._choices 
= [choice
.rjust( length 
) for choice 
in self
._choices
] 
1447                         self
._choices 
= [choice
.ljust( length 
) for choice 
in self
._choices
] 
1448 ##                    dbg('aligned choices:', self._choices) 
1450                 if hasattr(self
, '_template'): 
1451                     # Verify each choice specified is valid: 
1452                     for choice 
in self
._choices
: 
1453                         if self
.IsEmpty(choice
) and not self
._validRequired
: 
1454                             # allow empty values even if invalid, (just colored differently) 
1456                         if not self
.IsValid(choice
): 
1457 ##                            dbg(indent=0, suspend=0) 
1458                             raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
1459                 self
._hasList 
= True 
1461 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) 
1462 ####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) 
1463         if kwargs
.has_key('fillChar') and len(self
._choices
) > 0: 
1464             if kwargs
['fillChar'] != ' ': 
1465                 self
._choices 
= [choice
.replace(' ', self
._fillChar
) for choice 
in self
._choices
] 
1467                 self
._choices 
= [choice
.replace(self
._old
_fillChar
, self
._fillChar
) for choice 
in self
._choices
] 
1468 ##            dbg('updated choices:', self._choices) 
1471         if kwargs
.has_key('autoSelect') and kwargs
['autoSelect']: 
1472             if not self
._hasList
: 
1473 ##                dbg('no list to auto complete; ignoring "autoSelect=True"') 
1474                 self
._autoSelect 
= False 
1476         # reset field validity assumption: 
1478 ##        dbg(indent=0, suspend=0) 
1481     def _GetParameter(self
, paramname
): 
1483         Routine for retrieving the value of any given parameter 
1485         if Field
.valid_params
.has_key(paramname
): 
1486             return getattr(self
, '_' + paramname
) 
1488             TypeError('Field._GetParameter: invalid parameter "%s"' % key
) 
1491     def IsEmpty(self
, slice): 
1493         Indicates whether the specified slice is considered empty for the 
1496 ##        dbg('Field::IsEmpty("%s")' % slice, indent=1) 
1497         if not hasattr(self
, '_template'): 
1499             raise AttributeError('_template') 
1501 ##        dbg('self._template: "%s"' % self._template) 
1502 ##        dbg('self._defaultValue: "%s"' % str(self._defaultValue)) 
1503         if slice == self
._template 
and not self
._defaultValue
: 
1507         elif slice == self
._template
: 
1509             for pos 
in range(len(self
._template
)): 
1510 ####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) 
1511                 if slice[pos
] not in (' ', self
._fillChar
): 
1514 ##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) 
1517 ##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) 
1521     def IsValid(self
, slice): 
1523         Indicates whether the specified slice is considered a valid value for the 
1527 ##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) 
1528         valid 
= True    # assume true to start 
1530         if self
.IsEmpty(slice): 
1531 ##            dbg(indent=0, suspend=0) 
1532             if self
._emptyInvalid
: 
1537         elif self
._hasList 
and self
._choiceRequired
: 
1538 ##            dbg("(member of list required)") 
1539             # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): 
1540             if self
._fillChar 
!= ' ': 
1541                 slice = slice.replace(self
._fillChar
, ' ') 
1542 ##                dbg('updated slice:"%s"' % slice) 
1543             compareStr 
= slice.strip() 
1545             if self
._compareNoCase
: 
1546                 compareStr 
= compareStr
.lower() 
1547             valid 
= compareStr 
in self
._compareChoices
 
1549         elif self
._hasRange 
and not self
.IsEmpty(slice): 
1550 ##            dbg('validating against range') 
1552                 # allow float as well as int ranges (int comparisons for free.) 
1553                 valid 
= self
._rangeLow 
<= float(slice) <= self
._rangeHigh
 
1557         elif self
._validRegex 
and self
._filter
: 
1558 ##            dbg('validating against regex') 
1559             valid 
= (re
.match( self
._filter
, slice) is not None) 
1561         if valid 
and self
._validFunc
: 
1562 ##            dbg('validating against supplied function') 
1563             valid 
= self
._validFunc
(slice) 
1564 ##        dbg('valid?', valid, indent=0, suspend=0) 
1568     def _AdjustField(self
, slice): 
1569         """ 'Fixes' an integer field. Right or left-justifies, as required.""" 
1570 ##        dbg('Field::_AdjustField("%s")' % slice, indent=1) 
1571         length 
= len(self
._mask
) 
1572 ####        dbg('length(self._mask):', length) 
1573 ####        dbg('self._useParensForNegatives?', self._useParensForNegatives) 
1575             if self
._useParensForNegatives
: 
1576                 signpos 
= slice.find('(') 
1577                 right_signpos 
= slice.find(')') 
1578                 intStr 
= slice.replace('(', '').replace(')', '')    # drop sign, if any 
1580                 signpos 
= slice.find('-') 
1581                 intStr 
= slice.replace( '-', '' )                   # drop sign, if any 
1584             intStr 
= intStr
.replace(' ', '')                        # drop extra spaces 
1585             intStr 
= string
.replace(intStr
,self
._fillChar
,"")       # drop extra fillchars 
1586             intStr 
= string
.replace(intStr
,"-","")                  # drop sign, if any 
1587             intStr 
= string
.replace(intStr
, self
._groupChar
, "")    # lose commas/dots 
1588 ####            dbg('intStr:"%s"' % intStr) 
1589             start
, end 
= self
._extent
 
1590             field_len 
= end 
- start
 
1591             if not self
._padZero 
and len(intStr
) != field_len 
and intStr
.strip(): 
1592                 intStr 
= str(long(intStr
)) 
1593 ####            dbg('raw int str: "%s"' % intStr) 
1594 ####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) 
1595             if self
._groupdigits
: 
1598                 for i 
in range(len(intStr
)-1, -1, -1): 
1599                     new 
= intStr
[i
] + new
 
1601                         new 
= self
._groupChar 
+ new
 
1603                 if new 
and new
[0] == self
._groupChar
: 
1605                 if len(new
) <= length
: 
1606                     # expanded string will still fit and leave room for sign: 
1608                 # else... leave it without the commas... 
1610 ##            dbg('padzero?', self._padZero) 
1611 ##            dbg('len(intStr):', len(intStr), 'field length:', length) 
1612             if self
._padZero 
and len(intStr
) < length
: 
1613                 intStr 
= '0' * (length 
- len(intStr
)) + intStr
 
1614                 if signpos 
!= -1:   # we had a sign before; restore it 
1615                     if self
._useParensForNegatives
: 
1616                         intStr 
= '(' + intStr
[1:] 
1617                         if right_signpos 
!= -1: 
1620                         intStr 
= '-' + intStr
[1:] 
1621             elif signpos 
!= -1 and slice[0:signpos
].strip() == '':    # - was before digits 
1622                 if self
._useParensForNegatives
: 
1623                     intStr 
= '(' + intStr
 
1624                     if right_signpos 
!= -1: 
1627                     intStr 
= '-' + intStr
 
1628             elif right_signpos 
!= -1: 
1629                 # must have had ')' but '(' was before field; re-add ')' 
1633         slice = slice.strip() # drop extra spaces 
1635         if self
._alignRight
:     ## Only if right-alignment is enabled 
1636             slice = slice.rjust( length 
) 
1638             slice = slice.ljust( length 
) 
1639         if self
._fillChar 
!= ' ': 
1640             slice = slice.replace(' ', self
._fillChar
) 
1641 ##        dbg('adjusted slice: "%s"' % slice, indent=0) 
1645 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1647 class MaskedEditMixin
: 
1649     This class allows us to abstract the masked edit functionality that could 
1650     be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.) 
1651     It forms the basis for all of the lib.masked controls. 
1653     valid_ctrl_params 
= { 
1654               'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control 
1655               'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary 
1656               'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask 
1657               'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year 
1658               'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete 
1659               'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control 
1660               'defaultEncoding': 'latin1',      ## optional argument to indicate unicode codec to use (unicode ctrls only) 
1661               'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently, 
1662                                                 ## from field validation constraints 
1663               'emptyBackgroundColour': "White", 
1664               'validBackgroundColour': "White", 
1665               'invalidBackgroundColour': "Yellow", 
1666               'foregroundColour': "Black", 
1667               'signedForegroundColour': "Red", 
1671     def __init__(self
, name 
= 'MaskedEdit', **kwargs
): 
1673         This is the "constructor" for setting up the mixin variable parameters for the composite class. 
1678         # set up flag for doing optional things to base control if possible 
1679         if not hasattr(self
, 'controlInitialized'): 
1680             self
.controlInitialized 
= False 
1682         # Set internal state var for keeping track of whether or not a character 
1683         # action results in a modification of the control, since .SetValue() 
1684         # doesn't modify the base control's internal state: 
1685         self
.modified 
= False 
1686         self
._previous
_mask 
= None 
1688         # Validate legitimate set of parameters: 
1689         for key 
in kwargs
.keys(): 
1690             if key
.replace('Color', 'Colour') not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1691                 raise TypeError('%s: invalid parameter "%s"' % (name
, key
)) 
1693         ## Set up dictionary that can be used by subclasses to override or add to default 
1694         ## behavior for individual characters.  Derived subclasses needing to change 
1695         ## default behavior for keys can either redefine the default functions for the 
1696         ## common keys or add functions for specific keys to this list.  Each function 
1697         ## added should take the key event as argument, and return False if the key 
1698         ## requires no further processing. 
1700         ## Initially populated with navigation and function control keys: 
1701         self
._keyhandlers 
= { 
1702             # default navigation keys and handlers: 
1703             wx
.WXK_BACK
:   self
._OnErase
, 
1704             wx
.WXK_LEFT
:   self
._OnArrow
, 
1705             wx
.WXK_RIGHT
:  self
._OnArrow
, 
1706             wx
.WXK_UP
:     self
._OnAutoCompleteField
, 
1707             wx
.WXK_DOWN
:   self
._OnAutoCompleteField
, 
1708             wx
.WXK_TAB
:    self
._OnChangeField
, 
1709             wx
.WXK_HOME
:   self
._OnHome
, 
1710             wx
.WXK_END
:    self
._OnEnd
, 
1711             wx
.WXK_RETURN
: self
._OnReturn
, 
1712             wx
.WXK_PRIOR
:  self
._OnAutoCompleteField
, 
1713             wx
.WXK_NEXT
:   self
._OnAutoCompleteField
, 
1715             # default function control keys and handlers: 
1716             wx
.WXK_DELETE
: self
._OnErase
, 
1717             WXK_CTRL_A
: self
._OnCtrl
_A
, 
1718             WXK_CTRL_C
: self
._OnCtrl
_C
, 
1719             WXK_CTRL_S
: self
._OnCtrl
_S
, 
1720             WXK_CTRL_V
: self
._OnCtrl
_V
, 
1721             WXK_CTRL_X
: self
._OnCtrl
_X
, 
1722             WXK_CTRL_Z
: self
._OnCtrl
_Z
, 
1725         ## bind standard navigational and control keycodes to this instance, 
1726         ## so that they can be augmented and/or changed in derived classes: 
1727         self
._nav 
= list(nav
) 
1728         self
._control 
= list(control
) 
1730         ## Dynamically evaluate and store string constants for mask chars 
1731         ## so that locale settings can be made after this module is imported 
1732         ## and the controls created after that is done can allow the 
1733         ## appropriate characters: 
1734         self
.maskchardict  
= { 
1736             'A': string
.uppercase
, 
1737             'a': string
.lowercase
, 
1738             'X': string
.letters 
+ string
.punctuation 
+ string
.digits
, 
1739             'C': string
.letters
, 
1740             'N': string
.letters 
+ string
.digits
, 
1741             '&': string
.punctuation
, 
1745         ## self._ignoreChange is used by MaskedComboBox, because 
1746         ## of the hack necessary to determine the selection; it causes 
1747         ## EVT_TEXT messages from the combobox to be ignored if set. 
1748         self
._ignoreChange 
= False 
1750         # These are used to keep track of previous value, for undo functionality: 
1751         self
._curValue  
= None 
1752         self
._prevValue 
= None 
1756         # Set defaults for each parameter for this instance, and fully 
1757         # populate initial parameter list for configuration: 
1758         for key
, value 
in MaskedEditMixin
.valid_ctrl_params
.items(): 
1759             setattr(self
, '_' + key
, copy
.copy(value
)) 
1760             if not kwargs
.has_key(key
): 
1761 ####                dbg('%s: "%s"' % (key, repr(value))) 
1762                 kwargs
[key
] = copy
.copy(value
) 
1764         # Create a "field" that holds global parameters for control constraints 
1765         self
._ctrl
_constraints 
= self
._fields
[-1] = Field(index
=-1) 
1766         self
.SetCtrlParameters(**kwargs
) 
1770     def SetCtrlParameters(self
, **kwargs
): 
1772         This public function can be used to set individual or multiple masked edit 
1773         parameters after construction.  (See maskededit module overview for the list 
1774         of valid parameters.) 
1777 ##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1) 
1778 ####        dbg('kwargs:', indent=1) 
1779 ##        for key, value in kwargs.items(): 
1780 ####            dbg(key, '=', value) 
1783         # Validate keyword arguments: 
1784         constraint_kwargs 
= {} 
1786         for key
, value 
in kwargs
.items(): 
1787             key 
= key
.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling 
1788             if key 
not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1789 ##                dbg(indent=0, suspend=0) 
1790                 raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key
, self
.name
)) 
1791             elif key 
in Field
.valid_params
.keys(): 
1792                 constraint_kwargs
[key
] = value
 
1794                 ctrl_kwargs
[key
] = value
 
1799         if ctrl_kwargs
.has_key('autoformat'): 
1800             autoformat 
= ctrl_kwargs
['autoformat'] 
1804         # handle "parochial name" backward compatibility: 
1805         if autoformat 
and autoformat
.find('MILTIME') != -1 and autoformat 
not in masktags
.keys(): 
1806             autoformat 
= autoformat
.replace('MILTIME', '24HRTIME') 
1808         if autoformat 
!= self
._autoformat 
and autoformat 
in masktags
.keys(): 
1809 ##            dbg('autoformat:', autoformat) 
1810             self
._autoformat                  
= autoformat
 
1811             mask                              
= masktags
[self
._autoformat
]['mask'] 
1812             # gather rest of any autoformat parameters: 
1813             for param
, value 
in masktags
[self
._autoformat
].items(): 
1814                 if param 
== 'mask': continue    # (must be present; already accounted for) 
1815                 constraint_kwargs
[param
] = value
 
1817         elif autoformat 
and not autoformat 
in masktags
.keys(): 
1818             raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat
)) 
1820 ##            dbg('autoformat not selected') 
1821             if kwargs
.has_key('mask'): 
1822                 mask 
= kwargs
['mask'] 
1823 ##                dbg('mask:', mask) 
1825         ## Assign style flags 
1827 ##            dbg('preserving previous mask') 
1828             mask 
= self
._previous
_mask   
# preserve previous mask 
1830 ##            dbg('mask (re)set') 
1831             reset_args
['reset_mask'] = mask
 
1832             constraint_kwargs
['mask'] = mask
 
1834             # wipe out previous fields; preserve new control-level constraints 
1835             self
._fields 
= {-1: self._ctrl_constraints}
 
1838         if ctrl_kwargs
.has_key('fields'): 
1839             # do field parameter type validation, and conversion to internal dictionary 
1841             fields 
= ctrl_kwargs
['fields'] 
1842             if type(fields
) in (types
.ListType
, types
.TupleType
): 
1843                 for i 
in range(len(fields
)): 
1845                     if not isinstance(field
, Field
): 
1846 ##                        dbg(indent=0, suspend=0) 
1847                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1848                     self
._fields
[i
] = field
 
1850             elif type(fields
) == types
.DictionaryType
: 
1851                 for index
, field 
in fields
.items(): 
1852                     if not isinstance(field
, Field
): 
1853 ##                        dbg(indent=0, suspend=0) 
1854                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1855                     self
._fields
[index
] = field
 
1857 ##                dbg(indent=0, suspend=0) 
1858                 raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields
)) 
1860         # Assign constraint parameters for entire control: 
1861 ####        dbg('control constraints:', indent=1) 
1862 ##        for key, value in constraint_kwargs.items(): 
1863 ####            dbg('%s:' % key, value) 
1866         # determine if changing parameters that should affect the entire control: 
1867         for key 
in MaskedEditMixin
.valid_ctrl_params
.keys(): 
1868             if key 
in ( 'mask', 'fields' ): continue    # (processed separately) 
1869             if ctrl_kwargs
.has_key(key
): 
1870                 setattr(self
, '_' + key
, ctrl_kwargs
[key
]) 
1872         # Validate color parameters, converting strings to named colors and validating 
1873         # result if appropriate: 
1874         for key 
in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', 
1875                     'foregroundColour', 'signedForegroundColour'): 
1876             if ctrl_kwargs
.has_key(key
): 
1877                 if type(ctrl_kwargs
[key
]) in (types
.StringType
, types
.UnicodeType
): 
1878                     c 
= wx
.NamedColour(ctrl_kwargs
[key
]) 
1879                     if c
.Get() == (-1, -1, -1): 
1880                         raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1882                         # replace attribute with wxColour object: 
1883                         setattr(self
, '_' + key
, c
) 
1884                         # attach a python dynamic attribute to wxColour for debug printouts 
1885                         c
._name 
= ctrl_kwargs
[key
] 
1887                 elif type(ctrl_kwargs
[key
]) != type(wx
.BLACK
): 
1888                     raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1891 ##        dbg('self._retainFieldValidation:', self._retainFieldValidation) 
1892         if not self
._retainFieldValidation
: 
1893             # Build dictionary of any changing parameters which should be propagated to the 
1895             for arg 
in Field
.propagating_params
: 
1896 ####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) 
1897 ####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) 
1898                 reset_args
[arg
] = kwargs
.has_key(arg
) and kwargs
[arg
] != getattr(self
._ctrl
_constraints
, '_'+arg
) 
1899 ####                dbg('reset_args[%s]?' % arg, reset_args[arg]) 
1901         # Set the control-level constraints: 
1902         self
._ctrl
_constraints
._SetParameters
(**constraint_kwargs
) 
1904         # This routine does the bulk of the interdependent parameter processing, determining 
1905         # the field extents of the mask if changed, resetting parameters as appropriate, 
1906         # determining the overall template value for the control, etc. 
1907         self
._configure
(mask
, **reset_args
) 
1909         # now that we've propagated the field constraints and mask portions to the 
1910         # various fields, validate the constraints 
1911         self
._ctrl
_constraints
._ValidateParameters
(**constraint_kwargs
) 
1913         # Validate that all choices for given fields are at least of the 
1914         # necessary length, and that they all would be valid pastes if pasted 
1915         # into their respective fields: 
1916 ####        dbg('validating choices') 
1917         self
._validateChoices
() 
1920         self
._autofit 
= self
._ctrl
_constraints
._autofit
 
1923         self
._isDate     
= 'D' in self
._ctrl
_constraints
._formatcodes 
and _isDateType(mask
) 
1924         self
._isTime     
= 'T' in self
._ctrl
_constraints
._formatcodes 
and _isTimeType(mask
) 
1926             # Set _dateExtent, used in date validation to locate date in string; 
1927             # always set as though year will be 4 digits, even if mask only has 
1928             # 2 digits, so we can always properly process the intended year for 
1929             # date validation (leap years, etc.) 
1930             if self
._mask
.find('CCC') != -1: self
._dateExtent 
= 11 
1931             else:                            self
._dateExtent 
= 10 
1933             self
._4digityear 
= len(self
._mask
) > 8 and self
._mask
[9] == '#' 
1935         if self
._isDate 
and self
._autoformat
: 
1936             # Auto-decide datestyle: 
1937             if self
._autoformat
.find('MDDY')    != -1: self
._datestyle 
= 'MDY' 
1938             elif self
._autoformat
.find('YMMD')  != -1: self
._datestyle 
= 'YMD' 
1939             elif self
._autoformat
.find('YMMMD') != -1: self
._datestyle 
= 'YMD' 
1940             elif self
._autoformat
.find('DMMY')  != -1: self
._datestyle 
= 'DMY' 
1941             elif self
._autoformat
.find('DMMMY') != -1: self
._datestyle 
= 'DMY' 
1943         # Give derived controls a chance to react to parameter changes before 
1944         # potentially changing current value of the control. 
1945         self
._OnCtrlParametersChanged
() 
1947         if self
.controlInitialized
: 
1948             # Then the base control is available for configuration; 
1949             # take action on base control based on new settings, as appropriate. 
1950             if kwargs
.has_key('useFixedWidthFont'): 
1951                 # Set control font - fixed width by default 
1954             if reset_args
.has_key('reset_mask'): 
1955 ##                dbg('reset mask') 
1956                 curvalue 
= self
._GetValue
() 
1957                 if curvalue
.strip(): 
1959 ##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) 
1960                         self
._SetInitialValue
(self
._GetValue
()) 
1961                     except Exception, e
: 
1962 ##                        dbg('exception caught:', e) 
1963 ##                        dbg("current value doesn't work; attempting to reset to template") 
1964                         self
._SetInitialValue
() 
1966 ##                    dbg('attempting to _SetInitialValue() with template') 
1967                     self
._SetInitialValue
() 
1969             elif kwargs
.has_key('useParensForNegatives'): 
1970                 newvalue 
= self
._getSignedValue
()[0] 
1972                 if newvalue 
is not None: 
1973                     # Adjust for new mask: 
1974                     if len(newvalue
) < len(self
._mask
): 
1976                     elif len(newvalue
) > len(self
._mask
): 
1977                         if newvalue
[-1] in (' ', ')'): 
1978                             newvalue 
= newvalue
[:-1] 
1980 ##                    dbg('reconfiguring value for parens:"%s"' % newvalue) 
1981                     self
._SetValue
(newvalue
) 
1983                     if self
._prevValue 
!= newvalue
: 
1984                         self
._prevValue 
= newvalue  
# disallow undo of sign type 
1987 ##                dbg('calculated size:', self._CalcSize()) 
1988                 self
.SetClientSize(self
._CalcSize
()) 
1989                 width 
= self
.GetSize().width
 
1990                 height 
= self
.GetBestSize().height
 
1991 ##                dbg('setting client size to:', (width, height)) 
1992                 self
.SetBestFittingSize((width
, height
)) 
1994             # Set value/type-specific formatting 
1995             self
._applyFormatting
() 
1996 ##        dbg(indent=0, suspend=0) 
1998     def SetMaskParameters(self
, **kwargs
): 
1999         """ old name for the SetCtrlParameters function  (DEPRECATED)""" 
2000         return self
.SetCtrlParameters(**kwargs
) 
2003     def GetCtrlParameter(self
, paramname
): 
2005         Routine for retrieving the value of any given parameter 
2007         if MaskedEditMixin
.valid_ctrl_params
.has_key(paramname
.replace('Color','Colour')): 
2008             return getattr(self
, '_' + paramname
.replace('Color', 'Colour')) 
2009         elif Field
.valid_params
.has_key(paramname
): 
2010             return self
._ctrl
_constraints
._GetParameter
(paramname
) 
2012             TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2014     def GetMaskParameter(self
, paramname
): 
2015         """ old name for the GetCtrlParameters function  (DEPRECATED)""" 
2016         return self
.GetCtrlParameter(paramname
) 
2019 ## This idea worked, but Boa was unable to use this solution... 
2020 ##    def _attachMethod(self, func): 
2022 ##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) 
2025 ##    def _DefinePropertyFunctions(exposed_params): 
2026 ##        for param in exposed_params: 
2027 ##            propname = param[0].upper() + param[1:] 
2029 ##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2030 ##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2031 ##            self._attachMethod(locals()['Set%s' % propname]) 
2032 ##            self._attachMethod(locals()['Get%s' % propname]) 
2034 ##            if param.find('Colour') != -1: 
2035 ##                # add non-british spellings, for backward-compatibility 
2036 ##                propname.replace('Colour', 'Color') 
2038 ##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
2039 ##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
2040 ##                self._attachMethod(locals()['Set%s' % propname]) 
2041 ##                self._attachMethod(locals()['Get%s' % propname]) 
2045     def SetFieldParameters(self
, field_index
, **kwargs
): 
2047         Routine provided to modify the parameters of a given field. 
2048         Because changes to fields can affect the overall control, 
2049         direct access to the fields is prevented, and the control 
2050         is always "reconfigured" after setting a field parameter. 
2051         (See maskededit module overview for the list of valid field-level 
2054         if field_index 
not in self
._field
_indices
: 
2055             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2056         # set parameters as requested: 
2057         self
._fields
[field_index
]._SetParameters
(**kwargs
) 
2059         # Possibly reprogram control template due to resulting changes, and ensure 
2060         # control-level params are still propagated to fields: 
2061         self
._configure
(self
._previous
_mask
) 
2062         self
._fields
[field_index
]._ValidateParameters
(**kwargs
) 
2064         if self
.controlInitialized
: 
2065             if kwargs
.has_key('fillChar') or kwargs
.has_key('defaultValue'): 
2066                 self
._SetInitialValue
() 
2069                     # this is tricky, because, as Robin explains: 
2070                     # "Basically there are two sizes to deal with, that are potentially  
2071                     #  different.  The client size is the inside size and may, depending 
2072                     #  on platform, exclude the borders and such.  The normal size is 
2073                     #  the outside size that does include the borders.  What you are 
2074                     #  calculating (in _CalcSize) is the client size, but the sizers 
2075                     #  deal with the full size and so that is the minimum size that 
2076                     #  we need to set with SetBestFittingSize.  The root of the problem is 
2077                     #  that in _calcSize the current client size height is returned, 
2078                     #  instead of a height based on the current font.  So I suggest using 
2079                     #  _calcSize to just get the width, and then use GetBestSize to 
2081                     self
.SetClientSize(self
._CalcSize
()) 
2082                     width 
= self
.GetSize().width
 
2083                     height 
= self
.GetBestSize().height
 
2084                     self
.SetBestFittingSize((width
, height
)) 
2087             # Set value/type-specific formatting 
2088             self
._applyFormatting
() 
2091     def GetFieldParameter(self
, field_index
, paramname
): 
2093         Routine provided for getting a parameter of an individual field. 
2095         if field_index 
not in self
._field
_indices
: 
2096             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2097         elif Field
.valid_params
.has_key(paramname
): 
2098             return self
._fields
[field_index
]._GetParameter
(paramname
) 
2100             TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2103     def _SetKeycodeHandler(self
, keycode
, func
): 
2105         This function adds and/or replaces key event handling functions 
2106         used by the control.  <func> should take the event as argument 
2107         and return False if no further action on the key is necessary. 
2109         self
._keyhandlers
[keycode
] = func
 
2112     def _SetKeyHandler(self
, char
, func
): 
2114         This function adds and/or replaces key event handling functions 
2115         for ascii characters.  <func> should take the event as argument 
2116         and return False if no further action on the key is necessary. 
2118         self
._SetKeycodeHandler
(ord(char
), func
) 
2121     def _AddNavKeycode(self
, keycode
, handler
=None): 
2123         This function allows a derived subclass to augment the list of 
2124         keycodes that are considered "navigational" keys. 
2126         self
._nav
.append(keycode
) 
2128             self
._keyhandlers
[keycode
] = handler
 
2131     def _AddNavKey(self
, char
, handler
=None): 
2133         This function is a convenience function so you don't have to 
2134         remember to call ord() for ascii chars to be used for navigation. 
2136         self
._AddNavKeycode
(ord(char
), handler
) 
2139     def _GetNavKeycodes(self
): 
2141         This function retrieves the current list of navigational keycodes for 
2147     def _SetNavKeycodes(self
, keycode_func_tuples
): 
2149         This function allows you to replace the current list of keycode processed 
2150         as navigation keys, and bind associated optional keyhandlers. 
2153         for keycode
, func 
in keycode_func_tuples
: 
2154             self
._nav
.append(keycode
) 
2156                 self
._keyhandlers
[keycode
] = func
 
2159     def _processMask(self
, mask
): 
2161         This subroutine expands {n} syntax in mask strings, and looks for escaped 
2162         special characters and returns the expanded mask, and an dictionary 
2163         of booleans indicating whether or not a given position in the mask is 
2164         a mask character or not. 
2166 ##        dbg('_processMask: mask', mask, indent=1) 
2167         # regular expression for parsing c{n} syntax: 
2168         rex 
= re
.compile('([' +string
.join(maskchars
,"") + '])\{(\d+)\}') 
2170         match 
= rex
.search(s
) 
2171         while match
:    # found an(other) occurrence 
2172             maskchr 
= s
[match
.start(1):match
.end(1)]            # char to be repeated 
2173             repcount 
= int(s
[match
.start(2):match
.end(2)])      # the number of times 
2174             replacement 
= string
.join( maskchr 
* repcount
, "")  # the resulting substr 
2175             s 
= s
[:match
.start(1)] + replacement 
+ s
[match
.end(2)+1:]   #account for trailing '}' 
2176             match 
= rex
.search(s
)                               # look for another such entry in mask 
2178         self
._decimalChar 
= self
._ctrl
_constraints
._decimalChar
 
2179         self
._shiftDecimalChar 
= self
._ctrl
_constraints
._shiftDecimalChar
 
2181         self
._isFloat      
= _isFloatingPoint(s
) and not self
._ctrl
_constraints
._validRegex
 
2182         self
._isInt      
= _isInteger(s
) and not self
._ctrl
_constraints
._validRegex
 
2183         self
._signOk     
= '-' in self
._ctrl
_constraints
._formatcodes 
and (self
._isFloat 
or self
._isInt
) 
2184         self
._useParens  
= self
._ctrl
_constraints
._useParensForNegatives
 
2186 ####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) 
2187 ####        dbg('isFloatingPoint(%s)?' % (s), _isFloatingPoint(s), 
2188 ##            'ctrl regex:', self._ctrl_constraints._validRegex) 
2190         if self
._signOk 
and s
[0] != ' ': 
2192             if self
._ctrl
_constraints
._defaultValue 
and self
._ctrl
_constraints
._defaultValue
[0] != ' ': 
2193                 self
._ctrl
_constraints
._defaultValue 
= ' ' + self
._ctrl
_constraints
._defaultValue
 
2198                 self
._ctrl
_constraints
._defaultValue 
+= ' ' 
2200         # Now, go build up a dictionary of booleans, indexed by position, 
2201         # indicating whether or not a given position is masked or not 
2205             if s
[i
] == '\\':            # if escaped character: 
2206                 ismasked
[i
] = False     #     mark position as not a mask char 
2207                 if i
+1 < len(s
):        #     if another char follows... 
2208                     s 
= s
[:i
] + s
[i
+1:] #         elide the '\' 
2209                     if i
+2 < len(s
) and s
[i
+1] == '\\': 
2210                         # if next char also a '\', char is a literal '\' 
2211                         s 
= s
[:i
] + s
[i
+1:]     # elide the 2nd '\' as well 
2212             else:                       # else if special char, mark position accordingly 
2213                 ismasked
[i
] = s
[i
] in maskchars
 
2214 ####            dbg('ismasked[%d]:' % i, ismasked[i], s) 
2215             i 
+= 1                      # increment to next char 
2216 ####        dbg('ismasked:', ismasked) 
2217 ##        dbg('new mask: "%s"' % s, indent=0) 
2222     def _calcFieldExtents(self
): 
2224         Subroutine responsible for establishing/configuring field instances with 
2225         indices and editable extents appropriate to the specified mask, and building 
2226         the lookup table mapping each position to the corresponding field. 
2228         self
._lookupField 
= {} 
2231             ## Create dictionary of positions,characters in mask 
2233             for charnum 
in range( len( self
._mask
)): 
2234                 self
.maskdict
[charnum
] = self
._mask
[charnum
:charnum
+1] 
2236             # For the current mask, create an ordered list of field extents 
2237             # and a dictionary of positions that map to field indices: 
2239             if self
._signOk
: start 
= 1 
2243                 # Skip field "discovery", and just construct a 2-field control with appropriate 
2244                 # constraints for a floating-point entry. 
2246                 # .setdefault always constructs 2nd argument even if not needed, so we do this 
2247                 # the old-fashioned way... 
2248                 if not self
._fields
.has_key(0): 
2249                     self
._fields
[0] = Field() 
2250                 if not self
._fields
.has_key(1): 
2251                     self
._fields
[1] = Field() 
2253                 self
._decimalpos 
= string
.find( self
._mask
, '.') 
2254 ##                dbg('decimal pos =', self._decimalpos) 
2256                 formatcodes 
= self
._fields
[0]._GetParameter
('formatcodes') 
2257                 if 'R' not in formatcodes
: formatcodes 
+= 'R' 
2258                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, self
._decimalpos
), 
2259                                                mask
=self
._mask
[start
:self
._decimalpos
], formatcodes
=formatcodes
) 
2260                 end 
= len(self
._mask
) 
2261                 if self
._signOk 
and self
._useParens
: 
2263                 self
._fields
[1]._SetParameters
(index
=1, extent
=(self
._decimalpos
+1, end
), 
2264                                                mask
=self
._mask
[self
._decimalpos
+1:end
]) 
2266                 for i 
in range(self
._decimalpos
+1): 
2267                     self
._lookupField
[i
] = 0 
2269                 for i 
in range(self
._decimalpos
+1, len(self
._mask
)+1): 
2270                     self
._lookupField
[i
] = 1 
2273                 # Skip field "discovery", and just construct a 1-field control with appropriate 
2274                 # constraints for a integer entry. 
2275                 if not self
._fields
.has_key(0): 
2276                     self
._fields
[0] = Field(index
=0) 
2277                 end 
= len(self
._mask
) 
2278                 if self
._signOk 
and self
._useParens
: 
2280                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, end
), 
2281                                                mask
=self
._mask
[start
:end
]) 
2282                 for i 
in range(len(self
._mask
)+1): 
2283                     self
._lookupField
[i
] = 0 
2285                 # generic control; parse mask to figure out where the fields are: 
2288                 i 
= self
._findNextEntry
(pos
,adjustInsert
=False)  # go to 1st entry point: 
2289                 if i 
< len(self
._mask
):   # no editable chars! 
2290                     for j 
in range(pos
, i
+1): 
2291                         self
._lookupField
[j
] = field_index
 
2292                     pos 
= i       
# figure out field for 1st editable space: 
2294                 while i 
<= len(self
._mask
): 
2295 ####                    dbg('searching: outer field loop: i = ', i) 
2296                     if self
._isMaskChar
(i
): 
2297 ####                        dbg('1st char is mask char; recording edit_start=', i) 
2299                         # Skip to end of editable part of current field: 
2300                         while i 
< len(self
._mask
) and self
._isMaskChar
(i
): 
2301                             self
._lookupField
[i
] = field_index
 
2303 ####                        dbg('edit_end =', i) 
2305                         self
._lookupField
[i
] = field_index
 
2306 ####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) 
2307                         if not self
._fields
.has_key(field_index
): 
2308                             kwargs 
= Field
.valid_params
.copy() 
2309                             kwargs
['index'] = field_index
 
2310                             kwargs
['extent'] = (edit_start
, edit_end
) 
2311                             kwargs
['mask'] = self
._mask
[edit_start
:edit_end
] 
2312                             self
._fields
[field_index
] = Field(**kwargs
) 
2314                             self
._fields
[field_index
]._SetParameters
( 
2316                                                                 extent
=(edit_start
, edit_end
), 
2317                                                                 mask
=self
._mask
[edit_start
:edit_end
]) 
2319                     i 
= self
._findNextEntry
(pos
, adjustInsert
=False)  # go to next field: 
2321                         for j 
in range(pos
, i
+1): 
2322                             self
._lookupField
[j
] = field_index
 
2323                     if i 
>= len(self
._mask
): 
2324                         break           # if past end, we're done 
2327 ####                        dbg('next field:', field_index) 
2329         indices 
= self
._fields
.keys() 
2331         self
._field
_indices 
= indices
[1:] 
2332 ####        dbg('lookupField map:', indent=1) 
2333 ##        for i in range(len(self._mask)): 
2334 ####            dbg('pos %d:' % i, self._lookupField[i]) 
2337         # Verify that all field indices specified are valid for mask: 
2338         for index 
in self
._fields
.keys(): 
2339             if index 
not in [-1] + self
._lookupField
.values(): 
2340                 raise IndexError('field %d is not a valid field for mask "%s"' % (index
, self
._mask
)) 
2343     def _calcTemplate(self
, reset_fillchar
, reset_default
): 
2345         Subroutine for processing current fillchars and default values for 
2346         whole control and individual fields, constructing the resulting 
2347         overall template, and adjusting the current value as necessary. 
2350         if self
._ctrl
_constraints
._defaultValue
: 
2353             for field 
in self
._fields
.values(): 
2354                 if field
._defaultValue 
and not reset_default
: 
2356 ##        dbg('default set?', default_set) 
2358         # Determine overall new template for control, and keep track of previous 
2359         # values, so that current control value can be modified as appropriate: 
2360         if self
.controlInitialized
: curvalue 
= list(self
._GetValue
()) 
2361         else:                       curvalue 
= None 
2363         if hasattr(self
, '_fillChar'): old_fillchars 
= self
._fillChar
 
2364         else:                          old_fillchars 
= None 
2366         if hasattr(self
, '_template'): old_template 
= self
._template
 
2367         else:                          old_template 
= None 
2374         for field 
in self
._fields
.values(): 
2375             field
._template 
= "" 
2377         for pos 
in range(len(self
._mask
)): 
2378 ####            dbg('pos:', pos) 
2379             field 
= self
._FindField
(pos
) 
2380 ####            dbg('field:', field._index) 
2381             start
, end 
= field
._extent
 
2383             if pos 
== 0 and self
._signOk
: 
2384                 self
._template 
= ' ' # always make 1st 1st position blank, regardless of fillchar 
2385             elif self
._isFloat 
and pos 
== self
._decimalpos
: 
2386                 self
._template 
+= self
._decimalChar
 
2387             elif self
._isMaskChar
(pos
): 
2388                 if field
._fillChar 
!= self
._ctrl
_constraints
._fillChar 
and not reset_fillchar
: 
2389                     fillChar 
= field
._fillChar
 
2391                     fillChar 
= self
._ctrl
_constraints
._fillChar
 
2392                 self
._fillChar
[pos
] = fillChar
 
2394                 # Replace any current old fillchar with new one in current value; 
2395                 # if action required, set reset_value flag so we can take that action 
2396                 # after we're all done 
2397                 if self
.controlInitialized 
and old_fillchars 
and old_fillchars
.has_key(pos
) and curvalue
: 
2398                     if curvalue
[pos
] == old_fillchars
[pos
] and old_fillchars
[pos
] != fillChar
: 
2400                         curvalue
[pos
] = fillChar
 
2402                 if not field
._defaultValue 
and not self
._ctrl
_constraints
._defaultValue
: 
2403 ####                    dbg('no default value') 
2404                     self
._template 
+= fillChar
 
2405                     field
._template 
+= fillChar
 
2407                 elif field
._defaultValue 
and not reset_default
: 
2408 ####                    dbg('len(field._defaultValue):', len(field._defaultValue)) 
2409 ####                    dbg('pos-start:', pos-start) 
2410                     if len(field
._defaultValue
) > pos
-start
: 
2411 ####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) 
2412                         self
._template 
+= field
._defaultValue
[pos
-start
] 
2413                         field
._template 
+= field
._defaultValue
[pos
-start
] 
2415 ####                        dbg('field default not long enough; using fillChar') 
2416                         self
._template 
+= fillChar
 
2417                         field
._template 
+= fillChar
 
2419                     if len(self
._ctrl
_constraints
._defaultValue
) > pos
: 
2420 ####                        dbg('using control default') 
2421                         self
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2422                         field
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2424 ####                        dbg('ctrl default not long enough; using fillChar') 
2425                         self
._template 
+= fillChar
 
2426                         field
._template 
+= fillChar
 
2427 ####                dbg('field[%d]._template now "%s"' % (field._index, field._template)) 
2428 ####                dbg('self._template now "%s"' % self._template) 
2430                 self
._template 
+= self
._mask
[pos
] 
2432         self
._fields
[-1]._template 
= self
._template     
# (for consistency) 
2434         if curvalue
:    # had an old value, put new one back together 
2435             newvalue 
= string
.join(curvalue
, "") 
2440             self
._defaultValue 
= self
._template
 
2441 ##            dbg('self._defaultValue:', self._defaultValue) 
2442             if not self
.IsEmpty(self
._defaultValue
) and not self
.IsValid(self
._defaultValue
): 
2444                 raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self
._defaultValue
, self
.name
)) 
2446             # if no fillchar change, but old value == old template, replace it: 
2447             if newvalue 
== old_template
: 
2448                 newvalue 
= self
._template
 
2451             self
._defaultValue 
= None 
2454 ##            dbg('resetting value to: "%s"' % newvalue) 
2455             pos 
= self
._GetInsertionPoint
() 
2456             sel_start
, sel_to 
= self
._GetSelection
() 
2457             self
._SetValue
(newvalue
) 
2458             self
._SetInsertionPoint
(pos
) 
2459             self
._SetSelection
(sel_start
, sel_to
) 
2462     def _propagateConstraints(self
, **reset_args
): 
2464         Subroutine for propagating changes to control-level constraints and 
2465         formatting to the individual fields as appropriate. 
2467         parent_codes 
= self
._ctrl
_constraints
._formatcodes
 
2468         parent_includes 
= self
._ctrl
_constraints
._includeChars
 
2469         parent_excludes 
= self
._ctrl
_constraints
._excludeChars
 
2470         for i 
in self
._field
_indices
: 
2471             field 
= self
._fields
[i
] 
2473             if len(self
._field
_indices
) == 1: 
2474                 inherit_args
['formatcodes'] = parent_codes
 
2475                 inherit_args
['includeChars'] = parent_includes
 
2476                 inherit_args
['excludeChars'] = parent_excludes
 
2478                 field_codes 
= current_codes 
= field
._GetParameter
('formatcodes') 
2479                 for c 
in parent_codes
: 
2480                     if c 
not in field_codes
: field_codes 
+= c
 
2481                 if field_codes 
!= current_codes
: 
2482                     inherit_args
['formatcodes'] = field_codes
 
2484                 include_chars 
= current_includes 
= field
._GetParameter
('includeChars') 
2485                 for c 
in parent_includes
: 
2486                     if not c 
in include_chars
: include_chars 
+= c
 
2487                 if include_chars 
!= current_includes
: 
2488                     inherit_args
['includeChars'] = include_chars
 
2490                 exclude_chars 
= current_excludes 
= field
._GetParameter
('excludeChars') 
2491                 for c 
in parent_excludes
: 
2492                     if not c 
in exclude_chars
: exclude_chars 
+= c
 
2493                 if exclude_chars 
!= current_excludes
: 
2494                     inherit_args
['excludeChars'] = exclude_chars
 
2496             if reset_args
.has_key('defaultValue') and reset_args
['defaultValue']: 
2497                 inherit_args
['defaultValue'] = ""   # (reset for field) 
2499             for param 
in Field
.propagating_params
: 
2500 ####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) 
2501 ####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) 
2502                 if reset_args
.has_key(param
): 
2503                     inherit_args
[param
] = self
.GetCtrlParameter(param
) 
2504 ####                    dbg('inherit_args[%s]' % param, inherit_args[param]) 
2507                 field
._SetParameters
(**inherit_args
) 
2508                 field
._ValidateParameters
(**inherit_args
) 
2511     def _validateChoices(self
): 
2513         Subroutine that validates that all choices for given fields are at 
2514         least of the necessary length, and that they all would be valid pastes 
2515         if pasted into their respective fields. 
2517         for field 
in self
._fields
.values(): 
2519                 index 
= field
._index
 
2520                 if len(self
._field
_indices
) == 1 and index 
== 0 and field
._choices 
== self
._ctrl
_constraints
._choices
: 
2521 ##                    dbg('skipping (duplicate) choice validation of field 0') 
2523 ####                dbg('checking for choices for field', field._index) 
2524                 start
, end 
= field
._extent
 
2525                 field_length 
= end 
- start
 
2526 ####                dbg('start, end, length:', start, end, field_length) 
2527                 for choice 
in field
._choices
: 
2528 ####                    dbg('testing "%s"' % choice) 
2529                     valid_paste
, ignore
, replace_to 
= self
._validatePaste
(choice
, start
, end
) 
2532                         raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice
, index
, self
.name
)) 
2533                     elif replace_to 
> end
: 
2535                         raise ValueError('"%s" will not fit into field %d of control "%s"' (choice
, index
, self
.name
)) 
2536 ####                    dbg(choice, 'valid in field', index) 
2539     def _configure(self
, mask
, **reset_args
): 
2541         This function sets flags for automatic styling options.  It is 
2542         called whenever a control or field-level parameter is set/changed. 
2544         This routine does the bulk of the interdependent parameter processing, determining 
2545         the field extents of the mask if changed, resetting parameters as appropriate, 
2546         determining the overall template value for the control, etc. 
2548         reset_args is supplied if called from control's .SetCtrlParameters() 
2549         routine, and indicates which if any parameters which can be 
2550         overridden by individual fields have been reset by request for the 
2555 ##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) 
2557         # Preprocess specified mask to expand {n} syntax, handle escaped 
2558         # mask characters, etc and build the resulting positionally keyed 
2559         # dictionary for which positions are mask vs. template characters: 
2560         self
._mask
, self
.ismasked 
= self
._processMask
(mask
) 
2561         self
._masklength 
= len(self
._mask
) 
2562 ####        dbg('processed mask:', self._mask) 
2564         # Preserve original mask specified, for subsequent reprocessing 
2565         # if parameters change. 
2566 ##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) 
2567         self
._previous
_mask 
= mask    
# save unexpanded mask for next time 
2568             # Set expanded mask and extent of field -1 to width of entire control: 
2569         self
._ctrl
_constraints
._SetParameters
(mask 
= self
._mask
, extent
=(0,self
._masklength
)) 
2571         # Go parse mask to determine where each field is, construct field 
2572         # instances as necessary, configure them with those extents, and 
2573         # build lookup table mapping each position for control to its corresponding 
2575 ####        dbg('calculating field extents') 
2577         self
._calcFieldExtents
() 
2580         # Go process defaultValues and fillchars to construct the overall 
2581         # template, and adjust the current value as necessary: 
2582         reset_fillchar 
= reset_args
.has_key('fillChar') and reset_args
['fillChar'] 
2583         reset_default 
= reset_args
.has_key('defaultValue') and reset_args
['defaultValue'] 
2585 ####        dbg('calculating template') 
2586         self
._calcTemplate
(reset_fillchar
, reset_default
) 
2588         # Propagate control-level formatting and character constraints to each 
2589         # field if they don't already have them; if only one field, propagate 
2590         # control-level validation constraints to field as well: 
2591 ####        dbg('propagating constraints') 
2592         self
._propagateConstraints
(**reset_args
) 
2595         if self
._isFloat 
and self
._fields
[0]._groupChar 
== self
._decimalChar
: 
2596             raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % 
2597                                  (self
._fields
[0]._groupChar
, self
._decimalChar
) ) 
2599 ####        dbg('fields:', indent=1) 
2600 ##        for i in [-1] + self._field_indices: 
2601 ####            dbg('field %d:' % i, self._fields[i].__dict__) 
2604         # Set up special parameters for numeric control, if appropriate: 
2606             self
._signpos 
= 0   # assume it starts here, but it will move around on floats 
2607             signkeys 
= ['-', '+', ' '] 
2609                 signkeys 
+= ['(', ')'] 
2610             for key 
in signkeys
: 
2612                 if not self
._keyhandlers
.has_key(keycode
): 
2613                     self
._SetKeyHandler
(key
, self
._OnChangeSign
) 
2617         if self
._isFloat 
or self
._isInt
: 
2618             if self
.controlInitialized
: 
2619                 value 
= self
._GetValue
() 
2620 ####                dbg('value: "%s"' % value, 'len(value):', len(value), 
2621 ##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) 
2622                 if len(value
) < len(self
._ctrl
_constraints
._mask
): 
2624                     if self
._useParens 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find('(') == -1: 
2626                     if self
._signOk 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find(')') == -1: 
2627                         newvalue 
= ' ' + newvalue
 
2628                     if len(newvalue
) < len(self
._ctrl
_constraints
._mask
): 
2629                         if self
._ctrl
_constraints
._alignRight
: 
2630                             newvalue 
= newvalue
.rjust(len(self
._ctrl
_constraints
._mask
)) 
2632                             newvalue 
= newvalue
.ljust(len(self
._ctrl
_constraints
._mask
)) 
2633 ##                    dbg('old value: "%s"' % value) 
2634 ##                    dbg('new value: "%s"' % newvalue) 
2636                         self
._SetValue
(newvalue
) 
2637                     except Exception, e
: 
2638 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2639                         self
._SetInitialValue
() 
2641                 elif len(value
) > len(self
._ctrl
_constraints
._mask
): 
2643                     if not self
._useParens 
and newvalue
[-1] == ' ': 
2644                         newvalue 
= newvalue
[:-1] 
2645                     if not self
._signOk 
and len(newvalue
) > len(self
._ctrl
_constraints
._mask
): 
2646                         newvalue 
= newvalue
[1:] 
2647                     if not self
._signOk
: 
2648                         newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(newvalue
) 
2650 ##                    dbg('old value: "%s"' % value) 
2651 ##                    dbg('new value: "%s"' % newvalue) 
2653                         self
._SetValue
(newvalue
) 
2654                     except Exception, e
: 
2655 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2656                         self
._SetInitialValue
() 
2657                 elif not self
._signOk 
and ('(' in value 
or '-' in value
): 
2658                     newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(value
) 
2659 ##                    dbg('old value: "%s"' % value) 
2660 ##                    dbg('new value: "%s"' % newvalue) 
2662                         self
._SetValue
(newvalue
) 
2664 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2665                         self
._SetInitialValue
() 
2667             # Replace up/down arrow default handling: 
2668             # make down act like tab, up act like shift-tab: 
2670 ####            dbg('Registering numeric navigation and control handlers (if not already set)') 
2671             if not self
._keyhandlers
.has_key(wx
.WXK_DOWN
): 
2672                 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnChangeField
) 
2673             if not self
._keyhandlers
.has_key(wx
.WXK_UP
): 
2674                 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnUpNumeric
)  # (adds "shift" to up arrow, and calls _OnChangeField) 
2676             # On ., truncate contents right of cursor to decimal point (if any) 
2677             # leaves cusor after decimal point if floating point, otherwise at 0. 
2678             if not self
._keyhandlers
.has_key(ord(self
._decimalChar
)): 
2679                 self
._SetKeyHandler
(self
._decimalChar
, self
._OnDecimalPoint
) 
2680             if not self
._keyhandlers
.has_key(ord(self
._shiftDecimalChar
)): 
2681                 self
._SetKeyHandler
(self
._shiftDecimalChar
, self
._OnChangeField
)   # (Shift-'.' == '>' on US keyboards) 
2683             # Allow selective insert of groupchar in numbers: 
2684             if not self
._keyhandlers
.has_key(ord(self
._fields
[0]._groupChar
)): 
2685                 self
._SetKeyHandler
(self
._fields
[0]._groupChar
, self
._OnGroupChar
) 
2687 ##        dbg(indent=0, suspend=0) 
2690     def _SetInitialValue(self
, value
=""): 
2692         fills the control with the generated or supplied default value. 
2693         It will also set/reset the font if necessary and apply 
2694         formatting to the control at this time. 
2696 ##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) 
2698             self
._prevValue 
= self
._curValue 
= self
._template
 
2699             # don't apply external validation rules in this case, as template may 
2700             # not coincide with "legal" value... 
2702                 self
._SetValue
(self
._curValue
)  # note the use of "raw" ._SetValue()... 
2703             except Exception, e
: 
2704 ##                dbg('exception thrown:', e, indent=0) 
2707             # Otherwise apply validation as appropriate to passed value: 
2708 ####            dbg('value = "%s", length:' % value, len(value)) 
2709             self
._prevValue 
= self
._curValue 
= value
 
2711                 self
.SetValue(value
)            # use public (validating) .SetValue() 
2712             except Exception, e
: 
2713 ##                dbg('exception thrown:', e, indent=0) 
2717         # Set value/type-specific formatting 
2718         self
._applyFormatting
() 
2722     def _calcSize(self
, size
=None): 
2723         """ Calculate automatic size if allowed; must be called after the base control is instantiated""" 
2724 ####        dbg('MaskedEditMixin::_calcSize', indent=1) 
2725         cont 
= (size 
is None or size 
== wx
.DefaultSize
) 
2727         if cont 
and self
._autofit
: 
2728             sizing_text 
= 'M' * self
._masklength
 
2729             if wx
.Platform 
!= "__WXMSW__":   # give it a little extra space 
2731             if wx
.Platform 
== "__WXMAC__":   # give it even a little more... 
2733 ####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) 
2734             w
, h 
= self
.GetTextExtent(sizing_text
) 
2735             size 
= (w
+4, self
.GetSize().height
) 
2736 ####            dbg('size:', size, indent=0) 
2741         """ Set the control's font typeface -- pass the font name as str.""" 
2742 ####        dbg('MaskedEditMixin::_setFont', indent=1) 
2743         if not self
._useFixedWidthFont
: 
2744             self
._font 
= wx
.SystemSettings_GetFont(wx
.SYS_DEFAULT_GUI_FONT
) 
2746             font 
= self
.GetFont()   # get size, weight, etc from current font 
2748             # Set to teletype font (guaranteed to be mappable to all wxWindows 
2750             self
._font 
= wx
.Font( font
.GetPointSize(), wx
.TELETYPE
, font
.GetStyle(), 
2751                                  font
.GetWeight(), font
.GetUnderlined()) 
2752 ####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) 
2754         self
.SetFont(self
._font
) 
2758     def _OnTextChange(self
, event
): 
2760         Handler for EVT_TEXT event. 
2761         self._Change() is provided for subclasses, and may return False to 
2762         skip this method logic.  This function returns True if the event 
2763         detected was a legitimate event, or False if it was a "bogus" 
2764         EVT_TEXT event.  (NOTE: There is currently an issue with calling 
2765         .SetValue from within the EVT_CHAR handler that causes duplicate 
2766         EVT_TEXT events for the same change.) 
2768         newvalue 
= self
._GetValue
() 
2769 ##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) 
2771         if self
._ignoreChange
:      # ie. if an "intermediate text change event" 
2775         ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue 
2776         ## call is generating two (2) EVT_TEXT events.  On certain platforms, 
2777         ## (eg. linux/GTK) the 1st is an empty string value. 
2778         ## This is the only mechanism I can find to mask this problem: 
2779         if newvalue 
== self
._curValue 
or len(newvalue
) == 0: 
2780 ##            dbg('ignoring bogus text change event', indent=0) 
2783 ##            dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue))) 
2785                 if self
._signOk 
and self
._isNeg 
and newvalue
.find('-') == -1 and newvalue
.find('(') == -1: 
2786 ##                    dbg('clearing self._isNeg') 
2788                     text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
() 
2789                 self
._CheckValid
()  # Recolor control as appropriate 
2790 ##            dbg('calling event.Skip()') 
2793         self
._prevValue 
= self
._curValue    
# save for undo 
2794         self
._curValue 
= newvalue           
# Save last seen value for next iteration 
2799     def _OnKeyDown(self
, event
): 
2801         This function allows the control to capture Ctrl-events like Ctrl-tab, 
2802         that are not normally seen by the "cooked" EVT_CHAR routine. 
2804         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2805         key    
= event
.GetKeyCode() 
2806         if key 
in self
._nav 
and event
.ControlDown(): 
2807             # then this is the only place we will likely see these events; 
2809 ##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') 
2812         # else allow regular EVT_CHAR key processing 
2816     def _OnChar(self
, event
): 
2818         This is the engine of MaskedEdit controls.  It examines each keystroke, 
2819         decides if it's allowed, where it should go or what action to take. 
2821 ##        dbg('MaskedEditMixin::_OnChar', indent=1) 
2823         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2824         key 
= event
.GetKeyCode() 
2825         orig_pos 
= self
._GetInsertionPoint
() 
2826         orig_value 
= self
._GetValue
() 
2827 ##        dbg('keycode = ', key) 
2828 ##        dbg('current pos = ', orig_pos) 
2829 ##        dbg('current selection = ', self._GetSelection()) 
2831         if not self
._Keypress
(key
): 
2835         # If no format string for this control, or the control is marked as "read-only", 
2836         # skip the rest of the special processing, and just "do the standard thing:" 
2837         if not self
._mask 
or not self
._IsEditable
(): 
2842         # Process navigation and control keys first, with 
2843         # position/selection unadulterated: 
2844         if key 
in self
._nav 
+ self
._control
: 
2845             if self
._keyhandlers
.has_key(key
): 
2846                 keep_processing 
= self
._keyhandlers
[key
](event
) 
2847                 if self
._GetValue
() != orig_value
: 
2848                     self
.modified 
= True 
2849                 if not keep_processing
: 
2852                 self
._applyFormatting
() 
2856         # Else... adjust the position as necessary for next input key, 
2857         # and determine resulting selection: 
2858         pos 
= self
._adjustPos
( orig_pos
, key 
)    ## get insertion position, adjusted as needed 
2859         sel_start
, sel_to 
= self
._GetSelection
()                ## check for a range of selected text 
2860 ##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) 
2862         keep_processing 
= True 
2863         # Capture user past end of format field 
2864         if pos 
> len(self
.maskdict
): 
2865 ##            dbg("field length exceeded:",pos) 
2866             keep_processing 
= False 
2869             if self
._isMaskChar
(pos
):  ## Get string of allowed characters for validation 
2870                 okchars 
= self
._getAllowedChars
(pos
) 
2872 ##                dbg('Not a valid position: pos = ', pos,"chars=",maskchars) 
2875         key 
= self
._adjustKey
(pos
, key
)     # apply formatting constraints to key: 
2877         if self
._keyhandlers
.has_key(key
): 
2878             # there's an override for default behavior; use override function instead 
2879 ##            dbg('using supplied key handler:', self._keyhandlers[key]) 
2880             keep_processing 
= self
._keyhandlers
[key
](event
) 
2881             if self
._GetValue
() != orig_value
: 
2882                 self
.modified 
= True 
2883             if not keep_processing
: 
2886             # else skip default processing, but do final formatting 
2887         if key 
< wx
.WXK_SPACE 
or key 
> 255: 
2888 ##            dbg('key < WXK_SPACE or key > 255') 
2889             event
.Skip()                # non alphanumeric 
2890             keep_processing 
= False 
2892             field 
= self
._FindField
(pos
) 
2894 ##            dbg("key ='%s'" % chr(key)) 
2895 ##            if chr(key) == ' ': 
2896 ##                dbg('okSpaces?', field._okSpaces) 
2899             char 
= chr(key
) # (must work if we got this far) 
2901             if 'unicode' in wx
.PlatformInfo
: 
2902                 char 
= char
.decode(self
._defaultEncoding
) 
2904                 if type(field
._excludeChars
) != types
.UnicodeType
: 
2905                     excludes 
+= field
._excludeChars
.decode(self
._defaultEncoding
) 
2906                 if type(self
._ctrl
_constraints
) != types
.UnicodeType
: 
2907                     excludes 
+= self
._ctrl
_constraints
._excludeChars
.decode(self
._defaultEncoding
) 
2909                 excludes 
= field
._excludeChars 
+ self
._ctrl
_constraints
._excludeChars
 
2911             if char 
in excludes
: 
2912                 keep_processing 
= False 
2914             if keep_processing 
and self
._isCharAllowed
( char
, pos
, checkRegex 
= True ): 
2915 ##                dbg("key allowed by mask") 
2916                 # insert key into candidate new value, but don't change control yet: 
2917                 oldstr 
= self
._GetValue
() 
2918                 newstr
, newpos
, new_select_to
, match_field
, match_index 
= self
._insertKey
( 
2919                                 char
, pos
, sel_start
, sel_to
, self
._GetValue
(), allowAutoSelect 
= True) 
2920 ##                dbg("str with '%s' inserted:" % char, '"%s"' % newstr) 
2921                 if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
2922 ##                    dbg('not valid; checking to see if adjusted string is:') 
2923                     keep_processing 
= False 
2924                     if self
._isFloat 
and newstr 
!= self
._template
: 
2925                         newstr 
= self
._adjustFloat
(newstr
) 
2926 ##                        dbg('adjusted str:', newstr) 
2927                         if self
.IsValid(newstr
): 
2929                             keep_processing 
= True 
2930                             wx
.CallAfter(self
._SetInsertionPoint
, self
._decimalpos
) 
2931                     if not keep_processing
: 
2932 ##                        dbg("key disallowed by validation") 
2933                         if not wx
.Validator_IsSilent() and orig_pos 
== pos
: 
2939                     # special case: adjust date value as necessary: 
2940                     if self
._isDate 
and newstr 
!= self
._template
: 
2941                         newstr 
= self
._adjustDate
(newstr
) 
2942 ##                    dbg('adjusted newstr:', newstr) 
2944                     if newstr 
!= orig_value
: 
2945                         self
.modified 
= True 
2947                     wx
.CallAfter(self
._SetValue
, newstr
) 
2949                     # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: 
2950                     if not self
.IsDefault() and self
._isDate 
and self
._4digityear
: 
2951                         year2dig 
= self
._dateExtent 
- 2 
2952                         if pos 
== year2dig 
and unadjusted
[year2dig
] != newstr
[year2dig
]: 
2955                     wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
2957                     if match_field 
is not None: 
2958 ##                        dbg('matched field') 
2959                         self
._OnAutoSelect
(match_field
, match_index
) 
2961                     if new_select_to 
!= newpos
: 
2962 ##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) 
2963                         wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
2965                         newfield 
= self
._FindField
(newpos
) 
2966                         if newfield 
!= field 
and newfield
._selectOnFieldEntry
: 
2967 ##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) 
2968                             wx
.CallAfter(self
._SetSelection
, newfield
._extent
[0], newfield
._extent
[1]) 
2970                             wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
2971                     keep_processing 
= False 
2973             elif keep_processing
: 
2974 ##                dbg('char not allowed') 
2975                 keep_processing 
= False 
2976                 if (not wx
.Validator_IsSilent()) and orig_pos 
== pos
: 
2979         self
._applyFormatting
() 
2981         # Move to next insertion point 
2982         if keep_processing 
and key 
not in self
._nav
: 
2983             pos 
= self
._GetInsertionPoint
() 
2984             next_entry 
= self
._findNextEntry
( pos 
) 
2985             if pos 
!= next_entry
: 
2986 ##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) 
2987                 wx
.CallAfter(self
._SetInsertionPoint
, next_entry 
) 
2989             if self
._isTemplateChar
(pos
): 
2990                 self
._AdjustField
(pos
) 
2994     def _FindFieldExtent(self
, pos
=None, getslice
=False, value
=None): 
2995         """ returns editable extent of field corresponding to 
2996         position pos, and, optionally, the contents of that field 
2997         in the control or the value specified. 
2998         Template chars are bound to the preceding field. 
2999         For masks beginning with template chars, these chars are ignored 
3000         when calculating the current field. 
3002         Eg: with template (###) ###-####, 
3003         >>> self._FindFieldExtent(pos=0) 
3005         >>> self._FindFieldExtent(pos=1) 
3007         >>> self._FindFieldExtent(pos=5) 
3009         >>> self._FindFieldExtent(pos=6) 
3011         >>> self._FindFieldExtent(pos=10) 
3015 ##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) 
3017         field 
= self
._FindField
(pos
) 
3020                 return None, None, "" 
3023         edit_start
, edit_end 
= field
._extent
 
3025             if value 
is None: value 
= self
._GetValue
() 
3026             slice = value
[edit_start
:edit_end
] 
3027 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) 
3029             return edit_start
, edit_end
, slice 
3031 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end) 
3033             return edit_start
, edit_end
 
3036     def _FindField(self
, pos
=None): 
3038         Returns the field instance in which pos resides. 
3039         Template chars are bound to the preceding field. 
3040         For masks beginning with template chars, these chars are ignored 
3041         when calculating the current field. 
3044 ####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) 
3045         if pos 
is None: pos 
= self
._GetInsertionPoint
() 
3046         elif pos 
< 0 or pos 
> self
._masklength
: 
3047             raise IndexError('position %s out of range of control' % str(pos
)) 
3049         if len(self
._fields
) == 0: 
3055         return self
._fields
[self
._lookupField
[pos
]] 
3058     def ClearValue(self
): 
3059         """ Blanks the current control value by replacing it with the default value.""" 
3060 ##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") 
3061         self
._SetValue
( self
._template 
) 
3062         self
._SetInsertionPoint
(0) 
3066     def _baseCtrlEventHandler(self
, event
): 
3068         This function is used whenever a key should be handled by the base control. 
3074     def _OnUpNumeric(self
, event
): 
3076         Makes up-arrow act like shift-tab should; ie. take you to start of 
3079 ##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1) 
3080         event
.m_shiftDown 
= 1 
3081 ##        dbg('event.ShiftDown()?', event.ShiftDown()) 
3082         self
._OnChangeField
(event
) 
3086     def _OnArrow(self
, event
): 
3088         Used in response to left/right navigation keys; makes these actions skip 
3089         over mask template chars. 
3091 ##        dbg("MaskedEditMixin::_OnArrow", indent=1) 
3092         pos 
= self
._GetInsertionPoint
() 
3093         keycode 
= event
.GetKeyCode() 
3094         sel_start
, sel_to 
= self
._GetSelection
() 
3095         entry_end 
= self
._goEnd
(getPosOnly
=True) 
3096         if keycode 
in (wx
.WXK_RIGHT
, wx
.WXK_DOWN
): 
3097             if( ( not self
._isTemplateChar
(pos
) and pos
+1 > entry_end
) 
3098                 or ( self
._isTemplateChar
(pos
) and pos 
>= entry_end
) ): 
3099 ##                dbg("can't advance", indent=0) 
3101             elif self
._isTemplateChar
(pos
): 
3102                 self
._AdjustField
(pos
) 
3103         elif keycode 
in (wx
.WXK_LEFT
,wx
.WXK_UP
) and sel_start 
== sel_to 
and pos 
> 0 and self
._isTemplateChar
(pos
-1): 
3104 ##            dbg('adjusting field') 
3105             self
._AdjustField
(pos
) 
3107         # treat as shifted up/down arrows as tab/reverse tab: 
3108         if event
.ShiftDown() and keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
): 
3109             # remove "shifting" and treat as (forward) tab: 
3110             event
.m_shiftDown 
= False 
3111             keep_processing 
= self
._OnChangeField
(event
) 
3113         elif self
._FindField
(pos
)._selectOnFieldEntry
: 
3114             if( keycode 
in (wx
.WXK_UP
, wx
.WXK_LEFT
) 
3116                 and self
._isTemplateChar
(sel_start
-1) 
3117                 and sel_start 
!= self
._masklength
 
3118                 and not self
._signOk 
and not self
._useParens
): 
3120                 # call _OnChangeField to handle "ctrl-shifted event" 
3121                 # (which moves to previous field and selects it.) 
3122                 event
.m_shiftDown 
= True 
3123                 event
.m_ControlDown 
= True 
3124                 keep_processing 
= self
._OnChangeField
(event
) 
3125             elif( keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
) 
3126                   and sel_to 
!= self
._masklength
 
3127                   and self
._isTemplateChar
(sel_to
)): 
3129                 # when changing field to the right, ensure don't accidentally go left instead 
3130                 event
.m_shiftDown 
= False 
3131                 keep_processing 
= self
._OnChangeField
(event
) 
3133                 # treat arrows as normal, allowing selection 
3135 ##                dbg('using base ctrl event processing') 
3138             if( (sel_to 
== self
._fields
[0]._extent
[0] and keycode 
== wx
.WXK_LEFT
) 
3139                 or (sel_to 
== self
._masklength 
and keycode 
== wx
.WXK_RIGHT
) ): 
3140                 if not wx
.Validator_IsSilent(): 
3143                 # treat arrows as normal, allowing selection 
3145 ##                dbg('using base event processing') 
3148         keep_processing 
= False 
3150         return keep_processing
 
3153     def _OnCtrl_S(self
, event
): 
3154         """ Default Ctrl-S handler; prints value information if demo enabled. """ 
3155 ##        dbg("MaskedEditMixin::_OnCtrl_S") 
3157             print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self
.GetValue(), self
.GetPlainValue()) 
3158             print "Valid? => " + str(self
.IsValid()) 
3159             print "Current field, start, end, value =", str( self
._FindFieldExtent
(getslice
=True)) 
3163     def _OnCtrl_X(self
, event
=None): 
3164         """ Handles ctrl-x keypress in control and Cut operation on context menu. 
3165             Should return False to skip other processing. """ 
3166 ##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1) 
3171     def _OnCtrl_C(self
, event
=None): 
3172         """ Handles ctrl-C keypress in control and Copy operation on context menu. 
3173             Uses base control handling. Should return False to skip other processing.""" 
3177     def _OnCtrl_V(self
, event
=None): 
3178         """ Handles ctrl-V keypress in control and Paste operation on context menu. 
3179             Should return False to skip other processing. """ 
3180 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1) 
3185     def _OnCtrl_Z(self
, event
=None): 
3186         """ Handles ctrl-Z keypress in control and Undo operation on context menu. 
3187             Should return False to skip other processing. """ 
3188 ##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) 
3193     def _OnCtrl_A(self
,event
=None): 
3194         """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ 
3195         end 
= self
._goEnd
(getPosOnly
=True) 
3196         if not event 
or (isinstance(event
, wx
.KeyEvent
) and event
.ShiftDown()): 
3197             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3198             wx
.CallAfter(self
._SetSelection
, 0, self
._masklength
) 
3200             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3201             wx
.CallAfter(self
._SetSelection
, 0, end
) 
3205     def _OnErase(self
, event
=None, just_return_value
=False): 
3206         """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" 
3207 ##        dbg("MaskedEditMixin::_OnErase", indent=1) 
3208         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
3210         if event 
is None:   # called as action routine from Cut() operation. 
3213             key 
= event
.GetKeyCode() 
3215         field 
= self
._FindField
(sel_to
) 
3216         start
, end 
= field
._extent
 
3217         value 
= self
._GetValue
() 
3218         oldstart 
= sel_start
 
3220         # If trying to erase beyond "legal" bounds, disallow operation: 
3221         if( (sel_to 
== 0 and key 
== wx
.WXK_BACK
) 
3222             or (self
._signOk 
and sel_to 
== 1 and value
[0] == ' ' and key 
== wx
.WXK_BACK
) 
3223             or (sel_to 
== self
._masklength 
and sel_start 
== sel_to 
and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) 
3224             or (self
._signOk 
and self
._useParens
 
3225                 and sel_start 
== sel_to
 
3226                 and sel_to 
== self
._masklength 
- 1 
3227                 and value
[sel_to
] == ' ' and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) ): 
3228             if not wx
.Validator_IsSilent(): 
3234         if( field
._insertRight                                  
# an insert-right field 
3235             and value
[start
:end
] != self
._template
[start
:end
]   # and field not empty 
3236             and sel_start 
>= start                              
# and selection starts in field 
3237             and ((sel_to 
== sel_start                           
# and no selection 
3238                   and sel_to 
== end                             
# and cursor at right edge 
3239                   and key 
in (wx
.WXK_BACK
, wx
.WXK_DELETE
))            # and either delete or backspace key 
3241                  (key 
== wx
.WXK_BACK                               
# backspacing 
3242                     and (sel_to 
== end                          
# and selection ends at right edge 
3243                          or sel_to 
< end 
and field
._allowInsert
)) ) ):  # or allow right insert at any point in field 
3245 ##            dbg('delete left') 
3246             # if backspace but left of cursor is empty, adjust cursor right before deleting 
3247             while( key 
== wx
.WXK_BACK
 
3248                    and sel_start 
== sel_to
 
3250                    and value
[start
:sel_start
] == self
._template
[start
:sel_start
]): 
3254 ##            dbg('sel_start, start:', sel_start, start) 
3256             if sel_start 
== sel_to
: 
3260             newfield 
= value
[start
:keep
] + value
[sel_to
:end
] 
3262             # handle sign char moving from outside field into the field: 
3263             move_sign_into_field 
= False 
3264             if not field
._padZero 
and self
._signOk 
and self
._isNeg 
and value
[0] in ('-', '('): 
3266                 newfield 
= signchar 
+ newfield
 
3267                 move_sign_into_field 
= True 
3268 ##            dbg('cut newfield: "%s"' % newfield) 
3270             # handle what should fill in from the left: 
3272             for i 
in range(start
, end 
- len(newfield
)): 
3275                 elif( self
._signOk 
and self
._isNeg 
and i 
== 1 
3276                       and ((self
._useParens 
and newfield
.find('(') == -1) 
3277                            or (not self
._useParens 
and newfield
.find('-') == -1)) ): 
3280                     left 
+= self
._template
[i
]   # this can produce strange results in combination with default values... 
3281             newfield 
= left 
+ newfield
 
3282 ##            dbg('filled newfield: "%s"' % newfield) 
3284             newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3286             # (handle sign located in "mask position" in front of field prior to delete) 
3287             if move_sign_into_field
: 
3288                 newstr 
= ' ' + newstr
[1:] 
3291             # handle erasure of (left) sign, moving selection accordingly... 
3292             if self
._signOk 
and sel_start 
== 0: 
3293                 newstr 
= value 
= ' ' + value
[1:] 
3296             if field
._allowInsert 
and sel_start 
>= start
: 
3297                 # selection (if any) falls within current insert-capable field: 
3298                 select_len 
= sel_to 
- sel_start
 
3299                 # determine where cursor should end up: 
3300                 if key 
== wx
.WXK_BACK
: 
3302                         newpos 
= sel_start 
-1 
3308                     if sel_to 
== sel_start
: 
3309                         erase_to 
= sel_to 
+ 1 
3313                 if self
._isTemplateChar
(newpos
) and select_len 
== 0: 
3315                         if value
[newpos
] in ('(', '-'): 
3316                             newpos 
+= 1     # don't move cusor 
3317                             newstr 
= ' ' + value
[newpos
:] 
3318                         elif value
[newpos
] == ')': 
3319                             # erase right sign, but don't move cursor; (matching left sign handled later) 
3320                             newstr 
= value
[:newpos
] + ' ' 
3322                             # no deletion; just move cursor 
3325                         # no deletion; just move cursor 
3328                     if erase_to 
> end
: erase_to 
= end
 
3329                     erase_len 
= erase_to 
- newpos
 
3331                     left 
= value
[start
:newpos
] 
3332 ##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) 
3333                     right 
= value
[erase_to
:end
] + self
._template
[end
-erase_len
:end
] 
3335                     if field
._alignRight
: 
3336                         rstripped 
= right
.rstrip() 
3337                         if rstripped 
!= right
: 
3338                             pos_adjust 
= len(right
) - len(rstripped
) 
3341                     if not field
._insertRight 
and value
[-1] == ')' and end 
== self
._masklength 
- 1: 
3342                         # need to shift ) into the field: 
3343                         right 
= right
[:-1] + ')' 
3344                         value 
= value
[:-1] + ' ' 
3346                     newfield 
= left
+right
 
3348                         newfield 
= newfield
.rjust(end
-start
) 
3349                         newpos 
+= pos_adjust
 
3350 ##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) 
3351                     newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3356                 if sel_start 
== sel_to
: 
3357 ##                    dbg("current sel_start, sel_to:", sel_start, sel_to) 
3358                     if key 
== wx
.WXK_BACK
: 
3359                         sel_start
, sel_to 
= sel_to
-1, sel_to
-1 
3360 ##                        dbg("new sel_start, sel_to:", sel_start, sel_to) 
3362                     if field
._padZero 
and not value
[start
:sel_to
].replace('0', '').replace(' ','').replace(field
._fillChar
, ''): 
3363                         # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: 
3366                         newchar 
= self
._template
[sel_to
] ## get an original template character to "clear" the current char 
3367 ##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) 
3369                     if self
._isTemplateChar
(sel_to
): 
3370                         if sel_to 
== 0 and self
._signOk 
and value
[sel_to
] == '-':   # erasing "template" sign char 
3371                             newstr 
= ' ' + value
[1:] 
3373                         elif self
._signOk 
and self
._useParens 
and (value
[sel_to
] == ')' or value
[sel_to
] == '('): 
3374                             # allow "change sign" by removing both parens: 
3375                             newstr 
= value
[:self
._signpos
] + ' ' + value
[self
._signpos
+1:-1] + ' ' 
3380                         if field
._insertRight 
and sel_start 
== sel_to
: 
3381                             # force non-insert-right behavior, by selecting char to be replaced: 
3383                         newstr
, ignore 
= self
._insertKey
(newchar
, sel_start
, sel_start
, sel_to
, value
) 
3387                     newstr 
= self
._eraseSelection
(value
, sel_start
, sel_to
) 
3389                 pos 
= sel_start  
# put cursor back at beginning of selection 
3391         if self
._signOk 
and self
._useParens
: 
3392             # account for resultant unbalanced parentheses: 
3393             left_signpos 
= newstr
.find('(') 
3394             right_signpos 
= newstr
.find(')') 
3396             if left_signpos 
== -1 and right_signpos 
!= -1: 
3397                 # erased left-sign marker; get rid of right sign marker: 
3398                 newstr 
= newstr
[:right_signpos
] + ' ' + newstr
[right_signpos
+1:] 
3400             elif left_signpos 
!= -1 and right_signpos 
== -1: 
3401                 # erased right-sign marker; get rid of left-sign marker: 
3402                 newstr 
= newstr
[:left_signpos
] + ' ' + newstr
[left_signpos
+1:] 
3404 ##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) 
3405 ##        dbg("newstr:'%s'" % newstr, 'pos:', pos) 
3407         # if erasure results in an invalid field, disallow it: 
3408 ##        dbg('field._validRequired?', field._validRequired) 
3409 ##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) 
3410         if field
._validRequired 
and not field
.IsValid(newstr
[start
:end
]): 
3411             if not wx
.Validator_IsSilent(): 
3416         # if erasure results in an invalid value, disallow it: 
3417         if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3418             if not wx
.Validator_IsSilent(): 
3423         if just_return_value
: 
3428 ##        dbg('setting value (later) to', newstr) 
3429         wx
.CallAfter(self
._SetValue
, newstr
) 
3430 ##        dbg('setting insertion point (later) to', pos) 
3431         wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3434             self
.modified 
= True 
3438     def _OnEnd(self
,event
): 
3439         """ Handles End keypress in control. Should return False to skip other processing. """ 
3440 ##        dbg("MaskedEditMixin::_OnEnd", indent=1) 
3441         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3442         if not event
.ControlDown(): 
3443             end 
= self
._masklength  
# go to end of control 
3444             if self
._signOk 
and self
._useParens
: 
3445                 end 
= end 
- 1       # account for reserved char at end 
3447             end_of_input 
= self
._goEnd
(getPosOnly
=True) 
3448             sel_start
, sel_to 
= self
._GetSelection
() 
3449             if sel_to 
< pos
: sel_to 
= pos
 
3450             field 
= self
._FindField
(sel_to
) 
3451             field_end 
= self
._FindField
(end_of_input
) 
3453             # pick different end point if either: 
3454             # - cursor not in same field 
3455             # - or at or past last input already 
3456             # - or current selection = end of current field: 
3457 ####            dbg('field != field_end?', field != field_end) 
3458 ####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input) 
3459             if field 
!= field_end 
or sel_to 
>= end_of_input
: 
3460                 edit_start
, edit_end 
= field
._extent
 
3461 ####                dbg('edit_end:', edit_end) 
3462 ####                dbg('sel_to:', sel_to) 
3463 ####                dbg('sel_to == edit_end?', sel_to == edit_end) 
3464 ####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) 
3466                 if sel_to 
== edit_end 
and field
._index 
< self
._field
_indices
[-1]: 
3467                     edit_start
, edit_end 
= self
._FindFieldExtent
(self
._findNextEntry
(edit_end
))  # go to end of next field: 
3469 ##                    dbg('end moved to', end) 
3471                 elif sel_to 
== edit_end 
and field
._index 
== self
._field
_indices
[-1]: 
3472                     # already at edit end of last field; select to end of control: 
3473                     end 
= self
._masklength
 
3474 ##                    dbg('end moved to', end) 
3476                     end 
= edit_end  
# select to end of current field 
3477 ##                    dbg('end moved to ', end) 
3479                 # select to current end of input 
3483 ####        dbg('pos:', pos, 'end:', end) 
3485         if event
.ShiftDown(): 
3486             if not event
.ControlDown(): 
3487 ##                dbg("shift-end; select to end of control") 
3490 ##                dbg("shift-ctrl-end; select to end of non-whitespace") 
3492             wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3493             wx
.CallAfter(self
._SetSelection
, pos
, end
) 
3495             if not event
.ControlDown(): 
3496 ##                dbg('go to end of control:') 
3498             wx
.CallAfter(self
._SetInsertionPoint
, end
) 
3499             wx
.CallAfter(self
._SetSelection
, end
, end
) 
3505     def _OnReturn(self
, event
): 
3507          Swallows the return, issues a Navigate event instead, since 
3508          masked controls are "single line" by defn. 
3510 ##         dbg('MaskedEditMixin::OnReturn') 
3515     def _OnHome(self
,event
): 
3516         """ Handles Home keypress in control. Should return False to skip other processing.""" 
3517 ##        dbg("MaskedEditMixin::_OnHome", indent=1) 
3518         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3519         sel_start
, sel_to 
= self
._GetSelection
() 
3521         # There are 5 cases here: 
3523         # 1) shift: select from start of control to end of current 
3525         if event
.ShiftDown() and not event
.ControlDown(): 
3526 ##            dbg("shift-home; select to start of control") 
3530         # 2) no shift, no control: move cursor to beginning of control. 
3531         elif not event
.ControlDown(): 
3532 ##            dbg("home; move to start of control") 
3536         # 3) No shift, control: move cursor back to beginning of field; if 
3537         #    there already, go to beginning of previous field. 
3538         # 4) shift, control, start of selection not at beginning of control: 
3539         #    move sel_start back to start of field; if already there, go to 
3540         #    start of previous field. 
3541         elif( event
.ControlDown() 
3542               and (not event
.ShiftDown() 
3543                    or (event
.ShiftDown() and sel_start 
> 0) ) ): 
3544             if len(self
._field
_indices
) > 1: 
3545                 field 
= self
._FindField
(sel_start
) 
3546                 start
, ignore 
= field
._extent
 
3547                 if sel_start 
== start 
and field
._index 
!= self
._field
_indices
[0]:  # go to start of previous field: 
3548                     start
, ignore 
= self
._FindFieldExtent
(sel_start
-1) 
3549                 elif sel_start 
== start
: 
3550                     start 
= 0   # go to literal beginning if edit start 
3557             if not event
.ShiftDown(): 
3558 ##                dbg("ctrl-home; move to beginning of field") 
3561 ##                dbg("shift-ctrl-home; select to beginning of field") 
3565         # 5) shift, control, start of selection at beginning of control: 
3566         #    unselect by moving sel_to backward to beginning of current field; 
3567         #    if already there, move to start of previous field. 
3569             if len(self
._field
_indices
) > 1: 
3570                 # find end of previous field: 
3571                 field 
= self
._FindField
(sel_to
) 
3572                 if sel_to 
> start 
and field
._index 
!= self
._field
_indices
[0]: 
3573                     ignore
, end 
= self
._FindFieldExtent
(field
._extent
[0]-1) 
3579                 end_of_field 
= False 
3580 ##            dbg("shift-ctrl-home; unselect to beginning of field") 
3582 ##        dbg('queuing new sel_start, sel_to:', (start, end)) 
3583         wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3584         wx
.CallAfter(self
._SetSelection
, start
, end
) 
3589     def _OnChangeField(self
, event
): 
3591         Primarily handles TAB events, but can be used for any key that 
3592         designer wants to change fields within a masked edit control. 
3594 ##        dbg('MaskedEditMixin::_OnChangeField', indent = 1) 
3595         # determine end of current field: 
3596         pos 
= self
._GetInsertionPoint
() 
3597 ##        dbg('current pos:', pos) 
3598         sel_start
, sel_to 
= self
._GetSelection
() 
3600         if self
._masklength 
< 0:   # no fields; process tab normally 
3601             self
._AdjustField
(pos
) 
3602             if event
.GetKeyCode() == wx
.WXK_TAB
: 
3603 ##                dbg('tab to next ctrl') 
3604                 # As of 2.5.2, you don't call event.Skip() to do 
3605                 # this, but instead force explicit navigation, if 
3606                 # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3613         if event
.ShiftDown(): 
3617             # NOTE: doesn't yet work with SHIFT-tab under wx; the control 
3618             # never sees this event! (But I've coded for it should it ever work, 
3619             # and it *does* work for '.' in IpAddrCtrl.) 
3620             field 
= self
._FindField
(pos
) 
3621             index 
= field
._index
 
3622             field_start 
= field
._extent
[0] 
3623             if pos 
< field_start
: 
3624 ##                dbg('cursor before 1st field; cannot change to a previous field') 
3625                 if not wx
.Validator_IsSilent(): 
3629             if event
.ControlDown(): 
3630 ##                dbg('queuing select to beginning of field:', field_start, pos) 
3631                 wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3632                 wx
.CallAfter(self
._SetSelection
, field_start
, pos
) 
3637                   # We're already in the 1st field; process shift-tab normally: 
3638                 self
._AdjustField
(pos
) 
3639                 if event
.GetKeyCode() == wx
.WXK_TAB
: 
3640 ##                    dbg('tab to previous ctrl') 
3641                     # As of 2.5.2, you don't call event.Skip() to do 
3642                     # this, but instead force explicit navigation, if 
3643                     # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3644                     self
.Navigate(False) 
3646 ##                    dbg('position at beginning') 
3647                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3651                 # find beginning of previous field: 
3652                 begin_prev 
= self
._FindField
(field_start
-1)._extent
[0] 
3653                 self
._AdjustField
(pos
) 
3654 ##                dbg('repositioning to', begin_prev) 
3655                 wx
.CallAfter(self
._SetInsertionPoint
, begin_prev
) 
3656                 if self
._FindField
(begin_prev
)._selectOnFieldEntry
: 
3657                     edit_start
, edit_end 
= self
._FindFieldExtent
(begin_prev
) 
3658 ##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) 
3659                     wx
.CallAfter(self
._SetInsertionPoint
, edit_start
) 
3660                     wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3666             field 
= self
._FindField
(sel_to
) 
3667             field_start
, field_end 
= field
._extent
 
3668             if event
.ControlDown(): 
3669 ##                dbg('queuing select to end of field:', pos, field_end) 
3670                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3671                 wx
.CallAfter(self
._SetSelection
, pos
, field_end
) 
3675                 if pos 
< field_start
: 
3676 ##                    dbg('cursor before 1st field; go to start of field') 
3677                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3678                     if field
._selectOnFieldEntry
: 
3679                         wx
.CallAfter(self
._SetSelection
, field_start
, field_end
) 
3681                         wx
.CallAfter(self
._SetSelection
, field_start
, field_start
) 
3684 ##                dbg('end of current field:', field_end) 
3685 ##                dbg('go to next field') 
3686                 if field_end 
== self
._fields
[self
._field
_indices
[-1]]._extent
[1]: 
3687                     self
._AdjustField
(pos
) 
3688                     if event
.GetKeyCode() == wx
.WXK_TAB
: 
3689 ##                        dbg('tab to next ctrl') 
3690                         # As of 2.5.2, you don't call event.Skip() to do 
3691                         # this, but instead force explicit navigation, if 
3692                         # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3695 ##                        dbg('position at end') 
3696                         wx
.CallAfter(self
._SetInsertionPoint
, field_end
) 
3700                     # we have to find the start of the next field 
3701                     next_pos 
= self
._findNextEntry
(field_end
) 
3702                     if next_pos 
== field_end
: 
3703 ##                        dbg('already in last field') 
3704                         self
._AdjustField
(pos
) 
3705                         if event
.GetKeyCode() == wx
.WXK_TAB
: 
3706 ##                            dbg('tab to next ctrl') 
3707                             # As of 2.5.2, you don't call event.Skip() to do 
3708                             # this, but instead force explicit navigation, if 
3709                             # wx.TE_PROCESS_TAB is used (like in the masked edits) 
3715                         self
._AdjustField
( pos 
) 
3717                         # move cursor to appropriate point in the next field and select as necessary: 
3718                         field 
= self
._FindField
(next_pos
) 
3719                         edit_start
, edit_end 
= field
._extent
 
3720                         if field
._selectOnFieldEntry
: 
3721 ##                            dbg('move to ', next_pos) 
3722                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3723                             edit_start
, edit_end 
= self
._FindFieldExtent
(next_pos
) 
3724 ##                            dbg('queuing select', edit_start, edit_end) 
3725                             wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3727                             if field
._insertRight
: 
3728                                 next_pos 
= field
._extent
[1] 
3729 ##                            dbg('move to ', next_pos) 
3730                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3735     def _OnDecimalPoint(self
, event
): 
3736 ##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) 
3738         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3740         if self
._isFloat
:       ## handle float value, move to decimal place 
3741 ##            dbg('key == Decimal tab; decimal pos:', self._decimalpos) 
3742             value 
= self
._GetValue
() 
3743             if pos 
< self
._decimalpos
: 
3744                 clipped_text 
= value
[0:pos
] + self
._decimalChar 
+ value
[self
._decimalpos
+1:] 
3745 ##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3746                 newstr 
= self
._adjustFloat
(clipped_text
) 
3748                 newstr 
= self
._adjustFloat
(value
) 
3749             wx
.CallAfter(self
._SetValue
, newstr
) 
3750             fraction 
= self
._fields
[1] 
3751             start
, end 
= fraction
._extent
 
3752             wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3753             if fraction
._selectOnFieldEntry
: 
3754 ##                dbg('queuing selection after decimal point to:', (start, end)) 
3755                 wx
.CallAfter(self
._SetSelection
, start
, end
) 
3757                 wx
.CallAfter(self
._SetSelection
, start
, start
) 
3758             keep_processing 
= False 
3760         if self
._isInt
:      ## handle integer value, truncate from current position 
3761 ##            dbg('key == Integer decimal event') 
3762             value 
= self
._GetValue
() 
3763             clipped_text 
= value
[0:pos
] 
3764 ##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3765             newstr 
= self
._adjustInt
(clipped_text
) 
3766 ##            dbg('newstr: "%s"' % newstr) 
3767             wx
.CallAfter(self
._SetValue
, newstr
) 
3768             newpos 
= len(newstr
.rstrip()) 
3769             if newstr
.find(')') != -1: 
3770                 newpos 
-= 1     # (don't move past right paren) 
3771             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3772             wx
.CallAfter(self
._SetSelection
, newpos
, newpos
) 
3773             keep_processing 
= False 
3777     def _OnChangeSign(self
, event
): 
3778 ##        dbg('MaskedEditMixin::_OnChangeSign', indent=1) 
3779         key 
= event
.GetKeyCode() 
3780         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), key
) 
3781         value 
= self
._eraseSelection
() 
3782         integer 
= self
._fields
[0] 
3783         start
, end 
= integer
._extent
 
3785 ####        dbg('adjusted pos:', pos) 
3786         if chr(key
) in ('-','+','(', ')') or (chr(key
) == " " and pos 
== self
._signpos
): 
3787             cursign 
= self
._isNeg
 
3788 ##            dbg('cursign:', cursign) 
3789             if chr(key
) in ('-','(', ')'): 
3790                 self
._isNeg 
= (not self
._isNeg
)   ## flip value 
3793 ##            dbg('isNeg?', self._isNeg) 
3795             text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
(candidate
=value
) 
3796 ##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) 
3800             if self
._isNeg 
and self
._signpos 
is not None and self
._signpos 
!= -1: 
3801                 if self
._useParens 
and self
._right
_signpos 
is not None: 
3802                     text 
= text
[:self
._signpos
] + '(' + text
[self
._signpos
+1:self
._right
_signpos
] + ')' + text
[self
._right
_signpos
+1:] 
3804                     text 
= text
[:self
._signpos
] + '-' + text
[self
._signpos
+1:] 
3806 ####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) 
3808                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:self
._right
_signpos
] + ' ' + text
[self
._right
_signpos
+1:] 
3810                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:] 
3811 ##                dbg('clearing self._isNeg') 
3814             wx
.CallAfter(self
._SetValue
, text
) 
3815             wx
.CallAfter(self
._applyFormatting
) 
3816 ##            dbg('pos:', pos, 'signpos:', self._signpos) 
3817             if pos 
== self
._signpos 
or integer
.IsEmpty(text
[start
:end
]): 
3818                 wx
.CallAfter(self
._SetInsertionPoint
, self
._signpos
+1) 
3820                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3822             keep_processing 
= False 
3824             keep_processing 
= True 
3826         return keep_processing
 
3829     def _OnGroupChar(self
, event
): 
3831         This handler is only registered if the mask is a numeric mask. 
3832         It allows the insertion of ',' or '.' if appropriate. 
3834 ##        dbg('MaskedEditMixin::_OnGroupChar', indent=1) 
3835         keep_processing 
= True 
3836         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3837         sel_start
, sel_to 
= self
._GetSelection
() 
3838         groupchar 
= self
._fields
[0]._groupChar
 
3839         if not self
._isCharAllowed
(groupchar
, pos
, checkRegex
=True): 
3840             keep_processing 
= False 
3841             if not wx
.Validator_IsSilent(): 
3845             newstr
, newpos 
= self
._insertKey
(groupchar
, pos
, sel_start
, sel_to
, self
._GetValue
() ) 
3846 ##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) 
3847             if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3848                 keep_processing 
= False 
3849                 if not wx
.Validator_IsSilent(): 
3853             wx
.CallAfter(self
._SetValue
, newstr
) 
3854             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3855         keep_processing 
= False 
3857         return keep_processing
 
3860     def _findNextEntry(self
,pos
, adjustInsert
=True): 
3861         """ Find the insertion point for the next valid entry character position.""" 
3862         if self
._isTemplateChar
(pos
):   # if changing fields, pay attn to flag 
3863             adjustInsert 
= adjustInsert
 
3864         else:                           # else within a field; flag not relevant 
3865             adjustInsert 
= False 
3867         while self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3870         # if changing fields, and we've been told to adjust insert point, 
3871         # look at new field; if empty and right-insert field, 
3872         # adjust to right edge: 
3873         if adjustInsert 
and pos 
< self
._masklength
: 
3874             field 
= self
._FindField
(pos
) 
3875             start
, end 
= field
._extent
 
3876             slice = self
._GetValue
()[start
:end
] 
3877             if field
._insertRight 
and field
.IsEmpty(slice): 
3882     def _findNextTemplateChar(self
, pos
): 
3883         """ Find the position of the next non-editable character in the mask.""" 
3884         while not self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3889     def _OnAutoCompleteField(self
, event
): 
3890 ##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) 
3891         pos 
= self
._GetInsertionPoint
() 
3892         field 
= self
._FindField
(pos
) 
3893         edit_start
, edit_end
, slice = self
._FindFieldExtent
(pos
, getslice
=True) 
3896         keycode 
= event
.GetKeyCode() 
3898         if field
._fillChar 
!= ' ': 
3899             text 
= slice.replace(field
._fillChar
, '') 
3903         keep_processing 
= True  # (assume True to start) 
3904 ##        dbg('field._hasList?', field._hasList) 
3906 ##            dbg('choices:', field._choices) 
3907 ##            dbg('compareChoices:', field._compareChoices) 
3908             choices
, choice_required 
= field
._compareChoices
, field
._choiceRequired
 
3909             if keycode 
in (wx
.WXK_PRIOR
, wx
.WXK_UP
): 
3913             match_index
, partial_match 
= self
._autoComplete
(direction
, choices
, text
, compareNoCase
=field
._compareNoCase
, current_index 
= field
._autoCompleteIndex
) 
3914             if( match_index 
is None 
3915                 and (keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3916                      or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown() ) ) ): 
3917                 # Select the 1st thing from the list: 
3920             if( match_index 
is not None 
3921                 and ( keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3922                       or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown()) 
3923                       or (keycode 
== wx
.WXK_DOWN 
and partial_match
) ) ): 
3925                 # We're allowed to auto-complete: 
3926 ##                dbg('match found') 
3927                 value 
= self
._GetValue
() 
3928                 newvalue 
= value
[:edit_start
] + field
._choices
[match_index
] + value
[edit_end
:] 
3929 ##                dbg('setting value to "%s"' % newvalue) 
3930                 self
._SetValue
(newvalue
) 
3931                 self
._SetInsertionPoint
(min(edit_end
, len(newvalue
.rstrip()))) 
3932                 self
._OnAutoSelect
(field
, match_index
) 
3933                 self
._CheckValid
()  # recolor as appopriate 
3936         if keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
): 
3937             # treat as left right arrow if unshifted, tab/shift tab if shifted. 
3938             if event
.ShiftDown(): 
3939                 if keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
): 
3940                     # remove "shifting" and treat as (forward) tab: 
3941                     event
.m_shiftDown 
= False 
3942                 keep_processing 
= self
._OnChangeField
(event
) 
3944                 keep_processing 
= self
._OnArrow
(event
) 
3945         # else some other key; keep processing the key 
3947 ##        dbg('keep processing?', keep_processing, indent=0) 
3948         return keep_processing
 
3951     def _OnAutoSelect(self
, field
, match_index 
= None): 
3953         Function called if autoselect feature is enabled and entire control 
3956 ##        dbg('MaskedEditMixin::OnAutoSelect', field._index) 
3957         if match_index 
is not None: 
3958             field
._autoCompleteIndex 
= match_index
 
3961     def _autoComplete(self
, direction
, choices
, value
, compareNoCase
, current_index
): 
3963         This function gets called in response to Auto-complete events. 
3964         It attempts to find a match to the specified value against the 
3965         list of choices; if exact match, the index of then next 
3966         appropriate value in the list, based on the given direction. 
3967         If not an exact match, it will return the index of the 1st value from 
3968         the choice list for which the partial value can be extended to match. 
3969         If no match found, it will return None. 
3970         The function returns a 2-tuple, with the 2nd element being a boolean 
3971         that indicates if partial match was necessary. 
3973 ##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) 
3975 ##            dbg('nothing to match against', indent=0) 
3976             return (None, False) 
3978         partial_match 
= False 
3981             value 
= value
.lower() 
3983         last_index 
= len(choices
) - 1 
3984         if value 
in choices
: 
3985 ##            dbg('"%s" in', choices) 
3986             if current_index 
is not None and choices
[current_index
] == value
: 
3987                 index 
= current_index
 
3989                 index 
= choices
.index(value
) 
3991 ##            dbg('matched "%s" (%d)' % (choices[index], index)) 
3993 ##                dbg('going to previous') 
3994                 if index 
== 0: index 
= len(choices
) - 1 
3997                 if index 
== len(choices
) - 1: index 
= 0 
3999 ##            dbg('change value to "%s" (%d)' % (choices[index], index)) 
4002             partial_match 
= True 
4003             value 
= value
.strip() 
4004 ##            dbg('no match; try to auto-complete:') 
4006 ##            dbg('searching for "%s"' % value) 
4007             if current_index 
is None: 
4008                 indices 
= range(len(choices
)) 
4013                     indices 
= range(current_index 
+1, len(choices
)) + range(current_index
+1) 
4014 ##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) 
4016                     indices 
= range(current_index
-1, -1, -1) + range(len(choices
)-1, current_index
-1, -1) 
4017 ##                    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) 
4018 ####            dbg('indices:', indices) 
4019             for index 
in indices
: 
4020                 choice 
= choices
[index
] 
4021                 if choice
.find(value
, 0) == 0: 
4022 ##                    dbg('match found:', choice) 
4025                 else: dbg('choice: "%s" - no match' % choice
) 
4026             if match 
is not None: 
4027 ##                dbg('matched', match) 
4030 ##                dbg('no match found') 
4033         return (match
, partial_match
) 
4036     def _AdjustField(self
, pos
): 
4038         This function gets called by default whenever the cursor leaves a field. 
4039         The pos argument given is the char position before leaving that field. 
4040         By default, floating point, integer and date values are adjusted to be 
4041         legal in this function.  Derived classes may override this function 
4042         to modify the value of the control in a different way when changing fields. 
4044         NOTE: these change the value immediately, and restore the cursor to 
4045         the passed location, so that any subsequent code can then move it 
4046         based on the operation being performed. 
4048         newvalue 
= value 
= self
._GetValue
() 
4049         field 
= self
._FindField
(pos
) 
4050         start
, end
, slice = self
._FindFieldExtent
(getslice
=True) 
4051         newfield 
= field
._AdjustField
(slice) 
4052         newvalue 
= value
[:start
] + newfield 
+ value
[end
:] 
4054         if self
._isFloat 
and newvalue 
!= self
._template
: 
4055             newvalue 
= self
._adjustFloat
(newvalue
) 
4057         if self
._ctrl
_constraints
._isInt 
and value 
!= self
._template
: 
4058             newvalue 
= self
._adjustInt
(value
) 
4060         if self
._isDate 
and value 
!= self
._template
: 
4061             newvalue 
= self
._adjustDate
(value
, fixcentury
=True) 
4062             if self
._4digityear
: 
4063                 year2dig 
= self
._dateExtent 
- 2 
4064                 if pos 
== year2dig 
and value
[year2dig
] != newvalue
[year2dig
]: 
4067         if newvalue 
!= value
: 
4068 ##            dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue)) 
4069             self
._SetValue
(newvalue
) 
4070             self
._SetInsertionPoint
(pos
) 
4073     def _adjustKey(self
, pos
, key
): 
4074         """ Apply control formatting to the key (e.g. convert to upper etc). """ 
4075         field 
= self
._FindField
(pos
) 
4076         if field
._forceupper 
and key 
in range(97,123): 
4077             key 
= ord( chr(key
).upper()) 
4079         if field
._forcelower 
and key 
in range(97,123): 
4080             key 
= ord( chr(key
).lower()) 
4085     def _adjustPos(self
, pos
, key
): 
4087         Checks the current insertion point position and adjusts it if 
4088         necessary to skip over non-editable characters. 
4090 ##        dbg('_adjustPos', pos, key, indent=1) 
4091         sel_start
, sel_to 
= self
._GetSelection
() 
4092         # If a numeric or decimal mask, and negatives allowed, reserve the 
4093         # first space for sign, and last one if using parens. 
4095             and ((pos 
== self
._signpos 
and key 
in (ord('-'), ord('+'), ord(' ')) ) 
4096                  or self
._useParens 
and pos 
== self
._masklength 
-1)): 
4097 ##            dbg('adjusted pos:', pos, indent=0) 
4100         if key 
not in self
._nav
: 
4101             field 
= self
._FindField
(pos
) 
4103 ##            dbg('field._insertRight?', field._insertRight) 
4104             if field
._insertRight
:              # if allow right-insert 
4105                 start
, end 
= field
._extent
 
4106                 slice = self
._GetValue
()[start
:end
].strip() 
4107                 field_len 
= end 
- start
 
4108                 if pos 
== end
:                      # if cursor at right edge of field 
4109                     # if not filled or supposed to stay in field, keep current position 
4110 ####                    dbg('pos==end') 
4111 ####                    dbg('len (slice):', len(slice)) 
4112 ####                    dbg('field_len?', field_len) 
4113 ####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) 
4114 ####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) 
4115                     if len(slice) == field_len 
and field
._moveOnFieldFull
: 
4116                         # move cursor to next field: 
4117                         pos 
= self
._findNextEntry
(pos
) 
4118                         self
._SetInsertionPoint
(pos
) 
4120                             self
._SetSelection
(pos
, sel_to
)     # restore selection 
4122                             self
._SetSelection
(pos
, pos
)        # remove selection 
4123                     else: # leave cursor alone 
4126                     # if at start of control, move to right edge 
4127                     if (sel_to 
== sel_start
 
4128                         and (self
._isTemplateChar
(pos
) or (pos 
== start 
and len(slice)+ 1 < field_len
)) 
4130                         pos 
= end                   
# move to right edge 
4131 ##                    elif sel_start <= start and sel_to == end: 
4132 ##                        # select to right edge of field - 1 (to replace char) 
4134 ##                        self._SetInsertionPoint(pos) 
4135 ##                        # restore selection 
4136 ##                        self._SetSelection(sel_start, pos) 
4138                     elif self
._signOk 
and sel_start 
== 0:   # if selected to beginning and signed, 
4139                         # adjust to past reserved sign position: 
4140                         pos 
= self
._fields
[0]._extent
[0] 
4141                         self
._SetInsertionPoint
(pos
) 
4143                         self
._SetSelection
(pos
, sel_to
) 
4145                         pass    # leave position/selection alone 
4147             # else make sure the user is not trying to type over a template character 
4148             # If they are, move them to the next valid entry position 
4149             elif self
._isTemplateChar
(pos
): 
4150                 if( not field
._moveOnFieldFull
 
4151                       and (not self
._signOk
 
4153                                and field
._index 
== 0 
4154                                and pos 
> 0) ) ):      # don't move to next field without explicit cursor movement 
4157                     # find next valid position 
4158                     pos 
= self
._findNextEntry
(pos
) 
4159                     self
._SetInsertionPoint
(pos
) 
4160                     if pos 
< sel_to
:    # restore selection 
4161                         self
._SetSelection
(pos
, sel_to
) 
4163                         self
._SetSelection
(pos
, pos
) 
4164 ##        dbg('adjusted pos:', pos, indent=0) 
4168     def _adjustFloat(self
, candidate
=None): 
4170         'Fixes' an floating point control. Collapses spaces, right-justifies, etc. 
4172 ##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) 
4173         lenInt
,lenFraction  
= [len(s
) for s 
in self
._mask
.split('.')]  ## Get integer, fraction lengths 
4175         if candidate 
is None: value 
= self
._GetValue
() 
4176         else: value 
= candidate
 
4177 ##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) 
4178         intStr
, fracStr 
= value
.split(self
._decimalChar
) 
4180         intStr 
= self
._fields
[0]._AdjustField
(intStr
) 
4181 ##        dbg('adjusted intStr: "%s"' % intStr) 
4182         lenInt 
= len(intStr
) 
4183         fracStr 
= fracStr 
+ ('0'*(lenFraction
-len(fracStr
)))  # add trailing spaces to decimal 
4185 ##        dbg('intStr "%(intStr)s"' % locals()) 
4186 ##        dbg('lenInt:', lenInt) 
4188         intStr 
= string
.rjust( intStr
[-lenInt
:], lenInt
) 
4189 ##        dbg('right-justifed intStr = "%(intStr)s"' % locals()) 
4190         newvalue 
= intStr 
+ self
._decimalChar 
+ fracStr
 
4193             if len(newvalue
) < self
._masklength
: 
4194                 newvalue 
= ' ' + newvalue
 
4195             signedvalue 
= self
._getSignedValue
(newvalue
)[0] 
4196             if signedvalue 
is not None: newvalue 
= signedvalue
 
4198         # Finally, align string with decimal position, left-padding with 
4200         newdecpos 
= newvalue
.find(self
._decimalChar
) 
4201         if newdecpos 
< self
._decimalpos
: 
4202             padlen 
= self
._decimalpos 
- newdecpos
 
4203             newvalue 
= string
.join([' ' * padlen
] + [newvalue
] ,'') 
4205         if self
._signOk 
and self
._useParens
: 
4206             if newvalue
.find('(') != -1: 
4207                 newvalue 
= newvalue
[:-1] + ')' 
4209                 newvalue 
= newvalue
[:-1] + ' ' 
4211 ##        dbg('newvalue = "%s"' % newvalue) 
4212         if candidate 
is None: 
4213             wx
.CallAfter(self
._SetValue
, newvalue
) 
4218     def _adjustInt(self
, candidate
=None): 
4219         """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" 
4220 ##        dbg("MaskedEditMixin::_adjustInt", candidate) 
4221         lenInt 
= self
._masklength
 
4222         if candidate 
is None: value 
= self
._GetValue
() 
4223         else: value 
= candidate
 
4225         intStr 
= self
._fields
[0]._AdjustField
(value
) 
4226         intStr 
= intStr
.strip() # drop extra spaces 
4227 ##        dbg('adjusted field: "%s"' % intStr) 
4229         if self
._isNeg 
and intStr
.find('-') == -1 and intStr
.find('(') == -1: 
4231                 intStr 
= '(' + intStr 
+ ')' 
4233                 intStr 
= '-' + intStr
 
4234         elif self
._isNeg 
and intStr
.find('-') != -1 and self
._useParens
: 
4235             intStr 
= intStr
.replace('-', '(') 
4237         if( self
._signOk 
and ((self
._useParens 
and intStr
.find('(') == -1) 
4238                                 or (not self
._useParens 
and intStr
.find('-') == -1))): 
4239             intStr 
= ' ' + intStr
 
4241                 intStr 
+= ' '   # space for right paren position 
4243         elif self
._signOk 
and self
._useParens 
and intStr
.find('(') != -1 and intStr
.find(')') == -1: 
4244             # ensure closing right paren: 
4247         if self
._fields
[0]._alignRight
:     ## Only if right-alignment is enabled 
4248             intStr 
= intStr
.rjust( lenInt 
) 
4250             intStr 
= intStr
.ljust( lenInt 
) 
4252         if candidate 
is None: 
4253             wx
.CallAfter(self
._SetValue
, intStr 
) 
4257     def _adjustDate(self
, candidate
=None, fixcentury
=False, force4digit_year
=False): 
4259         'Fixes' a date control, expanding the year if it can. 
4260         Applies various self-formatting options. 
4262 ##        dbg("MaskedEditMixin::_adjustDate", indent=1) 
4263         if candidate 
is None: text    
= self
._GetValue
() 
4264         else: text 
= candidate
 
4265 ##        dbg('text=', text) 
4266         if self
._datestyle 
== "YMD": 
4271 ##        dbg('getYear: "%s"' % _getYear(text, self._datestyle)) 
4272         year    
= string
.replace( _getYear( text
, self
._datestyle
),self
._fields
[year_field
]._fillChar
,"")  # drop extra fillChars 
4273         month   
= _getMonth( text
, self
._datestyle
) 
4274         day     
= _getDay( text
, self
._datestyle
) 
4275 ##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) 
4278         yearstart 
= self
._dateExtent 
- 4 
4282                  or (self
._GetInsertionPoint
() > yearstart
+1 and text
[yearstart
+2] == ' ') 
4283                  or (self
._GetInsertionPoint
() > yearstart
+2 and text
[yearstart
+3] == ' ') ) ): 
4284             ## user entered less than four digits and changing fields or past point where we could 
4285             ## enter another digit: 
4289 ##                dbg('bad year=', year) 
4290                 year 
= text
[yearstart
:self
._dateExtent
] 
4292         if len(year
) < 4 and yearVal
: 
4294                 # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the 
4296                 now 
= wx
.DateTime_Now() 
4297                 century 
= (now
.GetYear() /100) * 100        # "this century" 
4298                 twodig_year 
= now
.GetYear() - century       
# "this year" (2 digits) 
4299                 # if separation between today's 2-digit year and typed value > 50, 
4300                 #      assume last century, 
4301                 # else assume this century. 
4303                 # Eg: if 2003 and yearVal == 30, => 2030 
4304                 #     if 2055 and yearVal == 80, => 2080 
4305                 #     if 2010 and yearVal == 96, => 1996 
4307                 if abs(yearVal 
- twodig_year
) > 50: 
4308                     yearVal 
= (century 
- 100) + yearVal
 
4310                     yearVal 
= century 
+ yearVal
 
4311                 year 
= str( yearVal 
) 
4312             else:   # pad with 0's to make a 4-digit year 
4313                 year 
= "%04d" % yearVal
 
4314             if self
._4digityear 
or force4digit_year
: 
4315                 text 
= _makeDate(year
, month
, day
, self
._datestyle
, text
) + text
[self
._dateExtent
:] 
4316 ##        dbg('newdate: "%s"' % text, indent=0) 
4320     def _goEnd(self
, getPosOnly
=False): 
4321         """ Moves the insertion point to the end of user-entry """ 
4322 ##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) 
4323         text 
= self
._GetValue
() 
4324 ####        dbg('text: "%s"' % text) 
4326         if len(text
.rstrip()): 
4327             for i 
in range( min( self
._masklength
-1, len(text
.rstrip())), -1, -1): 
4328 ####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) 
4329                 if self
._isMaskChar
(i
): 
4331 ####                    dbg("text[%d]: '%s'" % (i, char)) 
4337             pos 
= self
._goHome
(getPosOnly
=True) 
4339             pos 
= min(i
,self
._masklength
) 
4341         field 
= self
._FindField
(pos
) 
4342         start
, end 
= field
._extent
 
4343         if field
._insertRight 
and pos 
< end
: 
4345 ##        dbg('next pos:', pos) 
4350             self
._SetInsertionPoint
(pos
) 
4353     def _goHome(self
, getPosOnly
=False): 
4354         """ Moves the insertion point to the beginning of user-entry """ 
4355 ##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) 
4356         text 
= self
._GetValue
() 
4357         for i 
in range(self
._masklength
): 
4358             if self
._isMaskChar
(i
): 
4365             self
._SetInsertionPoint
(max(i
,0)) 
4369     def _getAllowedChars(self
, pos
): 
4370         """ Returns a string of all allowed user input characters for the provided 
4371             mask character plus control options 
4373         maskChar 
= self
.maskdict
[pos
] 
4374         okchars 
= self
.maskchardict
[maskChar
]    ## entry, get mask approved characters 
4376         # convert okchars to unicode if required; will force subsequent appendings to 
4377         # result in unicode strings 
4378         if 'unicode' in wx
.PlatformInfo 
and type(okchars
) != types
.UnicodeType
: 
4379             okchars 
= okchars
.decode(self
._defaultEncoding
) 
4381         field 
= self
._FindField
(pos
) 
4382         if okchars 
and field
._okSpaces
:          ## Allow spaces? 
4384         if okchars 
and field
._includeChars
:      ## any additional included characters? 
4385             okchars 
+= field
._includeChars
 
4386 ####        dbg('okchars[%d]:' % pos, okchars) 
4390     def _isMaskChar(self
, pos
): 
4391         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) 
4393         if pos 
< self
._masklength
: 
4394             return self
.ismasked
[pos
] 
4399     def _isTemplateChar(self
,Pos
): 
4400         """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) 
4402         if Pos 
< self
._masklength
: 
4403             return not self
._isMaskChar
(Pos
) 
4408     def _isCharAllowed(self
, char
, pos
, checkRegex
=False, allowAutoSelect
=True, ignoreInsertRight
=False): 
4409         """ Returns True if character is allowed at the specific position, otherwise False.""" 
4410 ##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1) 
4411         field 
= self
._FindField
(pos
) 
4412         right_insert 
= False 
4414         if self
.controlInitialized
: 
4415             sel_start
, sel_to 
= self
._GetSelection
() 
4417             sel_start
, sel_to 
= pos
, pos
 
4419         if (field
._insertRight 
or self
._ctrl
_constraints
._insertRight
) and not ignoreInsertRight
: 
4420             start
, end 
= field
._extent
 
4421             field_len 
= end 
- start
 
4422             if self
.controlInitialized
: 
4423                 value 
= self
._GetValue
() 
4424                 fstr 
= value
[start
:end
].strip() 
4426                     while fstr 
and fstr
[0] == '0': 
4428                 input_len 
= len(fstr
) 
4429                 if self
._signOk 
and '-' in fstr 
or '(' in fstr
: 
4430                     input_len 
-= 1  # sign can move out of field, so don't consider it in length 
4432                 value 
= self
._template
 
4433                 input_len 
= 0   # can't get the current "value", so use 0 
4436             # if entire field is selected or position is at end and field is not full, 
4437             # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar: 
4438             if( (sel_start
, sel_to
) == field
._extent
 
4439                 or (pos 
== end 
and input_len 
< field_len
)): 
4441 ##                dbg('pos = end - 1 = ', pos, 'right_insert? 1') 
4443             elif( field
._allowInsert 
and sel_start 
== sel_to
 
4444                   and (sel_to 
== end 
or (sel_to 
< self
._masklength 
and value
[sel_start
] != field
._fillChar
)) 
4445                   and input_len 
< field_len 
): 
4446                 pos 
= sel_to 
- 1    # where character will go 
4447 ##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') 
4449             # else leave pos alone... 
4451 ##                dbg('pos stays ', pos, 'right_insert? 0') 
4454         if self
._isTemplateChar
( pos 
):  ## if a template character, return empty 
4455 ##            dbg('%d is a template character; returning False' % pos, indent=0) 
4458         if self
._isMaskChar
( pos 
): 
4459             okChars  
= self
._getAllowedChars
(pos
) 
4461             if self
._fields
[0]._groupdigits 
and (self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
)): 
4462                 okChars 
+= self
._fields
[0]._groupChar
 
4465                 if self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
): 
4469                 elif self
._useParens 
and (self
._isInt 
or (self
._isFloat 
and pos 
> self
._decimalpos
)): 
4472 ####            dbg('%s in %s?' % (char, okChars), char in okChars) 
4473             approved 
= char 
in okChars
 
4475             if approved 
and checkRegex
: 
4476 ##                dbg("checking appropriate regex's") 
4477                 value 
= self
._eraseSelection
(self
._GetValue
()) 
4483                     newvalue
, ignore
, ignore
, ignore
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
, allowAutoSelect
=True) 
4485                     newvalue
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
) 
4486 ##                dbg('newvalue: "%s"' % newvalue) 
4488                 fields 
= [self
._FindField
(pos
)] + [self
._ctrl
_constraints
] 
4489                 for field 
in fields
:    # includes fields[-1] == "ctrl_constraints" 
4490                     if field
._regexMask 
and field
._filter
: 
4491 ##                        dbg('checking vs. regex') 
4492                         start
, end 
= field
._extent
 
4493                         slice = newvalue
[start
:end
] 
4494                         approved 
= (re
.match( field
._filter
, slice) is not None) 
4495 ##                        dbg('approved?', approved) 
4496                     if not approved
: break 
4500 ##            dbg('%d is a !???! character; returning False', indent=0) 
4504     def _applyFormatting(self
): 
4505         """ Apply formatting depending on the control's state. 
4506             Need to find a way to call this whenever the value changes, in case the control's 
4507             value has been changed or set programatically. 
4510 ##        dbg('MaskedEditMixin::_applyFormatting', indent=1) 
4512         # Handle negative numbers 
4514             text
, signpos
, right_signpos 
= self
._getSignedValue
() 
4515 ##            dbg('text: "%s", signpos:' % text, signpos) 
4516             if not text 
or text
[signpos
] not in ('-','('): 
4518 ##                dbg('no valid sign found; new sign:', self._isNeg) 
4519                 if text 
and signpos 
!= self
._signpos
: 
4520                     self
._signpos 
= signpos
 
4521             elif text 
and self
._valid 
and not self
._isNeg 
and text
[signpos
] in ('-', '('): 
4522 ##                dbg('setting _isNeg to True') 
4524 ##            dbg('self._isNeg:', self._isNeg) 
4526         if self
._signOk 
and self
._isNeg
: 
4527             fc 
= self
._signedForegroundColour
 
4529             fc 
= self
._foregroundColour
 
4531         if hasattr(fc
, '_name'): 
4535 ##        dbg('setting foreground to', c) 
4536         self
.SetForegroundColour(fc
) 
4541                 bc 
= self
._emptyBackgroundColour
 
4543                 bc 
= self
._validBackgroundColour
 
4546             bc 
= self
._invalidBackgroundColour
 
4547         if hasattr(bc
, '_name'): 
4551 ##        dbg('setting background to', c) 
4552         self
.SetBackgroundColour(bc
) 
4554 ##        dbg(indent=0, suspend=0) 
4557     def _getAbsValue(self
, candidate
=None): 
4558         """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). 
4560 ##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) 
4561         if candidate 
is None: text 
= self
._GetValue
() 
4562         else: text 
= candidate
 
4563         right_signpos 
= text
.find(')') 
4566             if self
._ctrl
_constraints
._alignRight 
and self
._fields
[0]._fillChar 
== ' ': 
4567                 signpos 
= text
.find('-') 
4569 ##                    dbg('no - found; searching for (') 
4570                     signpos 
= text
.find('(') 
4572 ##                    dbg('- found at', signpos) 
4576 ##                    dbg('signpos still -1') 
4577 ##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) 
4578                     if len(text
) < self
._masklength
: 
4580                     if len(text
) < self
._masklength
: 
4582                     if len(text
) > self
._masklength 
and text
[-1] in (')', ' '): 
4585 ##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) 
4586 ##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) 
4587                         signpos 
= len(text
) - (len(text
.lstrip()) + 1) 
4589                         if self
._useParens 
and not text
.strip(): 
4590                             signpos 
-= 1    # empty value; use penultimate space 
4591 ##                dbg('signpos:', signpos) 
4593                     text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4598                     text 
= self
._template
[0] + text
[1:] 
4602             if right_signpos 
!= -1: 
4604                     text 
= text
[:right_signpos
] + ' ' + text
[right_signpos
+1:] 
4605                 elif len(text
) > self
._masklength
: 
4606                     text 
= text
[:right_signpos
] + text
[right_signpos
+1:] 
4610             elif self
._useParens 
and self
._signOk
: 
4611                 # figure out where it ought to go: 
4612                 right_signpos 
= self
._masklength 
- 1     # initial guess 
4613                 if not self
._ctrl
_constraints
._alignRight
: 
4614 ##                    dbg('not right-aligned') 
4615                     if len(text
.strip()) == 0: 
4616                         right_signpos 
= signpos 
+ 1 
4617                     elif len(text
.strip()) < self
._masklength
: 
4618                         right_signpos 
= len(text
.rstrip()) 
4619 ##                dbg('right_signpos:', right_signpos) 
4621             groupchar 
= self
._fields
[0]._groupChar
 
4623                 value 
= long(text
.replace(groupchar
,'').replace('(','-').replace(')','').replace(' ', '')) 
4625 ##                dbg('invalid number', indent=0) 
4626                 return None, signpos
, right_signpos
 
4630                 groupchar 
= self
._fields
[0]._groupChar
 
4631                 value 
= float(text
.replace(groupchar
,'').replace(self
._decimalChar
, '.').replace('(', '-').replace(')','').replace(' ', '')) 
4632 ##                dbg('value:', value) 
4636             if value 
< 0 and value 
is not None: 
4637                 signpos 
= text
.find('-') 
4639                     signpos 
= text
.find('(') 
4641                 text 
= text
[:signpos
] + self
._template
[signpos
] + text
[signpos
+1:] 
4643                 # look forwards up to the decimal point for the 1st non-digit 
4644 ##                dbg('decimal pos:', self._decimalpos) 
4645 ##                dbg('text: "%s"' % text) 
4647                     signpos 
= self
._decimalpos 
- (len(text
[:self
._decimalpos
].lstrip()) + 1) 
4648                     # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET 
4649                     if len(text
) >= signpos
+1 and  text
[signpos
+1] in ('-','('): 
4653 ##                dbg('signpos:', signpos) 
4657                     right_signpos 
= self
._masklength 
- 1 
4658                     text 
= text
[:right_signpos
] + ' ' 
4659                     if text
[signpos
] == '(': 
4660                         text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4662                     right_signpos 
= text
.find(')') 
4663                     if right_signpos 
!= -1: 
4668 ##                dbg('invalid number') 
4671 ##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) 
4673         return text
, signpos
, right_signpos
 
4676     def _getSignedValue(self
, candidate
=None): 
4677         """ Return a signed value by adding a "-" prefix if the value 
4678             is set to negative, or a space if positive. 
4680 ##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) 
4681         if candidate 
is None: text 
= self
._GetValue
() 
4682         else: text 
= candidate
 
4685         abstext
, signpos
, right_signpos 
= self
._getAbsValue
(text
) 
4689                 return abstext
, signpos
, right_signpos
 
4691             if self
._isNeg 
or text
[signpos
] in ('-', '('): 
4698             if abstext
[signpos
] not in string
.digits
: 
4699                 text 
= abstext
[:signpos
] + sign 
+ abstext
[signpos
+1:] 
4701                 # this can happen if value passed is too big; sign assumed to be 
4702                 # in position 0, but if already filled with a digit, prepend sign... 
4703                 text 
= sign 
+ abstext
 
4704             if self
._useParens 
and text
.find('(') != -1: 
4705                 text 
= text
[:right_signpos
] + ')' + text
[right_signpos
+1:] 
4708 ##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) 
4710         return text
, signpos
, right_signpos
 
4713     def GetPlainValue(self
, candidate
=None): 
4714         """ Returns control's value stripped of the template text. 
4715             plainvalue = MaskedEditMixin.GetPlainValue() 
4717 ##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) 
4719         if candidate 
is None: text 
= self
._GetValue
() 
4720         else: text 
= candidate
 
4723 ##            dbg('returned ""', indent=0) 
4727             for idx 
in range( min(len(self
._template
), len(text
)) ): 
4728                 if self
._mask
[idx
] in maskchars
: 
4731             if self
._isFloat 
or self
._isInt
: 
4732 ##                dbg('plain so far: "%s"' % plain) 
4733                 plain 
= plain
.replace('(', '-').replace(')', ' ') 
4734 ##                dbg('plain after sign regularization: "%s"' % plain) 
4736                 if self
._signOk 
and self
._isNeg 
and plain
.count('-') == 0: 
4737                     # must be in reserved position; add to "plain value" 
4738                     plain 
= '-' + plain
.strip() 
4740                 if self
._fields
[0]._alignRight
: 
4741                     lpad 
= plain
.count(',') 
4742                     plain 
= ' ' * lpad 
+ plain
.replace(',','') 
4744                     plain 
= plain
.replace(',','') 
4745 ##                dbg('plain after pad and group:"%s"' % plain) 
4747 ##            dbg('returned "%s"' % plain.rstrip(), indent=0) 
4748             return plain
.rstrip() 
4751     def IsEmpty(self
, value
=None): 
4753         Returns True if control is equal to an empty value. 
4754         (Empty means all editable positions in the template == fillChar.) 
4756         if value 
is None: value 
= self
._GetValue
() 
4757         if value 
== self
._template 
and not self
._defaultValue
: 
4758 ####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") 
4759             return True     # (all mask chars == fillChar by defn) 
4760         elif value 
== self
._template
: 
4762             for pos 
in range(len(self
._template
)): 
4763 ####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) 
4764 ####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) 
4765                 if self
._isMaskChar
(pos
) and value
[pos
] not in (' ', self
._fillChar
[pos
]): 
4767 ####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) 
4770 ####            dbg("IsEmpty? 0 (value doesn't match template)") 
4774     def IsDefault(self
, value
=None): 
4776         Returns True if the value specified (or the value of the control if not specified) 
4777         is equal to the default value. 
4779         if value 
is None: value 
= self
._GetValue
() 
4780         return value 
== self
._template
 
4783     def IsValid(self
, value
=None): 
4784         """ Indicates whether the value specified (or the current value of the control 
4785         if not specified) is considered valid.""" 
4786 ####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) 
4787         if value 
is None: value 
= self
._GetValue
() 
4788         ret 
= self
._CheckValid
(value
) 
4793     def _eraseSelection(self
, value
=None, sel_start
=None, sel_to
=None): 
4794         """ Used to blank the selection when inserting a new character. """ 
4795 ##        dbg("MaskedEditMixin::_eraseSelection", indent=1) 
4796         if value 
is None: value 
= self
._GetValue
() 
4797         if sel_start 
is None or sel_to 
is None: 
4798             sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
4799 ##        dbg('value: "%s"' % value) 
4800 ##        dbg("current sel_start, sel_to:", sel_start, sel_to) 
4802         newvalue 
= list(value
) 
4803         for i 
in range(sel_start
, sel_to
): 
4804             if self
._signOk 
and newvalue
[i
] in ('-', '(', ')'): 
4805 ##                dbg('found sign (%s) at' % newvalue[i], i) 
4807                 # balance parentheses: 
4808                 if newvalue
[i
] == '(': 
4809                     right_signpos 
= value
.find(')') 
4810                     if right_signpos 
!= -1: 
4811                         newvalue
[right_signpos
] = ' ' 
4813                 elif newvalue
[i
] == ')': 
4814                     left_signpos 
= value
.find('(') 
4815                     if left_signpos 
!= -1: 
4816                         newvalue
[left_signpos
] = ' ' 
4820             elif self
._isMaskChar
(i
): 
4821                 field 
= self
._FindField
(i
) 
4825                     newvalue
[i
] = self
._template
[i
] 
4827         value 
= string
.join(newvalue
,"") 
4828 ##        dbg('new value: "%s"' % value) 
4833     def _insertKey(self
, char
, pos
, sel_start
, sel_to
, value
, allowAutoSelect
=False): 
4834         """ Handles replacement of the character at the current insertion point.""" 
4835 ##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) 
4837         text 
= self
._eraseSelection
(value
) 
4838         field 
= self
._FindField
(pos
) 
4839         start
, end 
= field
._extent
 
4843         # if >= 2 chars selected in a right-insert field, do appropriate erase on field, 
4844         # then set selection to end, and do usual right insert. 
4845         if sel_start 
!= sel_to 
and sel_to 
>= sel_start
+2: 
4846             field 
= self
._FindField
(sel_start
) 
4847             if( field
._insertRight                          
# if right-insert 
4848                 and field
._allowInsert                      
# and allow insert at any point in field 
4849                 and field 
== self
._FindField
(sel_to
) ):     # and selection all in same field 
4850                 text 
= self
._OnErase
(just_return_value
=True)    # remove selection before insert 
4851 ##                dbg('text after (left)erase: "%s"' % text) 
4852                 pos 
= sel_start 
= sel_to
 
4854         if pos 
!= sel_start 
and sel_start 
== sel_to
: 
4855             # adjustpos must have moved the position; make selection match: 
4856             sel_start 
= sel_to 
= pos
 
4858 ##        dbg('field._insertRight?', field._insertRight) 
4859 ##        dbg('field._allowInsert?', field._allowInsert) 
4860 ##        dbg('sel_start, end', sel_start, end) 
4862 ##            dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar) 
4865         if( field
._insertRight                                  
# field allows right insert 
4866             and ((sel_start
, sel_to
) == field
._extent           
# and whole field selected 
4867                  or (sel_start 
== sel_to                        
# or nothing selected 
4868                      and (sel_start 
== end                      
# and cursor at right edge 
4869                           or (field
._allowInsert                
# or field allows right-insert 
4870                               and sel_start 
< end               
# next to other char in field: 
4871                               and text
[sel_start
] != field
._fillChar
) ) ) ) ): 
4872 ##            dbg('insertRight') 
4873             fstr 
= text
[start
:end
] 
4874             erasable_chars 
= [field
._fillChar
, ' '] 
4877                 erasable_chars
.append('0') 
4880 ####            dbg("fstr[0]:'%s'" % fstr[0]) 
4881 ####            dbg('field_index:', field._index) 
4882 ####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) 
4883 ####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", 
4884 ##                 self._signOk and field._index == 0 and fstr[0] in ('-','(')) 
4885             if fstr
[0] in erasable_chars 
or (self
._signOk 
and field
._index 
== 0 and fstr
[0] in ('-','(')): 
4887 ####                dbg('value:      "%s"' % text) 
4888 ####                dbg('fstr:       "%s"' % fstr) 
4889 ####                dbg("erased:     '%s'" % erased) 
4890                 field_sel_start 
= sel_start 
- start
 
4891                 field_sel_to 
= sel_to 
- start
 
4892 ##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start]) 
4893 ##                dbg('right fstr: "%s"' % fstr[field_sel_to:end]) 
4894                 fstr 
= fstr
[1:field_sel_start
] + char 
+ fstr
[field_sel_to
:end
] 
4895             if field
._alignRight 
and sel_start 
!= sel_to
: 
4896                 field_len 
= end 
- start
 
4897 ##                pos += (field_len - len(fstr))    # move cursor right by deleted amount 
4899 ##                dbg('setting pos to:', pos) 
4901                     fstr 
= '0' * (field_len 
- len(fstr
)) + fstr
 
4903                     fstr 
= fstr
.rjust(field_len
)   # adjust the field accordingly 
4904 ##            dbg('field str: "%s"' % fstr) 
4906             newtext 
= text
[:start
] + fstr 
+ text
[end
:] 
4907             if erased 
in ('-', '(') and self
._signOk
: 
4908                 newtext 
= erased 
+ newtext
[1:] 
4909 ##            dbg('newtext: "%s"' % newtext) 
4911             if self
._signOk 
and field
._index 
== 0: 
4912                 start 
-= 1             # account for sign position 
4914 ####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull) 
4915 ####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) 
4916             if( field
._moveOnFieldFull 
and pos 
== end
 
4917                 and len(fstr
.lstrip()) == end
-start
):   # if field now full 
4918                 newpos 
= self
._findNextEntry
(end
)       #   go to next field 
4920                 newpos 
= pos                            
# else keep cursor at current position 
4923 ##            dbg('not newtext') 
4925 ##                dbg('newpos:', newpos) 
4927             if self
._signOk 
and self
._useParens
: 
4928                 old_right_signpos 
= text
.find(')') 
4930             if field
._allowInsert 
and not field
._insertRight 
and sel_to 
<= end 
and sel_start 
>= start
: 
4931                 # inserting within a left-insert-capable field 
4932                 field_len 
= end 
- start
 
4933                 before 
= text
[start
:sel_start
] 
4934                 after 
= text
[sel_to
:end
].strip() 
4935 ####                dbg("current field:'%s'" % text[start:end]) 
4936 ####                dbg("before:'%s'" % before, "after:'%s'" % after) 
4937                 new_len 
= len(before
) + len(after
) + 1 # (for inserted char) 
4938 ####                dbg('new_len:', new_len) 
4940                 if new_len 
< field_len
: 
4941                     retained 
= after 
+ self
._template
[end
-(field_len
-new_len
):end
] 
4942                 elif new_len 
> end
-start
: 
4943                     retained 
= after
[1:] 
4947                 left 
= text
[0:start
] + before
 
4948 ####                dbg("left:'%s'" % left, "retained:'%s'" % retained) 
4949                 right   
= retained 
+ text
[end
:] 
4952                 right   
= text
[pos
+1:] 
4954             if 'unicode' in wx
.PlatformInfo 
and type(char
) != types
.UnicodeType
: 
4955                 # convert the keyboard constant to a unicode value, to 
4956                 # ensure it can be concatenated into the control value: 
4957                 char 
= char
.decode(self
._defaultEncoding
) 
4959             newtext 
= left 
+ char 
+ right
 
4961             if self
._signOk 
and self
._useParens
: 
4962                 # Balance parentheses: 
4963                 left_signpos 
= newtext
.find('(') 
4965                 if left_signpos 
== -1:     # erased '('; remove ')' 
4966                     right_signpos 
= newtext
.find(')') 
4967                     if right_signpos 
!= -1: 
4968                         newtext 
= newtext
[:right_signpos
] + ' ' + newtext
[right_signpos
+1:] 
4970                 elif old_right_signpos 
!= -1: 
4971                     right_signpos 
= newtext
.find(')') 
4973                     if right_signpos 
== -1: # just replaced right-paren 
4974                         if newtext
[pos
] == ' ': # we just erased '); erase '(' 
4975                             newtext 
= newtext
[:left_signpos
] + ' ' + newtext
[left_signpos
+1:] 
4976                         else:   # replaced with digit; move ') over 
4977                             if self
._ctrl
_constraints
._alignRight 
or self
._isFloat
: 
4978                                 newtext 
= newtext
[:-1] + ')' 
4980                                 rstripped_text 
= newtext
.rstrip() 
4981                                 right_signpos 
= len(rstripped_text
) 
4982 ##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) 
4983                                 newtext 
= newtext
[:right_signpos
] + ')' + newtext
[right_signpos
+1:] 
4985             if( field
._insertRight                                  
# if insert-right field (but we didn't start at right edge) 
4986                 and field
._moveOnFieldFull                          
# and should move cursor when full 
4987                 and len(newtext
[start
:end
].strip()) == end
-start
):  # and field now full 
4988                 newpos 
= self
._findNextEntry
(end
)                   #   go to next field 
4989 ##                dbg('newpos = nextentry =', newpos) 
4991 ##                dbg('pos:', pos, 'newpos:', pos+1) 
4996             new_select_to 
= newpos     
# (default return values) 
5000             if field
._autoSelect
: 
5001                 match_index
, partial_match 
= self
._autoComplete
(1,  # (always forward) 
5002                                                                 field
._compareChoices
, 
5004                                                                 compareNoCase
=field
._compareNoCase
, 
5005                                                                 current_index 
= field
._autoCompleteIndex
-1) 
5006                 if match_index 
is not None and partial_match
: 
5007                     matched_str 
= newtext
[start
:end
] 
5008                     newtext 
= newtext
[:start
] + field
._choices
[match_index
] + newtext
[end
:] 
5011                     if field
._insertRight
: 
5012                         # adjust position to just after partial match in field 
5013                         newpos 
= end 
- (len(field
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5015             elif self
._ctrl
_constraints
._autoSelect
: 
5016                 match_index
, partial_match 
= self
._autoComplete
( 
5017                                         1,  # (always forward) 
5018                                         self
._ctrl
_constraints
._compareChoices
, 
5020                                         self
._ctrl
_constraints
._compareNoCase
, 
5021                                         current_index 
= self
._ctrl
_constraints
._autoCompleteIndex 
- 1) 
5022                 if match_index 
is not None and partial_match
: 
5023                     matched_str 
= newtext
 
5024                     newtext 
= self
._ctrl
_constraints
._choices
[match_index
] 
5025                     edit_end 
= self
._ctrl
_constraints
._extent
[1] 
5026                     new_select_to 
= min(edit_end
, len(newtext
.rstrip())) 
5027                     match_field 
= self
._ctrl
_constraints
 
5028                     if self
._ctrl
_constraints
._insertRight
: 
5029                         # adjust position to just after partial match in control: 
5030                         newpos 
= self
._masklength 
- (len(self
._ctrl
_constraints
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
5032 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) 
5034             return newtext
, newpos
, new_select_to
, match_field
, match_index
 
5036 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos) 
5038             return newtext
, newpos
 
5041     def _OnFocus(self
,event
): 
5043         This event handler is currently necessary to work around new default 
5044         behavior as of wxPython2.3.3; 
5045         The TAB key auto selects the entire contents of the wx.TextCtrl *after* 
5046         the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection 
5047         *here*, because it hasn't happened yet.  So to prevent this behavior, and 
5048         preserve the correct selection when the focus event is not due to tab, 
5049         we need to pull the following trick: 
5051 ##        dbg('MaskedEditMixin::_OnFocus') 
5052         if self
.IsBeingDeleted() or self
.GetParent().IsBeingDeleted(): 
5054         wx
.CallAfter(self
._fixSelection
) 
5059     def _CheckValid(self
, candidate
=None): 
5061         This is the default validation checking routine; It verifies that the 
5062         current value of the control is a "valid value," and has the side 
5063         effect of coloring the control appropriately. 
5066 ##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) 
5067         oldValid 
= self
._valid
 
5068         if candidate 
is None: value 
= self
._GetValue
() 
5069         else: value 
= candidate
 
5070 ##        dbg('value: "%s"' % value) 
5072         valid 
= True    # assume True 
5074         if not self
.IsDefault(value
) and self
._isDate
:                    ## Date type validation 
5075             valid 
= self
._validateDate
(value
) 
5076 ##            dbg("valid date?", valid) 
5078         elif not self
.IsDefault(value
) and self
._isTime
: 
5079             valid 
= self
._validateTime
(value
) 
5080 ##            dbg("valid time?", valid) 
5082         elif not self
.IsDefault(value
) and (self
._isInt 
or self
._isFloat
):  ## Numeric type 
5083             valid 
= self
._validateNumeric
(value
) 
5084 ##            dbg("valid Number?", valid) 
5086         if valid
:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault() 
5087             ## valid so far; ensure also allowed by any list or regex provided: 
5088             valid 
= self
._validateGeneric
(value
) 
5089 ##            dbg("valid value?", valid) 
5091 ##        dbg('valid?', valid) 
5095             self
._applyFormatting
() 
5096             if self
._valid 
!= oldValid
: 
5097 ##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) 
5098 ##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) 
5100 ##        dbg(indent=0, suspend=0) 
5104     def _validateGeneric(self
, candidate
=None): 
5105         """ Validate the current value using the provided list or Regex filter (if any). 
5107         if candidate 
is None: 
5108             text 
= self
._GetValue
() 
5112         valid 
= True    # assume True 
5113         for i 
in [-1] + self
._field
_indices
:   # process global constraints first: 
5114             field 
= self
._fields
[i
] 
5115             start
, end 
= field
._extent
 
5116             slice = text
[start
:end
] 
5117             valid 
= field
.IsValid(slice) 
5124     def _validateNumeric(self
, candidate
=None): 
5125         """ Validate that the value is within the specified range (if specified.)""" 
5126         if candidate 
is None: value 
= self
._GetValue
() 
5127         else: value 
= candidate
 
5129             groupchar 
= self
._fields
[0]._groupChar
 
5131                 number 
= float(value
.replace(groupchar
, '').replace(self
._decimalChar
, '.').replace('(', '-').replace(')', '')) 
5133                 number 
= long( value
.replace(groupchar
, '').replace('(', '-').replace(')', '')) 
5135                     if self
._fields
[0]._alignRight
: 
5136                         require_digit_at 
= self
._fields
[0]._extent
[1]-1 
5138                         require_digit_at 
= self
._fields
[0]._extent
[0] 
5139 ##                    dbg('require_digit_at:', require_digit_at) 
5140 ##                    dbg("value[rda]: '%s'" % value[require_digit_at]) 
5141                     if value
[require_digit_at
] not in list(string
.digits
): 
5145 ##            dbg('number:', number) 
5146             if self
._ctrl
_constraints
._hasRange
: 
5147                 valid 
= self
._ctrl
_constraints
._rangeLow 
<= number 
<= self
._ctrl
_constraints
._rangeHigh
 
5150             groupcharpos 
= value
.rfind(groupchar
) 
5151             if groupcharpos 
!= -1:  # group char present 
5152 ##                dbg('groupchar found at', groupcharpos) 
5153                 if self
._isFloat 
and groupcharpos 
> self
._decimalpos
: 
5154                     # 1st one found on right-hand side is past decimal point 
5155 ##                    dbg('groupchar in fraction; illegal') 
5158                     integer 
= value
[:self
._decimalpos
].strip() 
5160                     integer 
= value
.strip() 
5161 ##                dbg("integer:'%s'" % integer) 
5162                 if integer
[0] in ('-', '('): 
5163                     integer 
= integer
[1:] 
5164                 if integer
[-1] == ')': 
5165                     integer 
= integer
[:-1] 
5167                 parts 
= integer
.split(groupchar
) 
5168 ##                dbg('parts:', parts) 
5169                 for i 
in range(len(parts
)): 
5170                     if i 
== 0 and abs(int(parts
[0])) > 999: 
5171 ##                        dbg('group 0 too long; illegal') 
5174                     elif i 
> 0 and (len(parts
[i
]) != 3 or ' ' in parts
[i
]): 
5175 ##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i])) 
5179 ##            dbg('value not a valid number') 
5184     def _validateDate(self
, candidate
=None): 
5185         """ Validate the current date value using the provided Regex filter. 
5186             Generally used for character types.BufferType 
5188 ##        dbg('MaskedEditMixin::_validateDate', indent=1) 
5189         if candidate 
is None: value 
= self
._GetValue
() 
5190         else: value 
= candidate
 
5191 ##        dbg('value = "%s"' % value) 
5192         text 
= self
._adjustDate
(value
, force4digit_year
=True)     ## Fix the date up before validating it 
5193 ##        dbg('text =', text) 
5194         valid 
= True   # assume True until proven otherwise 
5197             # replace fillChar in each field with space: 
5198             datestr 
= text
[0:self
._dateExtent
] 
5200                 field 
= self
._fields
[i
] 
5201                 start
, end 
= field
._extent
 
5202                 fstr 
= datestr
[start
:end
] 
5203                 fstr
.replace(field
._fillChar
, ' ') 
5204                 datestr 
= datestr
[:start
] + fstr 
+ datestr
[end
:] 
5206             year
, month
, day 
= _getDateParts( datestr
, self
._datestyle
) 
5208 ##            dbg('self._dateExtent:', self._dateExtent) 
5209             if self
._dateExtent 
== 11: 
5210                 month 
= charmonths_dict
[month
.lower()] 
5214 ##            dbg('year, month, day:', year, month, day) 
5217 ##            dbg('cannot convert string to integer parts') 
5220 ##            dbg('cannot convert string to integer month') 
5224             # use wxDateTime to unambiguously try to parse the date: 
5225             # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, 
5226             # rather than 1-12, so handle accordingly: 
5232 ##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) 
5233                     dateHandler 
= wx
.DateTimeFromDMY(day
,month
,year
) 
5237 ##                    dbg('cannot convert string to valid date') 
5243                 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5244                 # so we eliminate them here: 
5245                 timeStr     
= text
[self
._dateExtent
+1:].strip()         ## time portion of the string 
5247 ##                    dbg('timeStr: "%s"' % timeStr) 
5249                         checkTime    
= dateHandler
.ParseTime(timeStr
) 
5250                         valid 
= checkTime 
== len(timeStr
) 
5254 ##                        dbg('cannot convert string to valid time') 
5256         if valid
: dbg('valid date') 
5261     def _validateTime(self
, candidate
=None): 
5262         """ Validate the current time value using the provided Regex filter. 
5263             Generally used for character types.BufferType 
5265 ##        dbg('MaskedEditMixin::_validateTime', indent=1) 
5266         # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5267         # so we eliminate them here: 
5268         if candidate 
is None: value 
= self
._GetValue
().strip() 
5269         else: value 
= candidate
.strip() 
5270 ##        dbg('value = "%s"' % value) 
5271         valid 
= True   # assume True until proven otherwise 
5273         dateHandler 
= wx
.DateTime_Today() 
5275             checkTime    
= dateHandler
.ParseTime(value
) 
5276 ##            dbg('checkTime:', checkTime, 'len(value)', len(value)) 
5277             valid 
= checkTime 
== len(value
) 
5282 ##            dbg('cannot convert string to valid time') 
5284 ##        if valid: dbg('valid time') 
5289     def _OnKillFocus(self
,event
): 
5290         """ Handler for EVT_KILL_FOCUS event. 
5292 ##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) 
5293         if self
._mask 
and self
._IsEditable
(): 
5294             self
._AdjustField
(self
._GetInsertionPoint
()) 
5295             self
._CheckValid
()   ## Call valid handler 
5297         self
._LostFocus
()    ## Provided for subclass use 
5302     def _fixSelection(self
): 
5304         This gets called after the TAB traversal selection is made, if the 
5305         focus event was due to this, but before the EVT_LEFT_* events if 
5306         the focus shift was due to a mouse event. 
5308         The trouble is that, a priori, there's no explicit notification of 
5309         why the focus event we received.  However, the whole reason we need to 
5310         do this is because the default behavior on TAB traveral in a wx.TextCtrl is 
5311         now to select the entire contents of the window, something we don't want. 
5312         So we can *now* test the selection range, and if it's "the whole text" 
5313         we can assume the cause, change the insertion point to the start of 
5314         the control, and deselect. 
5316 ##        dbg('MaskedEditMixin::_fixSelection', indent=1) 
5317         # can get here if called with wx.CallAfter after underlying  
5318         # control has been destroyed on close, but after focus 
5320         if not self 
or not self
._mask 
or not self
._IsEditable
(): 
5324         sel_start
, sel_to 
= self
._GetSelection
() 
5325 ##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) 
5327         if( sel_start 
== 0 and sel_to 
>= len( self
._mask 
)   #(can be greater in numeric controls because of reserved space) 
5328             and (not self
._ctrl
_constraints
._autoSelect 
or self
.IsEmpty() or self
.IsDefault() ) ): 
5329             # This isn't normally allowed, and so assume we got here by the new 
5330             # "tab traversal" behavior, so we need to reset the selection 
5331             # and insertion point: 
5332 ##            dbg('entire text selected; resetting selection to start of control') 
5334             field 
= self
._FindField
(self
._GetInsertionPoint
()) 
5335             edit_start
, edit_end 
= field
._extent
 
5336             if field
._selectOnFieldEntry
: 
5337                 self
._SetInsertionPoint
(edit_start
) 
5338                 self
._SetSelection
(edit_start
, edit_end
) 
5340             elif field
._insertRight
: 
5341                 self
._SetInsertionPoint
(edit_end
) 
5342                 self
._SetSelection
(edit_end
, edit_end
) 
5344         elif (self
._isFloat 
or self
._isInt
): 
5346             text
, signpos
, right_signpos 
= self
._getAbsValue
() 
5347             if text 
is None or text 
== self
._template
: 
5348                 integer 
= self
._fields
[0] 
5349                 edit_start
, edit_end 
= integer
._extent
 
5351                 if integer
._selectOnFieldEntry
: 
5352 ##                    dbg('select on field entry:') 
5353                     self
._SetInsertionPoint
(edit_start
) 
5354                     self
._SetSelection
(edit_start
, edit_end
) 
5356                 elif integer
._insertRight
: 
5357 ##                    dbg('moving insertion point to end') 
5358                     self
._SetInsertionPoint
(edit_end
) 
5359                     self
._SetSelection
(edit_end
, edit_end
) 
5361 ##                    dbg('numeric ctrl is empty; start at beginning after sign') 
5362                     self
._SetInsertionPoint
(signpos
+1)   ## Move past minus sign space if signed 
5363                     self
._SetSelection
(signpos
+1, signpos
+1) 
5365         elif sel_start 
> self
._goEnd
(getPosOnly
=True): 
5366 ##            dbg('cursor beyond the end of the user input; go to end of it') 
5369 ##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) 
5374     def _Keypress(self
,key
): 
5375         """ Method provided to override OnChar routine. Return False to force 
5376             a skip of the 'normal' OnChar process. Called before class OnChar. 
5381     def _LostFocus(self
): 
5382         """ Method provided for subclasses. _LostFocus() is called after 
5383             the class processes its EVT_KILL_FOCUS event code. 
5388     def _OnDoubleClick(self
, event
): 
5389         """ selects field under cursor on dclick.""" 
5390         pos 
= self
._GetInsertionPoint
() 
5391         field 
= self
._FindField
(pos
) 
5392         start
, end 
= field
._extent
 
5393         self
._SetInsertionPoint
(start
) 
5394         self
._SetSelection
(start
, end
) 
5398         """ Method provided for subclasses. Called by internal EVT_TEXT 
5399             handler. Return False to override the class handler, True otherwise. 
5406         Used to override the default Cut() method in base controls, instead 
5407         copying the selection to the clipboard and then blanking the selection, 
5408         leaving only the mask in the selected area behind. 
5409         Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the 
5410         derived control because the mixin functions can't override a method of 
5413 ##        dbg("MaskedEditMixin::_Cut", indent=1) 
5414         value 
= self
._GetValue
() 
5415 ##        dbg('current value: "%s"' % value) 
5416         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
5417 ##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) 
5418         do 
= wx
.TextDataObject() 
5419         do
.SetText(value
[sel_start
:sel_to
].strip()) 
5420         wx
.TheClipboard
.Open() 
5421         wx
.TheClipboard
.SetData(do
) 
5422         wx
.TheClipboard
.Close() 
5424         if sel_to 
- sel_start 
!= 0: 
5429 # WS Note: overriding Copy is no longer necessary given that you 
5430 # can no longer select beyond the last non-empty char in the control. 
5432 ##    def _Copy( self ): 
5434 ##        Override the wx.TextCtrl's .Copy function, with our own 
5435 ##        that does validation.  Need to strip trailing spaces. 
5437 ##        sel_start, sel_to = self._GetSelection() 
5438 ##        select_len = sel_to - sel_start 
5439 ##        textval = wx.TextCtrl._GetValue(self) 
5441 ##        do = wx.TextDataObject() 
5442 ##        do.SetText(textval[sel_start:sel_to].strip()) 
5443 ##        wx.TheClipboard.Open() 
5444 ##        wx.TheClipboard.SetData(do) 
5445 ##        wx.TheClipboard.Close() 
5448     def _getClipboardContents( self 
): 
5449         """ Subroutine for getting the current contents of the clipboard. 
5451         do 
= wx
.TextDataObject() 
5452         wx
.TheClipboard
.Open() 
5453         success 
= wx
.TheClipboard
.GetData(do
) 
5454         wx
.TheClipboard
.Close() 
5459             # Remove leading and trailing spaces before evaluating contents 
5460             return do
.GetText().strip() 
5463     def _validatePaste(self
, paste_text
, sel_start
, sel_to
, raise_on_invalid
=False): 
5465         Used by paste routine and field choice validation to see 
5466         if a given slice of paste text is legal for the area in question: 
5467         returns validity, replacement text, and extent of paste in 
5471 ##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) 
5472         select_length 
= sel_to 
- sel_start
 
5473         maxlength 
= select_length
 
5474 ##        dbg('sel_to - sel_start:', maxlength) 
5476             maxlength 
= self
._masklength 
- sel_start
 
5480 ##        dbg('maxlength:', maxlength) 
5481         if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5482             paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5484         length_considered 
= len(paste_text
) 
5485         if length_considered 
> maxlength
: 
5486 ##            dbg('paste text will not fit into the %s:' % item, indent=0) 
5487             if raise_on_invalid
: 
5488 ##                dbg(indent=0, suspend=0) 
5489                 if item 
== 'control': 
5490                     raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5492                     raise ValueError('"%s" will not fit into the selection' % paste_text
) 
5494 ##                dbg(indent=0, suspend=0) 
5495                 return False, None, None 
5497         text 
= self
._template
 
5498 ##        dbg('length_considered:', length_considered) 
5501         replacement_text 
= "" 
5502         replace_to 
= sel_start
 
5504         while valid_paste 
and i 
< length_considered 
and replace_to 
< self
._masklength
: 
5505             if paste_text
[i
:] == self
._template
[replace_to
:length_considered
]: 
5506                 # remainder of paste matches template; skip char-by-char analysis 
5507 ##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) 
5508                 replacement_text 
+= paste_text
[i
:] 
5509                 replace_to 
= i 
= length_considered
 
5512             char 
= paste_text
[i
] 
5513             field 
= self
._FindField
(replace_to
) 
5514             if not field
._compareNoCase
: 
5515                 if field
._forceupper
:   char 
= char
.upper() 
5516                 elif field
._forcelower
: char 
= char
.lower() 
5518 ##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) 
5519 ##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) 
5520             if not self
._isTemplateChar
(replace_to
) and self
._isCharAllowed
( char
, replace_to
, allowAutoSelect
=False, ignoreInsertRight
=True): 
5521                 replacement_text 
+= char
 
5522 ##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) 
5523 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5526             elif( char 
== self
._template
[replace_to
] 
5527                   or (self
._signOk 
and 
5528                           ( (i 
== 0 and (char 
== '-' or (self
._useParens 
and char 
== '('))) 
5529                             or (i 
== self
._masklength 
- 1 and self
._useParens 
and char 
== ')') ) ) ): 
5530                 replacement_text 
+= char
 
5531 ##                dbg("'%(char)s' == template(%(replace_to)d)" % locals()) 
5532 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5536                 next_entry 
= self
._findNextEntry
(replace_to
, adjustInsert
=False) 
5537                 if next_entry 
== replace_to
: 
5540                     replacement_text 
+= self
._template
[replace_to
:next_entry
] 
5541 ##                    dbg("skipping template; next_entry =", next_entry) 
5542 ##                    dbg("replacement_text:", '"'+replacement_text+'"') 
5543                     replace_to 
= next_entry  
# so next_entry will be considered on next loop 
5545         if not valid_paste 
and raise_on_invalid
: 
5546 ##            dbg('raising exception', indent=0, suspend=0) 
5547             raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text
, self
.name
)) 
5549         elif i 
< len(paste_text
): 
5551             if raise_on_invalid
: 
5552 ##                dbg('raising exception', indent=0, suspend=0) 
5553                 raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5555 ##        dbg('valid_paste?', valid_paste) 
5557 ##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) 
5559 ##        dbg(indent=0, suspend=0) 
5560         return valid_paste
, replacement_text
, replace_to
 
5563     def _Paste( self
, value
=None, raise_on_invalid
=False, just_return_value
=False ): 
5565         Used to override the base control's .Paste() function, 
5566         with our own that does validation. 
5567         Note: _Paste must be called from a Paste() override in the 
5568         derived control because the mixin functions can't override a 
5569         method of a sibling class. 
5571 ##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) 
5573             paste_text 
= self
._getClipboardContents
() 
5577         if paste_text 
is not None: 
5579             if 'unicode' in wx
.PlatformInfo 
and type(paste_text
) != types
.UnicodeType
: 
5580                 paste_text 
= paste_text
.decode(self
._defaultEncoding
) 
5582 ##            dbg('paste text: "%s"' % paste_text) 
5583             # (conversion will raise ValueError if paste isn't legal) 
5584             sel_start
, sel_to 
= self
._GetSelection
() 
5585 ##            dbg('selection:', (sel_start, sel_to)) 
5587             # special case: handle allowInsert fields properly 
5588             field 
= self
._FindField
(sel_start
) 
5589             edit_start
, edit_end 
= field
._extent
 
5591             if field
._allowInsert 
and sel_to 
<= edit_end 
and (sel_start 
+ len(paste_text
) < edit_end 
or field
._insertRight
): 
5592                 if field
._insertRight
: 
5593                     # want to paste to the left; see if it will fit: 
5594                     left_text 
= self
._GetValue
()[edit_start
:sel_start
].lstrip() 
5595 ##                    dbg('len(left_text):', len(left_text)) 
5596 ##                    dbg('len(paste_text):', len(paste_text)) 
5597 ##                    dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
5598                     if sel_start 
- (len(left_text
) - (sel_to 
- sel_start
) + len(paste_text
)) >= edit_start
: 
5599                         # will fit! create effective paste text, and move cursor back to do so: 
5600                         paste_text 
= left_text 
+ paste_text
 
5601                         sel_start 
-= len(left_text
) 
5602                         paste_text 
= paste_text
.rjust(sel_to 
- sel_start
) 
5603 ##                        dbg('modified paste_text to be: "%s"' % paste_text) 
5604 ##                        dbg('modified selection to:', (sel_start, sel_to)) 
5606 ##                        dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
5609                     paste_text 
= paste_text 
+ self
._GetValue
()[sel_to
:edit_end
].rstrip() 
5610 ##                    dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text) 
5613                 new_pos 
= sel_start 
+ len(paste_text
)   # store for subsequent positioning 
5614 ##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) 
5615 ##                dbg('expanded selection to:', (sel_start, sel_to)) 
5617             # Another special case: paste won't fit, but it's a right-insert field where entire 
5618             # non-empty value is selected, and there's room if the selection is expanded leftward: 
5619             if( len(paste_text
) > sel_to 
- sel_start
 
5620                 and field
._insertRight
 
5621                 and sel_start 
> edit_start
 
5622                 and sel_to 
>= edit_end
 
5623                 and not self
._GetValue
()[edit_start
:sel_start
].strip() ): 
5624                 # text won't fit within selection, but left of selection is empty; 
5625                 # check to see if we can expand selection to accommodate the value: 
5626                 empty_space 
= sel_start 
- edit_start
 
5627                 amount_needed 
= len(paste_text
) - (sel_to 
- sel_start
) 
5628                 if amount_needed 
<= empty_space
: 
5629                     sel_start 
-= amount_needed
 
5630 ##                    dbg('expanded selection to:', (sel_start, sel_to)) 
5633             # another special case: deal with signed values properly: 
5635                 signedvalue
, signpos
, right_signpos 
= self
._getSignedValue
() 
5636                 paste_signpos 
= paste_text
.find('-') 
5637                 if paste_signpos 
== -1: 
5638                     paste_signpos 
= paste_text
.find('(') 
5640                 # if paste text will result in signed value: 
5641 ####                dbg('paste_signpos != -1?', paste_signpos != -1) 
5642 ####                dbg('sel_start:', sel_start, 'signpos:', signpos) 
5643 ####                dbg('field._insertRight?', field._insertRight) 
5644 ####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) 
5645                 if paste_signpos 
!= -1 and (sel_start 
<= signpos
 
5646                                             or (field
._insertRight 
and sel_start 
- len(paste_text
) <= signpos
)): 
5650                 # remove "sign" from paste text, so we can auto-adjust for sign type after paste: 
5651                 paste_text 
= paste_text
.replace('-', ' ').replace('(',' ').replace(')','') 
5652 ##                dbg('unsigned paste text: "%s"' % paste_text) 
5656             # another special case: deal with insert-right fields when selection is empty and 
5657             # cursor is at end of field: 
5658 ####            dbg('field._insertRight?', field._insertRight) 
5659 ####            dbg('sel_start == edit_end?', sel_start == edit_end) 
5660 ####            dbg('sel_start', sel_start, 'sel_to', sel_to) 
5661             if field
._insertRight 
and sel_start 
== edit_end 
and sel_start 
== sel_to
: 
5662                 sel_start 
-= len(paste_text
) 
5665 ##                dbg('adjusted selection:', (sel_start, sel_to)) 
5668                 valid_paste
, replacement_text
, replace_to 
= self
._validatePaste
(paste_text
, sel_start
, sel_to
, raise_on_invalid
) 
5670 ##                dbg('exception thrown', indent=0) 
5674 ##                dbg('paste text not legal for the selection or portion of the control following the cursor;') 
5675                 if not wx
.Validator_IsSilent(): 
5680             text 
= self
._eraseSelection
() 
5682             new_text 
= text
[:sel_start
] + replacement_text 
+ text
[replace_to
:] 
5684                 new_text 
= string
.ljust(new_text
,self
._masklength
) 
5686                 new_text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=new_text
) 
5689                         new_text 
= new_text
[:signpos
] + '(' + new_text
[signpos
+1:right_signpos
] + ')' + new_text
[right_signpos
+1:] 
5691                         new_text 
= new_text
[:signpos
] + '-' + new_text
[signpos
+1:] 
5695 ##            dbg("new_text:", '"'+new_text+'"') 
5697             if not just_return_value
: 
5698                 if new_text 
!= self
._GetValue
(): 
5699                     self
.modified 
= True 
5703                     wx
.CallAfter(self
._SetValue
, new_text
) 
5705                         new_pos 
= sel_start 
+ len(replacement_text
) 
5706                     wx
.CallAfter(self
._SetInsertionPoint
, new_pos
) 
5709                 return new_text
, replace_to
 
5710         elif just_return_value
: 
5712             return self
._GetValue
(), sel_to
 
5715     def _Undo(self
, value
=None, prev
=None, just_return_results
=False): 
5716         """ Provides an Undo() method in base controls. """ 
5717 ##        dbg("MaskedEditMixin::_Undo", indent=1) 
5719             value 
= self
._GetValue
() 
5721             prev 
= self
._prevValue
 
5722 ##        dbg('current value:  "%s"' % value) 
5723 ##        dbg('previous value: "%s"' % prev) 
5725 ##            dbg('no previous value', indent=0) 
5729             # Determine what to select: (relies on fixed-length strings) 
5730             # (This is a lot harder than it would first appear, because 
5731             # of mask chars that stay fixed, and so break up the "diff"...) 
5733             # Determine where they start to differ: 
5735             length 
= len(value
)     # (both are same length in masked control) 
5737             while( value
[:i
] == prev
[:i
] ): 
5742             # handle signed values carefully, so undo from signed to unsigned or vice-versa 
5745                 text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=prev
) 
5747                     if prev
[signpos
] == '(' and prev
[right_signpos
] == ')': 
5751                     # eliminate source of "far-end" undo difference if using balanced parens: 
5752                     value 
= value
.replace(')', ' ') 
5753                     prev 
= prev
.replace(')', ' ') 
5754                 elif prev
[signpos
] == '-': 
5759             # Determine where they stop differing in "undo" result: 
5760             sm 
= difflib
.SequenceMatcher(None, a
=value
, b
=prev
) 
5761             i
, j
, k 
= sm
.find_longest_match(sel_start
, length
, sel_start
, length
) 
5762 ##            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] ) 
5764             if k 
== 0:                              # no match found; select to end 
5767                 code_5tuples 
= sm
.get_opcodes() 
5768                 for op
, i1
, i2
, j1
, j2 
in code_5tuples
: 
5769 ##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) 
5773                 # look backward through operations needed to produce "previous" value; 
5774                 # first change wins: 
5775                 for next_op 
in range(len(code_5tuples
)-1, -1, -1): 
5776                     op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5777 ##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) 
5778                     field 
= self
._FindField
(i2
) 
5779                     if op 
== 'insert' and prev
[j1
:j2
] != self
._template
[j1
:j2
]: 
5780 ##                        dbg('insert found: selection =>', (j1, j2)) 
5785                     elif op 
== 'delete' and value
[i1
:i2
] != self
._template
[i1
:i2
]: 
5786                         edit_start
, edit_end 
= field
._extent
 
5787                         if field
._insertRight 
and (field
._allowInsert 
or i2 
== edit_end
): 
5793 ##                        dbg('delete found: selection =>', (sel_start, sel_to)) 
5796                     elif op 
== 'replace': 
5797                         if not prev
[i1
:i2
].strip() and field
._insertRight
: 
5798                             sel_start 
= sel_to 
= j2
 
5802 ##                        dbg('replace found: selection =>', (sel_start, sel_to)) 
5808                     # now go forwards, looking for earlier changes: 
5809 ##                    dbg('searching forward...') 
5810                     for next_op 
in range(len(code_5tuples
)): 
5811                         op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5812                         field 
= self
._FindField
(i1
) 
5815                         elif op 
== 'replace': 
5816                             if field
._insertRight
: 
5817                                 # if replace with spaces in an insert-right control, ignore "forward" replace 
5818                                 if not prev
[i1
:i2
].strip(): 
5821 ##                                    dbg('setting sel_start to', j1) 
5824 ##                                    dbg('setting sel_start to', i1) 
5827 ##                                dbg('setting sel_start to', i1) 
5829 ##                            dbg('saw replace; breaking') 
5831                         elif op 
== 'insert' and not value
[i1
:i2
]: 
5832 ##                            dbg('forward %s found' % op) 
5833                             if prev
[j1
:j2
].strip(): 
5834 ##                                dbg('item to insert non-empty; setting sel_start to', j1) 
5837                             elif not field
._insertRight
: 
5838 ##                                dbg('setting sel_start to inserted space:', j1) 
5841                         elif op 
== 'delete': 
5842 ##                            dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip())) 
5843                             if field
._insertRight
: 
5844                                 if value
[i1
:i2
].lstrip(): 
5845 ##                                    dbg('setting sel_start to ', j1) 
5847 ##                                    dbg('breaking loop') 
5852 ##                                dbg('saw delete; breaking') 
5855 ##                            dbg('unknown code!') 
5856                             # we've got what we need 
5861 ##                    dbg('no insert,delete or replace found (!)') 
5862                     # do "left-insert"-centric processing of difference based on l.c.s.: 
5863                     if i 
== j 
and j 
!= sel_start
:         # match starts after start of selection 
5864                         sel_to 
= sel_start 
+ (j
-sel_start
)  # select to start of match 
5866                         sel_to 
= j                          
# (change ends at j) 
5869             # There are several situations where the calculated difference is 
5870             # not what we want to select.  If changing sign, or just adding 
5871             # group characters, we really don't want to highlight the characters 
5872             # changed, but instead leave the cursor where it is. 
5873             # Also, there a situations in which the difference can be ambiguous; 
5876             # current value:    11234 
5877             # previous value:   1111234 
5879             # Where did the cursor actually lie and which 1s were selected on the delete 
5882             # Also, difflib can "get it wrong;" Consider: 
5884             # current value:    "       128.66" 
5885             # previous value:   "       121.86" 
5887             # difflib produces the following opcodes, which are sub-optimal: 
5888             #    equal value[0:9] (       12) prev[0:9] (       12) 
5889             #   insert value[9:9] () prev[9:11] (1.) 
5890             #    equal value[9:10] (8) prev[11:12] (8) 
5891             #   delete value[10:11] (.) prev[12:12] () 
5892             #    equal value[11:12] (6) prev[12:13] (6) 
5893             #   delete value[12:13] (6) prev[13:13] () 
5895             # This should have been: 
5896             #    equal value[0:9] (       12) prev[0:9] (       12) 
5897             #  replace value[9:11] (8.6) prev[9:11] (1.8) 
5898             #    equal value[12:13] (6) prev[12:13] (6) 
5900             # But it didn't figure this out! 
5902             # To get all this right, we use the previous selection recorded to help us... 
5904             if (sel_start
, sel_to
) != self
._prevSelection
: 
5905 ##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) 
5907                 prev_sel_start
, prev_sel_to 
= self
._prevSelection
 
5908                 field 
= self
._FindField
(sel_start
) 
5910                       and sel_start 
< self
._masklength
 
5911                       and (prev
[sel_start
] in ('-', '(', ')') 
5912                                      or value
[sel_start
] in ('-', '(', ')')) ): 
5913                     # change of sign; leave cursor alone... 
5914 ##                    dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')')) 
5915 ##                    dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')')) 
5916 ##                    dbg('setting selection to previous one') 
5917                     sel_start
, sel_to 
= self
._prevSelection
 
5919                 elif field
._groupdigits 
and (value
[sel_start
:sel_to
] == field
._groupChar
 
5920                                              or prev
[sel_start
:sel_to
] == field
._groupChar
): 
5921                     # do not highlight grouping changes 
5922 ##                    dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar) 
5923 ##                    dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar) 
5924 ##                    dbg('setting selection to previous one') 
5925                     sel_start
, sel_to 
= self
._prevSelection
 
5928                     calc_select_len 
= sel_to 
- sel_start
 
5929                     prev_select_len 
= prev_sel_to 
- prev_sel_start
 
5931 ##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) 
5932 ##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) 
5934                     if prev_select_len 
>= calc_select_len
: 
5935                         # old selection was bigger; trust it: 
5936 ##                        dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len) 
5937                         if not field
._insertRight
: 
5938 ##                            dbg('setting selection to previous one') 
5939                             sel_start
, sel_to 
= self
._prevSelection
 
5941                             sel_to 
= self
._prevSelection
[1] 
5942 ##                            dbg('setting selection to', (sel_start, sel_to)) 
5944                     elif( sel_to 
> prev_sel_to                  
# calculated select past last selection 
5945                           and prev_sel_to 
< len(self
._template
) # and prev_sel_to not at end of control 
5946                           and sel_to 
== len(self
._template
) ):  # and calculated selection goes to end of control 
5948                         i
, j
, k 
= sm
.find_longest_match(prev_sel_to
, length
, prev_sel_to
, length
) 
5949 ##                        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] ) 
5951                             # difflib must not have optimized opcodes properly; 
5955                         # look for possible ambiguous diff: 
5957                         # if last change resulted in no selection, test from resulting cursor position: 
5958                         if prev_sel_start 
== prev_sel_to
: 
5959                             calc_select_len 
= sel_to 
- sel_start
 
5960                             field 
= self
._FindField
(prev_sel_start
) 
5962                             # determine which way to search from last cursor position for ambiguous change: 
5963                             if field
._insertRight
: 
5964                                 test_sel_start 
= prev_sel_start
 
5965                                 test_sel_to 
= prev_sel_start 
+ calc_select_len
 
5967                                 test_sel_start 
= prev_sel_start 
- calc_select_len
 
5968                                 test_sel_to 
= prev_sel_start
 
5970                             test_sel_start
, test_sel_to 
= prev_sel_start
, prev_sel_to
 
5972 ##                        dbg('test selection:', (test_sel_start, test_sel_to)) 
5973 ##                        dbg('calc change: "%s"' % prev[sel_start:sel_to]) 
5974 ##                        dbg('test change: "%s"' % prev[test_sel_start:test_sel_to]) 
5976                         # if calculated selection spans characters, and same characters 
5977                         # "before" the previous insertion point are present there as well, 
5978                         # select the ones related to the last known selection instead. 
5979                         if( sel_start 
!= sel_to
 
5980                             and test_sel_to 
< len(self
._template
) 
5981                             and prev
[test_sel_start
:test_sel_to
] == prev
[sel_start
:sel_to
] ): 
5983                             sel_start
, sel_to 
= test_sel_start
, test_sel_to
 
5985                 # finally, make sure that the old and new values are 
5986                 # different where we say they're different: 
5987                 while( sel_to 
- 1 > 0 
5988                         and sel_to 
> sel_start
 
5989                         and value
[sel_to
-1:] == prev
[sel_to
-1:]): 
5991                 while( sel_start 
+ 1 < self
._masklength
 
5992                         and sel_start 
< sel_to
 
5993                         and value
[:sel_start
+1] == prev
[:sel_start
+1]): 
5996 ##            dbg('sel_start, sel_to:', sel_start, sel_to) 
5997 ##            dbg('previous value: "%s"' % prev) 
5999             if just_return_results
: 
6000                 return prev
, (sel_start
, sel_to
) 
6002             self
._SetValue
(prev
) 
6003             self
._SetInsertionPoint
(sel_start
) 
6004             self
._SetSelection
(sel_start
, sel_to
) 
6007 ##            dbg('no difference between previous value') 
6009             if just_return_results
: 
6010                 return prev
, self
._GetSelection
() 
6013     def _OnClear(self
, event
): 
6014         """ Provides an action for context menu delete operation """ 
6018     def _OnContextMenu(self
, event
): 
6019 ##        dbg('MaskedEditMixin::OnContextMenu()', indent=1) 
6021         menu
.Append(wx
.ID_UNDO
, "Undo", "") 
6022         menu
.AppendSeparator() 
6023         menu
.Append(wx
.ID_CUT
, "Cut", "") 
6024         menu
.Append(wx
.ID_COPY
, "Copy", "") 
6025         menu
.Append(wx
.ID_PASTE
, "Paste", "") 
6026         menu
.Append(wx
.ID_CLEAR
, "Delete", "") 
6027         menu
.AppendSeparator() 
6028         menu
.Append(wx
.ID_SELECTALL
, "Select All", "") 
6030         wx
.EVT_MENU(menu
, wx
.ID_UNDO
, self
._OnCtrl
_Z
) 
6031         wx
.EVT_MENU(menu
, wx
.ID_CUT
, self
._OnCtrl
_X
) 
6032         wx
.EVT_MENU(menu
, wx
.ID_COPY
, self
._OnCtrl
_C
) 
6033         wx
.EVT_MENU(menu
, wx
.ID_PASTE
, self
._OnCtrl
_V
) 
6034         wx
.EVT_MENU(menu
, wx
.ID_CLEAR
, self
._OnClear
) 
6035         wx
.EVT_MENU(menu
, wx
.ID_SELECTALL
, self
._OnCtrl
_A
) 
6037         # ## WSS: The base control apparently handles 
6038         # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE 
6039         # and wx.ID_CLEAR menu items even if the menu is one 
6040         # we created.  However, it doesn't do undo properly, 
6041         # so we're keeping track of previous values ourselves. 
6042         # Therefore, we have to override the default update for 
6043         # that item on the menu: 
6044         wx
.EVT_UPDATE_UI(self
, wx
.ID_UNDO
, self
._UndoUpdateUI
) 
6045         self
._contextMenu 
= menu
 
6047         self
.PopupMenu(menu
, event
.GetPosition()) 
6049         self
._contextMenu 
= None 
6052     def _UndoUpdateUI(self
, event
): 
6053         if self
._prevValue 
is None or self
._prevValue 
== self
._curValue
: 
6054             self
._contextMenu
.Enable(wx
.ID_UNDO
, False) 
6056             self
._contextMenu
.Enable(wx
.ID_UNDO
, True) 
6059     def _OnCtrlParametersChanged(self
): 
6061         Overridable function to allow derived classes to take action as a 
6062         result of parameter changes prior to possibly changing the value 
6067  ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6068 class MaskedEditAccessorsMixin
: 
6070     To avoid a ton of boiler-plate, and to automate the getter/setter generation 
6071     for each valid control parameter so we never forget to add the functions when 
6072     adding parameters, this class programmatically adds the masked edit mixin 
6073     parameters to itself. 
6074     (This makes it easier for Designers like Boa to deal with masked controls.) 
6076     To further complicate matters, this is done with an extra level of inheritance, 
6077     so that "general" classes like masked.TextCtrl can have all possible attributes, 
6078     while derived classes, like masked.TimeCtrl and masked.NumCtrl can prevent 
6079     exposure of those optional attributes of their base class that do not make 
6080     sense for their derivation. 
6082     Therefore, we define: 
6083         BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) 
6085         masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). 
6087     This allows us to then derive: 
6088         masked.NumCtrl( BaseMaskedTextCtrl ) 
6090     and not have to expose all the same accessor functions for the 
6091     derived control when they don't all make sense for it. 
6095     # Define the default set of attributes exposed by the most generic masked controls: 
6096     exposed_basectrl_params 
= MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys() 
6097     exposed_basectrl_params
.remove('index') 
6098     exposed_basectrl_params
.remove('extent') 
6099     exposed_basectrl_params
.remove('foregroundColour')   # (base class already has this) 
6101     for param 
in exposed_basectrl_params
: 
6102         propname 
= param
[0].upper() + param
[1:] 
6103         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
6104         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
6106         if param.find('Colour
') != -1: 
6107             # add non-british spellings, for backward-compatibility 
6108             propname.replace('Colour
', 'Color
') 
6110             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
6111             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
6116 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6117 ## these are helper subroutines: 
6119 def _movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): 
6120     """ addseparators = add separator character every three numerals if True 
6122     fmt0 = fmtstring.split('.') 
6125     val  = origvalue.split('.')[0].strip() 
6126     ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) 
6129     return (ret,len(fmt1)) 
6132 def _isDateType( fmtstring ): 
6133     """ Checks the mask and returns True if it fits an allowed 
6134         date or datetime format. 
6136     dateMasks = ("^##/##/####", 
6148     reString  = "|".join(dateMasks) 
6149     filter = re.compile( reString) 
6150     if re.match(filter,fmtstring): return True 
6153 def _isTimeType( fmtstring ): 
6154     """ Checks the mask and returns True if it fits an allowed 
6157     reTimeMask = "^##:##(:##)?( (AM|PM))?" 
6158     filter = re.compile( reTimeMask ) 
6159     if re.match(filter,fmtstring): return True 
6163 def _isFloatingPoint( fmtstring): 
6164     filter = re.compile("[ ]?[#]+\.[#]+\n") 
6165     if re.match(filter,fmtstring+"\n"): return True 
6169 def _isInteger( fmtstring ): 
6170     filter = re.compile("[#]+\n") 
6171     if re.match(filter,fmtstring+"\n"): return True 
6175 def _getDateParts( dateStr, dateFmt ): 
6176     if len(dateStr) > 11: clip = dateStr[0:11] 
6177     else:                 clip = dateStr 
6178     if clip[-2] not in string.digits: 
6179         clip = clip[:-1]    # (got part of time; drop it) 
6181     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6182     slices  = clip.split(dateSep) 
6183     if dateFmt == "MDY": 
6184         y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts 
6185     elif dateFmt == "DMY": 
6186         y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts 
6187     elif dateFmt == "YMD": 
6188         y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts 
6190         y,m,d = None, None, None 
6197 def _getDateSepChar(dateStr): 
6198     clip   = dateStr[0:10] 
6199     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
6203 def _makeDate( year, month, day, dateFmt, dateStr): 
6204     sep    = _getDateSepChar( dateStr) 
6205     if dateFmt == "MDY": 
6206         return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts 
6207     elif dateFmt == "DMY": 
6208         return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts 
6209     elif dateFmt == "YMD": 
6210         return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts 
6215 def _getYear(dateStr,dateFmt): 
6216     parts = _getDateParts( dateStr, dateFmt) 
6219 def _getMonth(dateStr,dateFmt): 
6220     parts = _getDateParts( dateStr, dateFmt) 
6223 def _getDay(dateStr,dateFmt): 
6224     parts = _getDateParts( dateStr, dateFmt) 
6227 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6228 class __test(wx.PySimpleApp): 
6230             from wx.lib.rcsizer import RowColSizer 
6231             self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) 
6232             self.panel = wx.Panel( self.frame, -1) 
6233             self.sizer = RowColSizer() 
6238             id, id1 = wx.NewId(), wx.NewId() 
6239             self.command1  = wx.Button( self.panel, id, "&Close" ) 
6240             self.command2  = wx.Button( self.panel, id1, "&AutoFormats" ) 
6241             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6242             self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) 
6243             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) 
6244 ##            self.panel.SetDefaultItem(self.command1 ) 
6245             self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) 
6247             self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) 
6248             self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) 
6249             self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) 
6250             self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) 
6251             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) 
6252             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) 
6255             label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. 
6256 Note that all controls have been auto-sized by including F in the format code. 
6257 Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" 
6258             label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." 
6260             self.label1 = wx.StaticText( self.panel, -1, label) 
6261             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6262             self.label3 = wx.StaticText( self.panel, -1, "Mask Value") 
6263             self.label4 = wx.StaticText( self.panel, -1, "Format") 
6264             self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") 
6265             self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") 
6266             self.label7 = wx.StaticText( self.panel, -1, label2) 
6267             self.label7.SetForegroundColour("Blue") 
6268             self.label1.SetForegroundColour("Blue") 
6269             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6270             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6271             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6272             self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6273             self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6275             self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) 
6276             self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) 
6277             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6278             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6279             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6280             self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) 
6281             self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) 
6283             # The following list is of the controls for the demo. Feel free to play around with 
6286             #description        mask                    excl format     regexp                              range,list,initial 
6287            ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''), 
6288            ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''), 
6289            ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''), 
6290            ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''), 
6291            ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''), 
6292            ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'), 
6293            ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''), 
6294            ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'), 
6295            ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''), 
6296            ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '), 
6297            ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '), 
6298            ("Month selector",   "XXX",                  "", 'F',        "",                                 (), 
6299                 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), 
6300            ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (), 
6301                 ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") 
6304             for control in controls: 
6305                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6306                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6307                 self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) 
6308                 self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) 
6310                 if control in controls[:]:#-2]: 
6311                     newControl  = MaskedTextCtrl( self.panel, -1, "", 
6313                                                     excludeChars = control[2], 
6314                                                     formatcodes  = control[3], 
6316                                                     validRegex   = control[4], 
6317                                                     validRange   = control[5], 
6318                                                     choices      = control[6], 
6319                                                     defaultValue = control[7], 
6321                     if control[6]: newControl.SetCtrlParameters(choiceRequired = True) 
6323                     newControl = MaskedComboBox(  self.panel, -1, "", 
6324                                                     choices = control[7], 
6325                                                     choiceRequired  = True, 
6327                                                     formatcodes  = control[3], 
6328                                                     excludeChars = control[2], 
6330                                                     validRegex   = control[4], 
6331                                                     validRange   = control[5], 
6333                 self.editList.append( newControl ) 
6335                 self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) 
6338             self.sizer.AddGrowableCol(4) 
6340             self.panel.SetSizer(self.sizer) 
6341             self.panel.SetAutoLayout(1) 
6348         def onClick(self, event): 
6351         def onClickPage(self, event): 
6352             self.page2 = __test2(self.frame,-1,"") 
6353             self.page2.Show(True) 
6355         def _onCheck1(self,event): 
6356             """ Set required value on/off """ 
6357             value = event.IsChecked() 
6359                 for control in self.editList: 
6360                     control.SetCtrlParameters(emptyInvalid=True) 
6363                 for control in self.editList: 
6364                     control.SetCtrlParameters(emptyInvalid=False) 
6366             self.panel.Refresh() 
6368         def _onCheck2(self,event): 
6369             """ Highlight empty values""" 
6370             value = event.IsChecked() 
6372                 for control in self.editList: 
6373                     control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') 
6376                 for control in self.editList: 
6377                     control.SetCtrlParameters( emptyBackgroundColour = 'White') 
6379             self.panel.Refresh() 
6382 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6384 class __test2(wx.Frame): 
6385         def __init__(self, parent, id, caption): 
6386             wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) 
6387             from wx.lib.rcsizer import RowColSizer 
6388             self.panel = wx.Panel( self, -1) 
6389             self.sizer = RowColSizer() 
6395 All these controls have been created by passing a single parameter, the AutoFormat code. 
6396 The class contains an internal dictionary of types and formats (autoformats). 
6397 To see a great example of validations in action, try entering a bad email address, then tab out.""" 
6399             self.label1 = wx.StaticText( self.panel, -1, label) 
6400             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6401             self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") 
6402             self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") 
6403             self.label1.SetForegroundColour("Blue") 
6404             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6405             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6406             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6408             self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) 
6409             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6410             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6411             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6413             id, id1 = wx.NewId(), wx.NewId() 
6414             self.command1  = wx.Button( self.panel, id, "&Close") 
6415             self.command2  = wx.Button( self.panel, id1, "&Print Formats") 
6416             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) 
6417             self.panel.SetDefaultItem(self.command1) 
6418             self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) 
6420             # The following list is of the controls for the demo. Feel free to play around with 
6423            ("Phone No","USPHONEFULLEXT"), 
6424            ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), 
6425            ("US Date MMDDYYYY","USDATEMMDDYYYY/"), 
6426            ("Time (with seconds)","TIMEHHMMSS"), 
6427            ("Military Time\n(without seconds)","24HRTIMEHHMM"), 
6428            ("Social Sec#","USSOCIALSEC"), 
6429            ("Credit Card","CREDITCARD"), 
6430            ("Expiration MM/YY","EXPDATEMMYY"), 
6431            ("Percentage","PERCENT"), 
6432            ("Person's Age","AGE"), 
6433            ("US Zip Code","USZIP"), 
6434            ("US Zip+4","USZIPPLUS4"), 
6435            ("Email Address","EMAIL"), 
6436            ("IP Address", "(derived control IpAddrCtrl)") 
6439             for control in controls: 
6440                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6441                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6442                 if control in controls[:-1]: 
6443                     self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", 
6444                                                       autoformat  = control[1], 
6446                                 row=rowcount,col=2,flag=wx.ALL,border=5) 
6448                     self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), 
6449                                     row=rowcount,col=2,flag=wx.ALL,border=5) 
6452             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6453             self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) 
6454             self.sizer.AddGrowableCol(3) 
6456             self.panel.SetSizer(self.sizer) 
6457             self.panel.SetAutoLayout(1) 
6459         def onClick(self, event): 
6462         def onClickPrint(self, event): 
6463             for format in masktags.keys(): 
6464                 sep = "+------------------------+" 
6465                 print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) 
6467 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6469 if __name__ == "__main__": 
6475 ## =================================== 
6477 ## 1. WS: For some reason I don't understand, the control is generating two (2) 
6478 ##      EVT_TEXT events for every one (1) .SetValue() of the underlying control. 
6479 ##      I've been unsuccessful in determining why or in my efforts to make just one 
6480 ##      occur.  So, I've added a hack to save the last seen value from the 
6481 ##      control in the EVT_TEXT handler, and if *different*, call event.Skip() 
6482 ##      to propagate it down the event chain, and let the application see it. 
6484 ## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the 
6485 ##      behavior of the underlying control that I can't fix.  The problems are: 
6486 ##      a) The background coloring doesn't work in the text field of the control; 
6487 ##         instead, there's a only border around it that assumes the correct color. 
6488 ##      b) The control will not pass WXK_TAB to the event handler, no matter what 
6489 ##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to 
6490 ##         indicate that we want these events.  As a result, MaskedComboBox 
6491 ##         doesn't do the nice field-tabbing that MaskedTextCtrl does. 
6492 ##      c) Auto-complete had to be reimplemented for the control because programmatic 
6493 ##         setting of the value of the text field does not set up the auto complete 
6494 ##         the way that the control processing keystrokes does.  (But I think I've 
6495 ##         implemented a fairly decent approximation.)  Because of this the control 
6496 ##         also won't auto-complete on dropdown, and there's no event I can catch 
6497 ##         to work around this problem. 
6498 ##      d) There is no method provided for getting the selection; the hack I've 
6499 ##         implemented has its flaws, not the least of which is that due to the 
6500 ##         strategy that I'm using, the paste buffer is always replaced by the 
6501 ##         contents of the control's selection when in focus, on each keystroke; 
6502 ##         this makes it impossible to paste anything into a MaskedComboBox 
6503 ##         at the moment... :-( 
6504 ##      e) The other deficient behavior, likely induced by the workaround for (d), 
6505 ##         is that you can can't shift-left to select more than one character 
6509 ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their 
6510 ##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in 
6511 ##      wxWindows, shift-tab won't take you backwards through the fields of 
6512 ##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will 
6513 ##      work like shift-tab and tab ought to. 
6517 ## =============================## 
6518 ##  1. Add Popup list for auto-completable fields that simulates combobox on individual 
6519 ##     fields.  Example: City validates against list of cities, or zip vs zip code list. 
6520 ##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" 
6522 ##  3. Fix shift-left selection for MaskedComboBox. 
6523 ##  5. Transform notion of "decimal control" to be less "entire control"-centric, 
6524 ##     so that monetary symbols can be included and still have the appropriate 
6525 ##     semantics.  (Big job, as currently written, but would make control even 
6526 ##     more useful for business applications.) 
6530 ## ==================== 
6532 ##  1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail. 
6533 ##  2. Added proper support for unicode version of wxPython 
6534 ##  3. Added * as mask char meaning "all ansi chars" (ordinals 32-255). 
6535 ##  4. Converted doc strings to use reST format, for ePyDoc documentation. 
6536 ##  5. Renamed helper functions, classes, etc. not intended to be visible in public 
6537 ##     interface to code. 
6540 ##  1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead 
6541 ##     shifts the text to the left accordingly. 
6542 ##  2. Fixed _SetValue() to place cursor after last character inserted, rather than end of 
6544 ##  3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes 
6545 ##     (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping 
6547 ##  4. Fixed autoselect behavior to work similarly to (2) above, so that combobox 
6548 ##     selection will only select the non-empty text, as per request. 
6549 ##  5. Fixed tabbing to work with 2.5.2 semantics. 
6550 ##  6. Fixed size calculation to handle changing fonts 
6553 ##  1. Reorganized masked controls into separate package, renamed things accordingly 
6554 ##  2. Split actual controls out of this file into their own files. 
6556 ##  (Reported) bugs fixed: 
6557 ##   1. Crash ensues if you attempt to change the mask of a read-only 
6558 ##      MaskedComboBox after initial construction. 
6559 ##   2. Changed strategy of defining Get/Set property functions so that 
6560 ##      these are now generated dynamically at runtime, rather than as 
6561 ##      part of the class definition.  (This makes it possible to have 
6562 ##      more general base classes that have many more options for configuration 
6563 ##      without requiring that derivations support the same options.) 
6564 ##   3. Fixed IsModified for _Paste() and _OnErase(). 
6567 ##   1. Fixed "attribute function inheritance," since base control is more 
6568 ##      generic than subsequent derivations, not all property functions of a 
6569 ##      generic control should be exposed in those derivations.  New strategy 
6570 ##      uses base control classes (eg. BaseMaskedTextCtrl) that should be 
6571 ##      used to derive new class types, and mixed with their own mixins to 
6572 ##      only expose those attributes from the generic masked controls that 
6573 ##      make sense for the derivation.  (This makes Boa happier.) 
6574 ##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less 
6578 ##  (Reported) bugs fixed: 
6579 ##   1. Right-click menu allowed "cut" operation that destroyed mask 
6580 ##      (was implemented by base control) 
6581 ##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all 
6582 ##      got converted to lower case. 
6583 ##   3. MaskedComboBox selection didn't deal with spaces in values 
6584 ##      properly when autocompleting, and didn't have a concept of "next" 
6585 ##      match for handling choice list duplicates. 
6586 ##   4. Size of MaskedComboBox was always default. 
6587 ##   5. Email address regexp allowed some "non-standard" things, and wasn't 
6589 ##   6. Couldn't easily reset MaskedComboBox contents programmatically. 
6590 ##   7. Couldn't set emptyInvalid during construction. 
6591 ##   8. Under some versions of wxPython, readonly comboboxes can apparently 
6592 ##      return a GetInsertionPoint() result (655535), causing masked control 
6594 ##   9. Specifying an empty mask caused the controls to traceback. 
6595 ##  10. Can't specify float ranges for validRange. 
6596 ##  11. '.' from within a the static portion of a restricted IP address 
6597 ##      destroyed the mask from that point rightward; tab when cursor is 
6598 ##      before 1st field takes cursor past that field. 
6601 ##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) 
6602 ##  13. Added auto-select option on char input for masked controls with 
6604 ##  14. Added '>' formatcode, allowing insert within a given or each field 
6605 ##      as appropriate, rather than requiring "overwrite".  This makes single 
6606 ##      field controls that just have validation rules (eg. EMAIL) much more 
6607 ##      friendly.  The same flag controls left shift when deleting vs just 
6608 ##      blanking the value, and for right-insert fields, allows right-insert 
6609 ##      at any non-blank (non-sign) position in the field. 
6610 ##  15. Added option to use to indicate negative values for numeric controls. 
6611 ##  16. Improved OnFocus handling of numeric controls. 
6612 ##  17. Enhanced Home/End processing to allow operation on a field level, 
6614 ##  18. Added individual Get/Set functions for control parameters, for 
6615 ##      simplified integration with Boa Constructor. 
6616 ##  19. Standardized "Colour" parameter names to match wxPython, with 
6617 ##      non-british spellings still supported for backward-compatibility. 
6618 ##  20. Added '&' mask specification character for punctuation only (no letters 
6620 ##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide 
6621 ##      unified interface to the masked edit subclasses. 
6625 ##   1. Made it possible to configure grouping, decimal and shift-decimal characters, 
6626 ##      to make controls more usable internationally. 
6627 ##   2. Added code to smart "adjust" value strings presented to .SetValue() 
6628 ##      for right-aligned numeric format controls if they are shorter than 
6629 ##      than the control width,  prepending the missing portion, prepending control 
6630 ##      template left substring for the missing characters, so that setting 
6631 ##      numeric values is easier. 
6632 ##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved 
6633 ##      for b-c), as this makes more sense. 
6636 ##   1. Fixed .SetValue() to replace the current value, rather than the current 
6637 ##      selection. Also changed it to generate ValueError if presented with 
6638 ##      either a value which doesn't follow the format or won't fit.  Also made 
6639 ##      set value adjust numeric and date controls as if user entered the value. 
6640 ##      Expanded doc explaining how SetValue() works. 
6641 ##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to 
6642 ##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. 
6643 ##   3. Made all date autoformats automatically pick implied "datestyle". 
6644 ##   4. Added IsModified override, since base wx.TextCtrl never reports modified if 
6645 ##      .SetValue used to change the value, which is what the masked edit controls 
6647 ##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when 
6648 ##      using tab to "leave field" and auto-adjust. 
6649 ##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, 
6650 ##      and bug in ._Paste() that didn't account for signs in signed masks either. 
6651 ##   7. Fixed issues with _adjustPos for right-insert fields causing improper 
6652 ##      selection/replacement of values 
6653 ##   8. Fixed _OnHome handler to properly handle extending current selection to 
6654 ##      beginning of control. 
6655 ##   9. Exposed all (valid) autoformats to demo, binding descriptions to 
6657 ##  10. Fixed a couple of bugs in email regexp. 
6658 ##  11. Made maskchardict an instance var, to make mask chars to be more 
6659 ##      amenable to international use. 
6660 ##  12. Clarified meaning of '-' formatcode in doc. 
6661 ##  13. Fixed a couple of coding bugs being flagged by Python2.1. 
6662 ##  14. Fixed several issues with sign positioning, erasure and validity 
6663 ##      checking for "numeric" masked controls. 
6664 ##  15. Added validation to IpAddrCtrl.SetValue(). 
6667 ##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) 
6668 ##      vs. literal font facename, and use wxTELETYPE as the font family 
6670 ##   2. Switched to use of dbg module vs. locally defined version. 
6671 ##   3. Revamped entire control structure to use Field classes to hold constraint 
6672 ##      and formatting data, to make code more hierarchical, allow for more 
6673 ##      sophisticated masked edit construction. 
6674 ##   4. Better strategy for managing options, and better validation on keywords. 
6675 ##   5. Added 'V' format code, which requires that in order for a character 
6676 ##      to be accepted, it must result in a string that passes the validRegex. 
6677 ##   6. Added 'S' format code which means "select entire field when navigating 
6679 ##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) 
6680 ##   8. Added '<' format code to allow fields to require explicit cursor movement 
6682 ##   9. Added validFunc option to other validation mechanisms, that allows derived 
6683 ##      classes to add dynamic validation constraints to the control. 
6684 ##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also 
6685 ##      fixed failure to obey case conversion codes when pasting. 
6686 ##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... 
6687 ##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' 
6688 ##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, 
6689 ##      individual field validation to prevent field values > 255, and require explicit 
6690 ##      tab/. to change fields. 
6691 ##  14. Added handler for left double-click to select field under cursor. 
6692 ##  15. Fixed handling for "Read-only" styles. 
6693 ##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor 
6694 ##      attribute, for more consistent and controllable coloring. 
6695 ##  17. Added retainFieldValidation parameter, allowing top-level constraints 
6696 ##      such as "validRequired" to be set independently of field-level equivalent. 
6697 ##      (needed in TimeCtrl for bounds constraints.) 
6698 ##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed 
6699 ##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, 
6701 ##  19. Fixed maskchar setting for upper/lowercase, to work in all locales. 
6705 ##   1. Decimal point behavior restored for decimal and integer type controls: 
6706 ##      decimal point now trucates the portion > 0. 
6707 ##   2. Return key now works like the tab character and moves to the next field, 
6708 ##      provided no default button is set for the form panel on which the control 
6710 ##   3. Support added in _FindField() for subclasses controls (like timecontrol) 
6711 ##      to determine where the current insertion point is within the mask (i.e. 
6712 ##      which sub-'field'). See method documentation for more info and examples. 
6713 ##   4. Added Field class and support for all constraints to be field-specific 
6714 ##      in addition to being globally settable for the control. 
6715 ##      Choices for each field are validated for length and pastability into 
6716 ##      the field in question, raising ValueError if not appropriate for the control. 
6717 ##      Also added selective additional validation based on individual field constraints. 
6718 ##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all 
6719 ##      auto-complete fields with choice lists, supplying the 1st entry in 
6720 ##      the choice list if the field is empty, and cycling through the list in 
6721 ##      the appropriate direction if already a match.  WXK_DOWN will also auto- 
6722 ##      complete if the field is partially completed and a match can be made. 
6723 ##      SHIFT-WXK_UP/DOWN will also take you to the next field after any 
6724 ##      auto-completion performed. 
6725 ##   5. Added autoCompleteKeycodes=[] parameters for allowing further 
6726 ##      customization of the control.  Any keycode supplied as a member 
6727 ##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If 
6728 ##      requireFieldChoice is set, then a valid value from each non-empty 
6729 ##      choice list will be required for the value of the control to validate. 
6730 ##   6. Fixed "auto-sizing" to be relative to the font actually used, rather 
6731 ##      than making assumptions about character width. 
6732 ##   7. Fixed GetMaskParameter(), which was non-functional in previous version. 
6733 ##   8. Fixed exceptions raised to provide info on which control had the error. 
6734 ##   9. Fixed bug in choice management of MaskedComboBox. 
6735 ##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of 
6736 ##     the form '# #'.  Modified control code for IpAddrCtrl so that '.' 
6737 ##     in the middle of a field clips the rest of that field, similar to 
6738 ##     decimal and integer controls. 
6742 ##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. 
6743 ##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). 
6744 ##   3. New support for selecting text within the control.(thanks Will Sadkin!) 
6745 ##      Shift-End and Shift-Home now select text as you would expect 
6746 ##      Control-Shift-End selects to the end of the mask string, even if value not entered. 
6747 ##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control. 
6748 ##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- 
6749 ##      for some reason I couldn't find the original email but thanks!!!) 
6750 ##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome, 
6751 ##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. 
6752 ##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). 
6753 ##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) 
6754 ##   7. New mechanism for replacing default behavior for any given key, using 
6755 ##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available 
6756 ##      for easier subclassing of the control. 
6757 ##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs 
6758 ##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut" 
6759 ##      semantics, erasing the selection, rather than erasing the entire control. 
6760 ##   9. Added option for an "default value" (ie. the template) for use when a single fillChar 
6761 ##      is not desired in every position.  Added IsDefault() function to mean "does the value 
6762 ##      equal the template?" and modified .IsEmpty() to mean "do all of the editable 
6763 ##      positions in the template == the fillChar?" 
6764 ##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, 
6766 ##  11. MaskedComboBox now adds the capability to validate from list of valid values. 
6767 ##      Example: City validates against list of cities, or zip vs zip code list. 
6768 ##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being 
6769 ##      passed to the next handler in the event chain, causing updates to the 
6770 ##      control to be invisible to the parent code. 
6771 ##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within 
6772 ##      the control, that auto-reformats as you move between cells. 
6773 ##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. 
6774 ##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} 
6775 ##  16. Fixed major bugs in date validation, due to the fact that 
6776 ##      wxDateTime.ParseDate is too liberal, and will accept any form that 
6777 ##      makes any kind of sense, regardless of the datestyle you specified 
6778 ##      for the control.  Unfortunately, the strategy used to fix it only 
6779 ##      works for versions of wxPython post 2.3.3.1, as a C++ assert box 
6780 ##      seems to show up on an invalid date otherwise, instead of a catchable 
6782 ##  17. Enhanced date adjustment to automatically adjust heuristic based on 
6783 ##      current year, making last century/this century determination on 
6784 ##      2-digit year based on distance between today's year and value; 
6785 ##      if > 50 year separation, assume last century (and don't assume last 
6786 ##      century is 20th.) 
6787 ##  18. Added autoformats and support for including HHMMSS as well as HHMM for 
6788 ##      date times, and added similar time, and militaray time autoformats. 
6789 ##  19. Enhanced tabbing logic so that tab takes you to the next field if the 
6790 ##      control is a multi-field control. 
6791 ##  20. Added stub method called whenever the control "changes fields", that 
6792 ##      can be overridden by subclasses (eg. IpAddrCtrl.) 
6793 ##  21. Changed a lot of code to be more functionally-oriented so side-effects 
6794 ##      aren't as problematic when maintaining code and/or adding features. 
6795 ##      Eg: IsValid() now does not have side-effects; it merely reflects the 
6796 ##      validity of the value of the control; to determine validity AND recolor 
6797 ##      the control, _CheckValid() should be used with a value argument of None. 
6798 ##      Similarly, made most reformatting function take an optional candidate value 
6799 ##      rather than just using the current value of the control, and only 
6800 ##      have them change the value of the control if a candidate is not specified. 
6801 ##      In this way, you can do validation *before* changing the control. 
6802 ##  22. Changed validRequired to mean "disallow chars that result in invalid 
6803 ##      value."  (Old meaning now represented by emptyInvalid.)  (This was 
6804 ##      possible once I'd made the changes in (19) above.) 
6805 ##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they 
6806 ##      can be set/modified/retrieved after construction.  Removed individual 
6807 ##      parameter setting functions, in favor of this mechanism, so that 
6808 ##      all adjustment of the control based on changing parameter values can 
6809 ##      be handled in one place with unified mechanism. 
6810 ##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability 
6811 ##      to type "grouping char" (ie. ',') and validate as appropriate. 
6812 ##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. 
6813 ##  26. Fixed assumption about "decimal or integer" masks so that they're only 
6814 ##      made iff there's no validRegex associated with the field.  (This 
6815 ##      is so things like zipcodes which look like integers can have more 
6816 ##      restrictive validation (ie. must be 5 digits.) 
6817 ##  27. Added a ton more doc strings to explain use and derivation requirements 
6818 ##      and did regularization of the naming conventions. 
6819 ##  28. Fixed a range bug in _adjustKey preventing z from being handled properly. 
6820 ##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to 
6821 ##      reformat the value and move the next field as appropriate. (shift-'.', 
6822 ##      ie. '>' moves to the previous field. 
6825 ##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending 
6827 ##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the 
6828 ##      year position, the control will expand the value to four digits, using numerals below 
6829 ##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). 
6830 ##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} 
6831 ##   3. revalid parameter renamed validRegex to conform to standard for all validation 
6832 ##      parameters (see 2 new ones below). 
6833 ##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types. 
6834 ##      Allows the developer to specify a valid low/high range of values. 
6835 ##   5. New optional init parameter = validList. Used for character types. Allows developer 
6836 ##      to send a list of values to the control to be used for specific validation. 
6837 ##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams. 
6838 ##   6. Date type fields now use wxDateTime's parser to validate the date and time. 
6839 ##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing 
6840 ##      me toward this solution! 
6841 ##   7. Date fields now automatically expand 2-digit years when it can. For example, 
6842 ##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year 
6843 ##      date is entered it will be expanded in any case when the user tabs out of the 
6845 ##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, 
6846 ##      SetSignedForeColor allow accessto override default class coloring behavior. 
6847 ##   9. Documentation updated and improved. 
6848 ##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. 
6849 ##      Two new options (checkboxes) - test highlight empty and disallow empty. 
6850 ##  11. Home and End now work more intuitively, moving to the first and last user-entry 
6851 ##      value, respectively. 
6852 ##  12. New class function: SetRequired(bool). Sets the control's entry required flag 
6853 ##      (i.e. disallow empty values if True). 
6856 ##   1. get_plainValue method renamed to GetPlainValue following the wxWindows 
6857 ##      StudlyCaps(tm) standard (thanks Paul Moore).  ;) 
6858 ##   2. New format code 'F' causes the control to auto-fit (auto-size) itself 
6859 ##      based on the length of the mask template. 
6860 ##   3. Class now supports "autoformat" codes. These can be passed to the class 
6861 ##      on instantiation using the parameter autoformat="code". If the code is in 
6862 ##      the dictionary, it will self set the mask, formatting, and validation string. 
6863 ##      I have included a number of samples, but I am hoping that someone out there 
6864 ##      can help me to define a whole bunch more. 
6865 ##   4. I have added a second page to the demo (as well as a second demo class, test2) 
6866 ##      to showcase how autoformats work. The way they self-format and self-size is, 
6867 ##      I must say, pretty cool. 
6868 ##   5. Comments added and some internal cosmetic revisions re: matching the code 
6869 ##      standards for class submission. 
6870 ##   6. Regex validation is now done in real time - field turns yellow immediately 
6871 ##      and stays yellow until the entered value is valid 
6872 ##   7. Cursor now skips over template characters in a more intuitive way (before the 
6874 ##   8. Change, Keypress and LostFocus methods added for convenience of subclasses. 
6875 ##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, 
6876 ##      and EVT_KILL_FOCUS, respectively. 
6877 ##   9. Decimal and numeric handlers have been rewritten and now work more intuitively. 
6880 ##   1. New .IsEmpty() method returns True if the control's value is equal to the 
6881 ##      blank template string 
6882 ##   2. Control now supports a new init parameter: revalid. Pass a regular expression 
6883 ##      that the value will have to match when the control loses focus. If invalid, 
6884 ##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next). 
6885 ##   3. Demo now shows revalid functionality. Try entering a partial value, such as a 
6886 ##      partial social security number. 
6887 ##   4. New .IsValid() value returns True if the control is empty, or if the value matches 
6888 ##      the revalid expression. If not, .IsValid() returns False. 
6889 ##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never 
6890 ##      presses the decimal point. 
6891 ##   6. Cursor now goes to the beginning of the field if the user clicks in an 
6892 ##      "empty" field intead of leaving the insertion point in the middle of the 
6894 ##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. 
6895 ##   8. New formatcodes init parameter replaces other init params and adds functions. 
6896 ##      String passed to control on init controls: 
6900 ##        R Show negative #s in red 
6902 ##        - Signed numerals 
6903 ##        0 Numeric fields get leading zeros 
6904 ##   9. Ctrl-X in any field clears the current value. 
6905 ##   10. Code refactored and made more modular (esp in OnChar method). Should be more 
6906 ##       easy to read and understand. 
6907 ##   11. Demo enhanced. 
6908 ##   12. Now has _doc_. 
6911 ##   1. GetPlainValue() now returns the value without the template characters; 
6912 ##      so, for example, a social security number (123-33-1212) would return as 
6913 ##      123331212; also removes white spaces from numeric/decimal values, so 
6914 ##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value. 
6915 ##   2. Press '.' in an integer style masked control and truncate any trailing digits. 
6916 ##   3. Code moderately refactored. Internal names improved for clarity. Additional 
6917 ##      internal documentation. 
6918 ##   4. Home and End keys now supported to move cursor to beginning or end of field. 
6919 ##   5. Un-signed integers and decimals now supported. 
6920 ##   6. Cosmetic improvements to the demo. 
6921 ##   7. Class renamed to MaskedTextCtrl. 
6922 ##   8. Can now specify include characters that will override the basic 
6923 ##      controls: for example, includeChars = "@." for email addresses 
6924 ##   9. Added mask character 'C' -> allow any upper or lowercase character 
6925 ##   10. .SetSignColor(str:color) sets the foreground color for negative values 
6926 ##       in signed controls (defaults to red) 
6927 ##   11. Overview documentation written. 
6930 ##   1. Tab now works properly when pressed in last position 
6931 ##   2. Decimal types now work (e.g. #####.##) 
6932 ##   3. Signed decimal or numeric values supported (i.e. negative numbers) 
6933 ##   4. Negative decimal or numeric values now can show in red. 
6934 ##   5. Can now specify an "exclude list" with the excludeChars parameter. 
6935 ##      See date/time formatted example - you can only enter A or P in the 
6936 ##      character mask space (i.e. AM/PM). 
6937 ##   6. Backspace now works properly, including clearing data from a selected 
6938 ##      region but leaving template characters intact. Also delete key. 
6939 ##   7. Left/right arrows now work properly. 
6940 ##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3