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 
  54 <b>Masked Edit Overview: 
  55 =====================</b> 
  56 <b>masked.TextCtrl</b> 
  57     is a sublassed text control that can carefully control the user's input 
  58     based on a mask string you provide. 
  60     General usage example: 
  61         control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####') 
  63     The example above will create a text control that allows only numbers to be 
  64     entered and then only in the positions indicated in the mask by the # sign. 
  66 <b>masked.ComboBox</b> 
  67     is a similar subclass of wxComboBox that allows the same sort of masking, 
  68     but also can do auto-complete of values, and can require the value typed 
  69     to be in the list of choices to be colored appropriately. 
  72     is actually a factory function for several types of masked edit controls: 
  74     <b>masked.TextCtrl</b>   - standard masked edit text box 
  75     <b>masked.ComboBox</b>   - adds combobox capabilities 
  76     <b>masked.IpAddrCtrl</b> - adds special semantics for IP address entry 
  77     <b>masked.TimeCtrl</b>   - special subclass handling lots of types as values 
  78     <b>masked.NumCtrl</b>    - special subclass handling numeric values 
  80     It works by looking for a <b><i>controlType</i></b> parameter in the keyword 
  81     arguments of the control, to determine what kind of instance to return. 
  82     If not specified as a keyword argument, the default control type returned 
  83     will be masked.TextCtrl. 
  85     Each of the above classes has its own set of arguments, but masked.Ctrl 
  86     provides a single "unified" interface for masked controls.  Those for 
  87     masked.TextCtrl, masked.ComboBox and masked.IpAddrCtrl are all documented 
  88     below; the others have their own demo pages and interface descriptions. 
  89     (See end of following discussion for how to configure the wx.MaskedCtrl() 
  90     to select the above control types.) 
  93 <b>INITILIZATION PARAMETERS 
  94 ======================== 
  96 Allowed mask characters and function: 
  98             #       Allow numeric only (0-9) 
  99             N       Allow letters and numbers (0-9) 
 100             A       Allow uppercase letters only 
 101             a       Allow lowercase letters only 
 102             C       Allow any letter, upper or lower 
 103             X       Allow string.letters, string.punctuation, string.digits 
 104             &       Allow string.punctuation only 
 107     These controls define these sets of characters using string.letters, 
 108     string.uppercase, etc.  These sets are affected by the system locale 
 109     setting, so in order to have the masked controls accept characters 
 110     that are specific to your users' language, your application should 
 112     For example, to allow international characters to be used in the 
 113     above masks, you can place the following in your code as part of 
 114     your application's initialization code: 
 117       locale.setlocale(locale.LC_ALL, '') 
 120   Using these mask characters, a variety of template masks can be built. See 
 121   the demo for some other common examples include date+time, social security 
 122   number, etc.  If any of these characters are needed as template rather 
 123   than mask characters, they can be escaped with \, ie. \N means "literal N". 
 124   (use \\ for literal backslash, as in: r'CCC\\NNN'.) 
 128       Masks containing only # characters and one optional decimal point 
 129       character are handled specially, as "numeric" controls.  Such 
 130       controls have special handling for typing the '-' key, handling 
 131       the "decimal point" character as truncating the integer portion, 
 132       optionally allowing grouping characters and so forth. 
 133       There are several parameters and format codes that only make sense 
 134       when combined with such masks, eg. groupChar, decimalChar, and so 
 135       forth (see below).  These allow you to construct reasonable 
 136       numeric entry controls. 
 139       Changing the mask for a control deletes any previous field classes 
 140       (and any associated validation or formatting constraints) for them. 
 142 <b>useFixedWidthFont=</b> 
 143   By default, masked edit controls use a fixed width font, so that 
 144   the mask characters are fixed within the control, regardless of 
 145   subsequent modifications to the value.  Set to False if having 
 146   the control font be the same as other controls is required. 
 150   These other properties can be passed to the class when instantiating it: 
 151     Formatcodes are specified as a string of single character formatting 
 152     codes that modify  behavior of the control: 
 156             R  Right-align field(s) 
 157             r  Right-insert in field(s) (implies R) 
 158             <  Stay in field until explicit navigation out of it 
 160             >  Allow insert/delete within partially filled fields (as 
 161                opposed to the default "overwrite" mode for fixed-width 
 162                masked edit controls.)  This allows single-field controls 
 163                or each field within a multi-field control to optionally 
 164                behave more like standard text controls. 
 165                (See EMAIL or phone number autoformat examples.) 
 167                <i>Note: This also governs whether backspace/delete operations 
 168                shift contents of field to right of cursor, or just blank the 
 171                Also, when combined with 'r', this indicates that the field 
 172                or control allows right insert anywhere within the current 
 173                non-empty value in the field.  (Otherwise right-insert behavior 
 174                is only performed to when the entire right-insertable field is 
 175                selected or the cursor is at the right edge of the field.</i> 
 178             ,  Allow grouping character in integer fields of numeric controls 
 179                and auto-group/regroup digits (if the result fits) when leaving 
 180                such a field.  (If specified, .SetValue() will attempt to 
 182                ',' is also the default grouping character.  To change the 
 183                grouping character and/or decimal character, use the groupChar 
 184                and decimalChar parameters, respectively. 
 185                Note: typing the "decimal point" character in such fields will 
 186                clip the value to that left of the cursor for integer 
 187                fields of controls with "integer" or "floating point" masks. 
 188                If the ',' format code is specified, this will also cause the 
 189                resulting digits to be regrouped properly, using the current 
 191             -  Prepend and reserve leading space for sign to mask and allow 
 192                signed values (negative #s shown in red by default.) Can be 
 193                used with argument useParensForNegatives (see below.) 
 194             0  integer fields get leading zeros 
 197             F  Auto-Fit: the control calulates its size from 
 198                the length of the template mask 
 199             V  validate entered chars against validRegex before allowing them 
 200                to be entered vs. being allowed by basic mask and then having 
 201                the resulting value just colored as invalid. 
 202                (See USSTATE autoformat demo for how this can be used.) 
 203             S  select entire field when navigating to new field 
 207   These controls have two options for the initial state of the control. 
 208   If a blank control with just the non-editable characters showing 
 209   is desired, simply leave the constructor variable fillChar as its 
 210   default (' ').  If you want some other character there, simply 
 211   change the fillChar to that value.  Note: changing the control's fillChar 
 212   will implicitly reset all of the fields' fillChars to this value. 
 214   If you need different default characters in each mask position, 
 215   you can specify a defaultValue parameter in the constructor, or 
 216   set them for each field individually. 
 217   This value must satisfy the non-editable characters of the mask, 
 218   but need not conform to the replaceable characters. 
 222   These parameters govern what character is used to group numbers 
 223   and is used to indicate the decimal point for numeric format controls. 
 224   The default groupChar is ',', the default decimalChar is '.' 
 225   By changing these, you can customize the presentation of numbers 
 227   eg: formatcodes = ',', groupChar="'"                   allows  12'345.34 
 228       formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34 
 230 <b>shiftDecimalChar=</b> 
 231   The default "shiftDecimalChar" (used for "backwards-tabbing" until 
 232   shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for 
 233   other keyboards, you may want to customize this, eg '?' for shift ',' on 
 234   AZERTY keyboards, ':' or ';' for other European keyboards, etc. 
 236 <b>useParensForNegatives=False</b> 
 237   This option can be used with signed numeric format controls to 
 238   indicate signs via () rather than '-'. 
 240 <b>autoSelect=False</b> 
 241   This option can be used to have a field or the control try to 
 242   auto-complete on each keystroke if choices have been specified. 
 244 <b>autoCompleteKeycodes=[]</b> 
 245   By default, DownArrow, PageUp and PageDown will auto-complete a 
 246   partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp 
 247   and PageDown will also auto-complete, but if the field already 
 248   contains a matched value, these keys will cycle through the list 
 249   of choices forward or backward as appropriate.  Shift-Up and 
 250   Shift-Down also take you to the next/previous field after any 
 251   auto-complete action. 
 253   Additional auto-complete keys can be specified via this parameter. 
 254   Any keys so specified will act like PageDown. 
 258 <b>Validating User Input: 
 259 ======================</b> 
 260   There are a variety of initialization parameters that are used to validate 
 261   user input.  These parameters can apply to the control as a whole, and/or 
 262   to individual fields: 
 264         excludeChars=   A string of characters to exclude even if otherwise allowed 
 265         includeChars=   A string of characters to allow even if otherwise disallowed 
 266         validRegex=     Use a regular expression to validate the contents of the text box 
 267         validRange=     Pass a rangeas list (low,high) to limit numeric fields/values 
 268         choices=        A list of strings that are allowed choices for the control. 
 269         choiceRequired= value must be member of choices list 
 270         compareNoCase=  Perform case-insensitive matching when validating against list 
 271                         <i>Note: for masked.ComboBox, this defaults to True.</i> 
 272         emptyInvalid=   Boolean indicating whether an empty value should be considered invalid 
 274         validFunc=      A function to call of the form: bool = func(candidate_value) 
 275                         which will return True if the candidate_value satisfies some 
 276                         external criteria for the control in addition to the the 
 277                         other validation, or False if not.  (This validation is 
 278                         applied last in the chain of validations.) 
 280         validRequired=  Boolean indicating whether or not keys that are allowed by the 
 281                         mask, but result in an invalid value are allowed to be entered 
 282                         into the control.  Setting this to True implies that a valid 
 283                         default value is set for the control. 
 285         retainFieldValidation= 
 286                         False by default; if True, this allows individual fields to 
 287                         retain their own validation constraints independently of any 
 288                         subsequent changes to the control's overall parameters. 
 290         validator=      Validators are not normally needed for masked controls, because 
 291                         of the nature of the validation and control of input.  However, 
 292                         you can supply one to provide data transfer routines for the 
 296 <b>Coloring Behavior: 
 297 ==================</b> 
 298   The following parameters have been provided to allow you to change the default 
 299   coloring behavior of the control.   These can be set at construction, or via 
 300   the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow': 
 302         emptyBackgroundColour=      Control Background color when identified as empty. Default=White 
 303         invalidBackgroundColour=    Control Background color when identified as Not valid. Default=Yellow 
 304         validBackgroundColour=      Control Background color when identified as Valid. Default=white 
 307   The following parameters control the default foreground color coloring behavior of the 
 308   control. Pass a color as string e.g. 'Yellow': 
 309         foregroundColour=           Control foreground color when value is not negative.  Default=Black 
 310         signedForegroundColour=     Control foreground color when value is negative. Default=Red 
 315   Each part of the mask that allows user input is considered a field.  The fields 
 316   are represented by their own class instances.  You can specify field-specific 
 317   constraints by constructing or accessing the field instances for the control 
 318   and then specifying those constraints via parameters. 
 321   This parameter allows you to specify Field instances containing 
 322   constraints for the individual fields of a control, eg: local 
 323   choice lists, validation rules, functions, regexps, etc. 
 324   It can be either an ordered list or a dictionary.  If a list, 
 325   the fields will be applied as fields 0, 1, 2, etc. 
 326   If a dictionary, it should be keyed by field index. 
 327   the values should be a instances of maskededit.Field. 
 329   Any field not represented by the list or dictionary will be 
 330   implicitly created by the control. 
 333     fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ] 
 336               1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']), 
 337               3: ( Field(choices=['01', '02', '03'], choiceRequired=True) 
 340   The following parameters are available for individual fields, with the 
 341   same semantics as for the whole control but applied to the field in question: 
 343     fillChar        # if set for a field, it will override the control's fillChar for that field 
 344     groupChar       # if set for a field, it will override the control's default 
 345     defaultValue    # sets field-specific default value; overrides any default from control 
 346     compareNoCase   # overrides control's settings 
 347     emptyInvalid    # determines whether field is required to be filled at all times 
 348     validRequired   # if set, requires field to contain valid value 
 350   If any of the above parameters are subsequently specified for the control as a 
 351   whole, that new value will be propagated to each field, unless the 
 352   retainFieldValidation control-level parameter is set. 
 354     formatcodes     # Augments control's settings 
 360     choiceRequired  #     '       '        ' 
 365 <b>Control Class Functions: 
 366 ======================== 
 367   .GetPlainValue(value=None)</b> 
 368                     Returns the value specified (or the control's text value 
 369                     not specified) without the formatting text. 
 370                     In the example above, might return phone no='3522640075', 
 371                     whereas control.GetValue() would return '(352) 264-0075' 
 373                     Returns the control's value to its default, and places the 
 374                     cursor at the beginning of the control. 
 376                     Does "smart replacement" of passed value into the control, as does 
 377                     the .Paste() method.  As with other text entry controls, the 
 378                     .SetValue() text replacement begins at left-edge of the control, 
 379                     with missing mask characters inserted as appropriate. 
 380                     .SetValue will also adjust integer, float or date mask entry values, 
 381                     adding commas, auto-completing years, etc. as appropriate. 
 382                     For "right-aligned" numeric controls, it will also now automatically 
 383                     right-adjust any value whose length is less than the width of the 
 384                     control before attempting to set the value. 
 385                     If a value does not follow the format of the control's mask, or will 
 386                     not fit into the control, a ValueError exception will be raised. 
 388                       mask = '(###) ###-####' 
 389                           .SetValue('1234567890')           => '(123) 456-7890' 
 390                           .SetValue('(123)4567890')         => '(123) 456-7890' 
 391                           .SetValue('(123)456-7890')        => '(123) 456-7890' 
 392                           .SetValue('123/4567-890')         => illegal paste; ValueError 
 394                       mask = '#{6}.#{2}', formatcodes = '_,-', 
 395                           .SetValue('111')                  => ' 111   .  ' 
 396                           .SetValue(' %9.2f' % -111.12345 ) => '   -111.12' 
 397                           .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00' 
 398                           .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError 
 400                       mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control 
 401                           .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit 
 402                           .SetValue('%.2f' % 111 )          => '    111.00' 
 403                           .SetValue('%.2f' % -111.12345 )   => '   -111.12' 
 406   <b>.IsValid(value=None)</b> 
 407                     Returns True if the value specified (or the value of the control 
 408                     if not specified) passes validation tests 
 409   <b>.IsEmpty(value=None)</b> 
 410                     Returns True if the value specified (or the value of the control 
 411                     if not specified) is equal to an "empty value," ie. all 
 412                     editable characters == the fillChar for their respective fields. 
 413   <b>.IsDefault(value=None)</b> 
 414                     Returns True if the value specified (or the value of the control 
 415                     if not specified) is equal to the initial value of the control. 
 418                     Recolors the control as appropriate to its current settings. 
 420   <b>.SetCtrlParameters(**kwargs)</b> 
 421                     This function allows you to set up and/or change the control parameters 
 422                     after construction; it takes a list of key/value pairs as arguments, 
 423                     where the keys can be any of the mask-specific parameters in the constructor. 
 425                         ctl = masked.TextCtrl( self, -1 ) 
 426                         ctl.SetCtrlParameters( mask='###-####', 
 427                                                defaultValue='555-1212', 
 430   <b>.GetCtrlParameter(parametername)</b> 
 431                     This function allows you to retrieve the current value of a parameter 
 434   <b><i>Note:</i></b> Each of the control parameters can also be set using its 
 435       own Set and Get function.  These functions follow a regular form: 
 436       All of the parameter names start with lower case; for their 
 437       corresponding Set/Get function, the parameter name is capitalized. 
 438       Eg: ctl.SetMask('###-####') 
 439           ctl.SetDefaultValue('555-1212') 
 440           ctl.GetChoiceRequired() 
 443   <b><i>Note:</i></b> After any change in parameters, the choices for the 
 444       control are reevaluated to ensure that they are still legal.  If you 
 445       have large choice lists, it is therefore more efficient to set parameters 
 446       before setting the choices available. 
 448   <b>.SetFieldParameters(field_index, **kwargs)</b> 
 449                     This function allows you to specify change individual field 
 450                     parameters after construction. (Indices are 0-based.) 
 452   <b>.GetFieldParameter(field_index, parametername)</b> 
 453                     Allows the retrieval of field parameters after construction 
 456 The control detects certain common constructions. In order to use the signed feature 
 457 (negative numbers and coloring), the mask has to be all numbers with optionally one 
 458 decimal point. Without a decimal (e.g. '######', the control will treat it as an integer 
 459 value. With a decimal (e.g. '###.##'), the control will act as a floating point control 
 460 (i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the 
 461 integer control truncates the value.  However, for a true numeric control, 
 462 masked.NumCtrl provides all this, and true numeric input/output support as well. 
 465 Check your controls by calling each control's .IsValid() function and the 
 466 .IsEmpty() function to determine which controls have been a) filled in and 
 467 b) filled in properly. 
 470 Regular expression validations can be used flexibly and creatively. 
 471 Take a look at the demo; the zip-code validation succeeds as long as the 
 472 first five numerals are entered. the last four are optional, but if 
 473 any are entered, there must be 4 to be valid. 
 475 <B>masked.Ctrl Configuration 
 476 ==========================</B> 
 477 masked.Ctrl works by looking for a special <b><i>controlType</i></b> 
 478 parameter in the variable arguments of the control, to determine 
 479 what kind of instance to return. 
 480 controlType can be one of: 
 488 These constants are also available individually, ie, you can 
 489 use either of the following: 
 491     from wxPython.wx.lib.masked import MaskedCtrl, controlTypes 
 492     from wxPython.wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR 
 494 If not specified as a keyword argument, the default controlType is 
 499 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
 504   All methods of the Mixin that are not meant to be exposed to the external 
 505   interface are prefaced with '_'.  Those functions that are primarily 
 506   intended to be internal subroutines subsequently start with a lower-case 
 507   letter; those that are primarily intended to be used and/or overridden 
 508   by derived subclasses start with a capital letter. 
 510   The following methods must be used and/or defined when deriving a control 
 511   from MaskedEditMixin.  NOTE: if deriving from a *masked edit* control 
 512   (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary, 
 513   as it's already been done for you in the base class. 
 516                         This function must be called after the associated base 
 517                         control has been initialized in the subclass __init__ 
 518                         function.  It sets the initial value of the control, 
 519                         either to the value specified if non-empty, the 
 520                         default value if specified, or the "template" for 
 521                         the empty control as necessary.  It will also set/reset 
 522                         the font if necessary and apply formatting to the 
 523                         control at this time. 
 527                         Each class derived from MaskedEditMixin must define 
 528                         the function for getting the start and end of the 
 529                         current text selection.  The reason for this is 
 530                         that not all controls have the same function name for 
 531                         doing this; eg. wx.TextCtrl uses .GetSelection(), 
 532                         whereas we had to write a .GetMark() function for 
 533                         wxComboBox, because .GetSelection() for the control 
 534                         gets the currently selected list item from the combo 
 535                         box, and the control doesn't (yet) natively provide 
 536                         a means of determining the text selection. 
 539                         Similarly to _GetSelection, each class derived from 
 540                         MaskedEditMixin must define the function for setting 
 541                         the start and end of the current text selection. 
 542                         (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for 
 545         ._GetInsertionPoint() 
 546         ._SetInsertionPoint() 
 548                         For consistency, and because the mixin shouldn't rely 
 549                         on fixed names for any manipulations it does of any of 
 550                         the base controls, we require each class derived from 
 551                         MaskedEditMixin to define these functions as well. 
 554         ._SetValue()    REQUIRED 
 555                         Each class derived from MaskedEditMixin must define 
 556                         the functions used to get and set the raw value of the 
 558                         This is necessary so that recursion doesn't take place 
 559                         when setting the value, and so that the mixin can 
 560                         call the appropriate function after doing all its 
 561                         validation and manipulation without knowing what kind 
 562                         of base control it was mixed in with.  To handle undo 
 563                         functionality, the ._SetValue() must record the current 
 564                         selection prior to setting the value. 
 570                         Each class derived from MaskedEditMixin must redefine 
 571                         these functions to call the _Cut(), _Paste(), _Undo() 
 572                         and _SetValue() methods, respectively for the control, 
 573                         so as to prevent programmatic corruption of the control's 
 574                         value.  This must be done in each derivation, as the 
 575                         mixin cannot itself override a member of a sibling class. 
 578                         Each class derived from MaskedEditMixin must define 
 579                         the function used to refresh the base control. 
 582                         Each class derived from MaskedEditMixin must redefine 
 583                         this function so that it checks the validity of the 
 584                         control (via self._CheckValid) and then refreshes 
 585                         control using the base class method. 
 587         ._IsEditable()  REQUIRED 
 588                         Each class derived from MaskedEditMixin must define 
 589                         the function used to determine if the base control is 
 590                         editable or not.  (For masked.ComboBox, this has to 
 591                         be done with code, rather than specifying the proper 
 592                         function in the base control, as there isn't one...) 
 593         ._CalcSize()    REQUIRED 
 594                         Each class derived from MaskedEditMixin must define 
 595                         the function used to determine how wide the control 
 596                         should be given the mask.  (The mixin function 
 597                         ._calcSize() provides a baseline estimate.) 
 602   Event handlers are "chained", and MaskedEditMixin usually 
 603   swallows most of the events it sees, thereby preventing any other 
 604   handlers from firing in the chain.  It is therefore required that 
 605   each class derivation using the mixin to have an option to hook up 
 606   the event handlers itself or forego this operation and let a 
 607   subclass of the masked control do so.  For this reason, each 
 608   subclass should probably include the following code: 
 610     if setupEventHandling: 
 611         ## Setup event handlers 
 612         EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection 
 613         EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator 
 614         EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick 
 615         EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu 
 616         EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab. 
 617         EVT_CHAR( self, self._OnChar )              ## handle each keypress 
 618         EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep 
 619                                                             ## track of previous value for undo 
 621   where setupEventHandling is an argument to its constructor. 
 623   These 5 handlers must be "wired up" for the masked edit 
 624   controls to provide default behavior.  (The setupEventHandling 
 625   is an argument to masked.TextCtrl and masked.ComboBox, so 
 626   that controls derived from *them* may replace one of these 
 627   handlers if they so choose.) 
 629   If your derived control wants to preprocess events before 
 630   taking action, it should then set up the event handling itself, 
 631   so it can be first in the event handler chain. 
 634   The following routines are available to facilitate changing 
 635   the default behavior of masked edit controls: 
 637         ._SetKeycodeHandler(keycode, func) 
 638         ._SetKeyHandler(char, func) 
 639                         Use to replace default handling for any given keycode. 
 640                         func should take the key event as argument and return 
 641                         False if no further action is required to handle the 
 643                             self._SetKeycodeHandler(WXK_UP, self.IncrementValue) 
 644                             self._SetKeyHandler('-', self._OnChangeSign) 
 646         "Navigation" keys are assumed to change the cursor position, and 
 647         therefore don't cause automatic motion of the cursor as insertable 
 650         ._AddNavKeycode(keycode, handler=None) 
 651         ._AddNavKey(char, handler=None) 
 652                         Allows controls to specify other keys (and optional handlers) 
 653                         to be treated as navigational characters. (eg. '.' in IpAddrCtrl) 
 655         ._GetNavKeycodes()  Returns the current list of navigational keycodes. 
 657         ._SetNavKeycodes(key_func_tuples) 
 658                         Allows replacement of the current list of keycode 
 659                         processed as navigation keys, and bind associated 
 660                         optional keyhandlers. argument is a list of key/handler 
 661                         tuples.  Passing a value of None for the handler in a 
 662                         given tuple indicates that default processing for the key 
 665         ._FindField(pos) Returns the Field object associated with this position 
 668         ._FindFieldExtent(pos, getslice=False, value=None) 
 669                         Returns edit_start, edit_end of the field corresponding 
 670                         to the specified position within the control, and 
 671                         optionally also returns the current contents of that field. 
 672                         If value is specified, it will retrieve the slice the corresponding 
 673                         slice from that value, rather than the current value of the 
 677                         This is, the function that gets called for a given position 
 678                         whenever the cursor is adjusted to leave a given field. 
 679                         By default, it adjusts the year in date fields if mask is a date, 
 680                         It can be overridden by a derived class to 
 681                         adjust the value of the control at that time. 
 682                         (eg. IpAddrCtrl reformats the address in this way.) 
 684         ._Change()      Called by internal EVT_TEXT handler. Return False to force 
 685                         skip of the normal class change event. 
 686         ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force 
 687                         skip of the normal class keypress event. 
 688         ._LostFocus()   Called by internal EVT_KILL_FOCUS handler 
 691                         This is the default EVT_KEY_DOWN routine; it just checks for 
 692                         "navigation keys", and if event.ControlDown(), it fires the 
 693                         mixin's _OnChar() routine, as such events are not always seen 
 694                         by the "cooked" EVT_CHAR routine. 
 696         ._OnChar(event) This is the main EVT_CHAR handler for the 
 699     The following routines are used to handle standard actions 
 701         _OnArrow(event)         used for arrow navigation events 
 702         _OnCtrl_A(event)        'select all' 
 703         _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive) 
 704         _OnCtrl_S(event)        'save' (does nothing) 
 705         _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste 
 706         _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection 
 707         _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any) 
 709         _OnChangeField(event)   primarily used for tab events, but can be 
 710                                 used for other keys (eg. '.' in IpAddrCtrl) 
 712         _OnErase(event)         used for backspace and delete 
 716     The following routine provides a hook back to any class derivations, so that 
 717     they can react to parameter changes before any value is set/reset as a result of 
 718     those changes.  (eg. masked.ComboBox needs to detect when the choices list is 
 719     modified, either implicitly or explicitly, so it can reset the base control 
 720     to have the appropriate choice list *before* the initial value is reset to match.) 
 722         _OnCtrlParametersChanged() 
 726     For convenience, each class derived from MaskedEditMixin should 
 727     define an accessors mixin, so that it exposes only those parameters 
 728     that make sense for the derivation.  This is done with an intermediate 
 729     level of inheritance, ie: 
 731     class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): 
 733     class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): 
 734     class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): 
 735     class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): 
 736     class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): 
 737     class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): 
 741     Each accessors mixin defines Get/Set functions for the base class parameters 
 742     that are appropriate for that derivation. 
 743     This allows the base classes to be "more generic," exposing the widest 
 744     set of options, while not requiring derived classes to be so general. 
 755 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
 756 # be a good place to implement the 2.3 logger class 
 757 from wx
.tools
.dbg 
import Logger
 
 762 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 764 ## Constants for identifying control keys and classes of keys: 
 766 WXK_CTRL_A 
= (ord('A')+1) - ord('A')   ## These keys are not already defined in wx 
 767 WXK_CTRL_C 
= (ord('C')+1) - ord('A') 
 768 WXK_CTRL_S 
= (ord('S')+1) - ord('A') 
 769 WXK_CTRL_V 
= (ord('V')+1) - ord('A') 
 770 WXK_CTRL_X 
= (ord('X')+1) - ord('A') 
 771 WXK_CTRL_Z 
= (ord('Z')+1) - ord('A') 
 774     wx
.WXK_BACK
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
, wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_TAB
, 
 775     wx
.WXK_HOME
, wx
.WXK_END
, wx
.WXK_RETURN
, wx
.WXK_PRIOR
, wx
.WXK_NEXT
 
 779     wx
.WXK_BACK
, wx
.WXK_DELETE
, WXK_CTRL_A
, WXK_CTRL_C
, WXK_CTRL_S
, WXK_CTRL_V
, 
 780     WXK_CTRL_X
, WXK_CTRL_Z
 
 784 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 786 ## Constants for masking. This is where mask characters 
 788 ##  maskchars used to identify valid mask characters from all others 
 789 ##   #- allow numeric 0-9 only 
 790 ##   A- allow uppercase only. Combine with forceupper to force lowercase to upper 
 791 ##   a- allow lowercase only. Combine with forcelower to force upper to lowercase 
 792 ##   X- allow any character (string.letters, string.punctuation, string.digits) 
 793 ## Note: locale settings affect what "uppercase", lowercase, etc comprise. 
 795 maskchars 
= ("#","A","a","X","C","N", '&') 
 797 months 
= '(01|02|03|04|05|06|07|08|09|10|11|12)' 
 798 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)' 
 799 charmonths_dict 
= {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 
 800                    'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} 
 802 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)' 
 803 hours  
= '(0\d| \d|1[012])' 
 804 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)' 
 805 minutes 
= """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ 
 806 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ 
 807 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ 
 810 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' 
 812 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(',') 
 814 state_names 
= ['Alabama','Alaska','Arizona','Arkansas', 
 815                'California','Colorado','Connecticut', 
 816                'Delaware','District of Columbia', 
 817                'Florida','Georgia','Hawaii', 
 818                'Idaho','Illinois','Indiana','Iowa', 
 819                'Kansas','Kentucky','Louisiana', 
 820                'Maine','Maryland','Massachusetts','Michigan', 
 821                'Minnesota','Mississippi','Missouri','Montana', 
 822                'Nebraska','Nevada','New Hampshire','New Jersey', 
 823                'New Mexico','New York','North Carolina','North Dakokta', 
 824                'Ohio','Oklahoma','Oregon', 
 825                'Pennsylvania','Puerto Rico','Rhode Island', 
 826                'South Carolina','South Dakota', 
 827                'Tennessee','Texas','Utah', 
 828                'Vermont','Virginia', 
 829                'Washington','West Virginia', 
 830                'Wisconsin','Wyoming'] 
 832 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
 834 ## The following dictionary defines the current set of autoformats: 
 838            'mask': "(###) ###-#### x:###", 
 839            'formatcodes': 'F^->', 
 840            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 841            'description': "Phone Number w/opt. ext" 
 844            'mask': "###-###-#### x:###", 
 845            'formatcodes': 'F^->', 
 846            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 847            'description': "Phone Number\n (w/hyphens and opt. ext)" 
 850            'mask': "(###) ###-####", 
 851            'formatcodes': 'F^->', 
 852            'validRegex': "^\(\d{3}\) \d{3}-\d{4}", 
 853            'description': "Phone Number only" 
 856            'mask': "###-###-####", 
 857            'formatcodes': 'F^->', 
 858            'validRegex': "^\d{3}-\d{3}-\d{4}", 
 859            'description': "Phone Number\n(w/hyphens)" 
 863            'formatcodes': 'F!V', 
 864            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(states
,'|'), 
 866            'choiceRequired': True, 
 867            'description': "US State Code" 
 870            'mask': "ACCCCCCCCCCCCCCCCCCC", 
 872            'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string
.join(state_names
,'|'), 
 873            'choices': state_names
, 
 874            'choiceRequired': True, 
 875            'description': "US State Name" 
 878        "USDATETIMEMMDDYYYY/HHMMSS": { 
 879            'mask': "##/##/#### ##:##:## AM", 
 880            'excludeChars': am_pm_exclude
, 
 881            'formatcodes': 'DF!', 
 882            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 883            'description': "US Date + Time" 
 885        "USDATETIMEMMDDYYYY-HHMMSS": { 
 886            'mask': "##-##-#### ##:##:## AM", 
 887            'excludeChars': am_pm_exclude
, 
 888            'formatcodes': 'DF!', 
 889            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 890            'description': "US Date + Time\n(w/hypens)" 
 892        "USDATE24HRTIMEMMDDYYYY/HHMMSS": { 
 893            'mask': "##/##/#### ##:##:##", 
 895            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 896            'description': "US Date + 24Hr (Military) Time" 
 898        "USDATE24HRTIMEMMDDYYYY-HHMMSS": { 
 899            'mask': "##-##-#### ##:##:##", 
 901            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
 902            'description': "US Date + 24Hr Time\n(w/hypens)" 
 904        "USDATETIMEMMDDYYYY/HHMM": { 
 905            'mask': "##/##/#### ##:## AM", 
 906            'excludeChars': am_pm_exclude
, 
 907            'formatcodes': 'DF!', 
 908            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 909            'description': "US Date + Time\n(without seconds)" 
 911        "USDATE24HRTIMEMMDDYYYY/HHMM": { 
 912            'mask': "##/##/#### ##:##", 
 914            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 915            'description': "US Date + 24Hr Time\n(without seconds)" 
 917        "USDATETIMEMMDDYYYY-HHMM": { 
 918            'mask': "##-##-#### ##:## AM", 
 919            'excludeChars': am_pm_exclude
, 
 920            'formatcodes': 'DF!', 
 921            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
 922            'description': "US Date + Time\n(w/hypens and w/o secs)" 
 924        "USDATE24HRTIMEMMDDYYYY-HHMM": { 
 925            'mask': "##-##-#### ##:##", 
 927            'validRegex': '^' + months 
+ '-' + days 
+ '-' + '\d{4} ' + milhours 
+ ':' + minutes
, 
 928            'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" 
 931            'mask': "##/##/####", 
 933            'validRegex': '^' + months 
+ '/' + days 
+ '/' + '\d{4}', 
 934            'description': "US Date\n(MMDDYYYY)" 
 939            'validRegex': '^' + months 
+ '/' + days 
+ '/\d\d', 
 940            'description': "US Date\n(MMDDYY)" 
 943            'mask': "##-##-####", 
 945            'validRegex': '^' + months 
+ '-' + days 
+ '-' +'\d{4}', 
 946            'description': "MM-DD-YYYY" 
 950            'mask': "####/##/##", 
 952            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days
, 
 953            'description': "YYYY/MM/DD" 
 956            'mask': "####.##.##", 
 958            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days
, 
 959            'description': "YYYY.MM.DD" 
 962            'mask': "##/##/####", 
 964            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4}', 
 965            'description': "DD/MM/YYYY" 
 968            'mask': "##.##.####", 
 970            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4}', 
 971            'description': "DD.MM.YYYY" 
 973        "EUDATEDDMMMYYYY.": { 
 974            'mask': "##.CCC.####", 
 976            'validRegex': '^' + days 
+ '.' + charmonths 
+ '.' + '\d{4}', 
 977            'description': "DD.Month.YYYY" 
 979        "EUDATEDDMMMYYYY/": { 
 980            'mask': "##/CCC/####", 
 982            'validRegex': '^' + days 
+ '/' + charmonths 
+ '/' + '\d{4}', 
 983            'description': "DD/Month/YYYY" 
 986        "EUDATETIMEYYYYMMDD/HHMMSS": { 
 987            'mask': "####/##/## ##:##:## AM", 
 988            'excludeChars': am_pm_exclude
, 
 989            'formatcodes': 'DF!', 
 990            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 991            'description': "YYYY/MM/DD HH:MM:SS" 
 993        "EUDATETIMEYYYYMMDD.HHMMSS": { 
 994            'mask': "####.##.## ##:##:## AM", 
 995            'excludeChars': am_pm_exclude
, 
 996            'formatcodes': 'DF!', 
 997            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
 998            'description': "YYYY.MM.DD HH:MM:SS" 
1000        "EUDATETIMEDDMMYYYY/HHMMSS": { 
1001            'mask': "##/##/#### ##:##:## AM", 
1002            'excludeChars': am_pm_exclude
, 
1003            'formatcodes': 'DF!', 
1004            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1005            'description': "DD/MM/YYYY HH:MM:SS" 
1007        "EUDATETIMEDDMMYYYY.HHMMSS": { 
1008            'mask': "##.##.#### ##:##:## AM", 
1009            'excludeChars': am_pm_exclude
, 
1010            'formatcodes': 'DF!', 
1011            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1012            'description': "DD.MM.YYYY HH:MM:SS" 
1015        "EUDATETIMEYYYYMMDD/HHMM": { 
1016            'mask': "####/##/## ##:## AM", 
1017            'excludeChars': am_pm_exclude
, 
1018            'formatcodes': 'DF!', 
1019            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1020            'description': "YYYY/MM/DD HH:MM" 
1022        "EUDATETIMEYYYYMMDD.HHMM": { 
1023            'mask': "####.##.## ##:## AM", 
1024            'excludeChars': am_pm_exclude
, 
1025            'formatcodes': 'DF!', 
1026            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1027            'description': "YYYY.MM.DD HH:MM" 
1029        "EUDATETIMEDDMMYYYY/HHMM": { 
1030            'mask': "##/##/#### ##:## AM", 
1031            'excludeChars': am_pm_exclude
, 
1032            'formatcodes': 'DF!', 
1033            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1034            'description': "DD/MM/YYYY HH:MM" 
1036        "EUDATETIMEDDMMYYYY.HHMM": { 
1037            'mask': "##.##.#### ##:## AM", 
1038            'excludeChars': am_pm_exclude
, 
1039            'formatcodes': 'DF!', 
1040            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1041            'description': "DD.MM.YYYY HH:MM" 
1044        "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { 
1045            'mask': "####/##/## ##:##:##", 
1046            'formatcodes': 'DF', 
1047            'validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1048            'description': "YYYY/MM/DD 24Hr Time" 
1050        "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { 
1051            'mask': "####.##.## ##:##:##", 
1052            'formatcodes': 'DF', 
1053            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1054            'description': "YYYY.MM.DD 24Hr Time" 
1056        "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { 
1057            'mask': "##/##/#### ##:##:##", 
1058            'formatcodes': 'DF', 
1059            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1060            'description': "DD/MM/YYYY 24Hr Time" 
1062        "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { 
1063            'mask': "##.##.#### ##:##:##", 
1064            'formatcodes': 'DF', 
1065            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1066            'description': "DD.MM.YYYY 24Hr Time" 
1068        "EUDATE24HRTIMEYYYYMMDD/HHMM": { 
1069            'mask': "####/##/## ##:##", 
1070            'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months 
+ '/' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1071            'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" 
1073        "EUDATE24HRTIMEYYYYMMDD.HHMM": { 
1074            'mask': "####.##.## ##:##", 
1075            'formatcodes': 'DF', 
1076            'validRegex': '^' + '\d{4}'+ '.' + months 
+ '.' + days 
+ ' ' + milhours 
+ ':' + minutes
, 
1077            'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" 
1079        "EUDATE24HRTIMEDDMMYYYY/HHMM": { 
1080            'mask': "##/##/#### ##:##", 
1081            'formatcodes': 'DF', 
1082            'validRegex': '^' + days 
+ '/' + months 
+ '/' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1083            'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" 
1085        "EUDATE24HRTIMEDDMMYYYY.HHMM": { 
1086            'mask': "##.##.#### ##:##", 
1087            'formatcodes': 'DF', 
1088            'validRegex': '^' + days 
+ '.' + months 
+ '.' + '\d{4} ' + milhours 
+ ':' + minutes
, 
1089            'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" 
1093            'mask': "##:##:## AM", 
1094            'excludeChars': am_pm_exclude
, 
1095            'formatcodes': 'TF!', 
1096            'validRegex': '^' + hours 
+ ':' + minutes 
+ ':' + seconds 
+ ' (A|P)M', 
1097            'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" 
1101            'excludeChars': am_pm_exclude
, 
1102            'formatcodes': 'TF!', 
1103            'validRegex': '^' + hours 
+ ':' + minutes 
+ ' (A|P)M', 
1104            'description': "HH:MM (A|P)M\n(see TimeCtrl)" 
1108            'formatcodes': 'TF', 
1109            'validRegex': '^' + milhours 
+ ':' + minutes 
+ ':' + seconds
, 
1110            'description': "24Hr HH:MM:SS\n(see TimeCtrl)" 
1114            'formatcodes': 'TF', 
1115            'validRegex': '^' + milhours 
+ ':' + minutes
, 
1116            'description': "24Hr HH:MM\n(see TimeCtrl)" 
1119            'mask': "###-##-####", 
1121            'validRegex': "\d{3}-\d{2}-\d{4}", 
1122            'description': "Social Sec#" 
1125            'mask': "####-####-####-####", 
1127            'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", 
1128            'description': "Credit Card" 
1133            'validRegex': "^" + months 
+ "/\d\d", 
1134            'description': "Expiration MM/YY" 
1139            'validRegex': "^\d{5}", 
1140            'description': "US 5-digit zip code" 
1143            'mask': "#####-####", 
1145            'validRegex': "\d{5}-(\s{4}|\d{4})", 
1146            'description': "US zip+4 code" 
1151            'validRegex': "^0.\d\d", 
1152            'description': "Percentage" 
1157            'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]", 
1158            'description': "Age" 
1161            'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 
1162            'excludeChars': " \\/*&%$#!+='\"", 
1163            'formatcodes': "F>", 
1164            '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}\]) *$", 
1165            'description': "Email address" 
1168            'mask': "###.###.###.###", 
1169            'formatcodes': 'F_Sr', 
1170            '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}", 
1171            'description': "IP Address\n(see IpAddrCtrl)" 
1175 # build demo-friendly dictionary of descriptions of autoformats 
1177 for key
, value 
in masktags
.items(): 
1178     autoformats
.append((key
, value
['description'])) 
1181 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1185               'index': None,                    ## which field of mask; set by parent control. 
1186               'mask': "",                       ## mask chars for this field 
1187               'extent': (),                     ## (edit start, edit_end) of field; set by parent control. 
1188               'formatcodes':  "",               ## codes indicating formatting options for the control 
1189               'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given 
1190               'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits 
1191               'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction 
1192               'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard 
1193               'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. 
1194               'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar 
1195               'excludeChars': "",               ## optional string of chars to exclude even if main mask type does 
1196               'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't 
1197               'validRegex':   "",               ## optional regular expression to use to validate the control 
1198               'validRange':   (),               ## Optional hi-low range for numerics 
1199               'choices':    [],                 ## Optional list for character expressions 
1200               'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list 
1201               'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search 
1202               'autoSelect': False,              ## Set to True to try auto-completion on each keystroke: 
1203               'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl 
1204               'validRequired': False,           ## Set to True to disallow input that results in an invalid value 
1205               'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID 
1206               'description': "",                ## primarily for autoformats, but could be useful elsewhere 
1209     # This list contains all parameters that when set at the control level should 
1210     # propagate down to each field: 
1211     propagating_params 
= ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', 
1212                           'compareNoCase', 'emptyInvalid', 'validRequired') 
1214     def __init__(self
, **kwargs
): 
1216         This is the "constructor" for setting up parameters for fields. 
1217         a field_index of -1 is used to indicate "the entire control." 
1219 ####        dbg('Field::Field', indent=1) 
1220         # Validate legitimate set of parameters: 
1221         for key 
in kwargs
.keys(): 
1222             if key 
not in Field
.valid_params
.keys(): 
1224                 raise TypeError('invalid parameter "%s"' % (key
)) 
1226         # Set defaults for each parameter for this instance, and fully 
1227         # populate initial parameter list for configuration: 
1228         for key
, value 
in Field
.valid_params
.items(): 
1229             setattr(self
, '_' + key
, copy
.copy(value
)) 
1230             if not kwargs
.has_key(key
): 
1231                 kwargs
[key
] = copy
.copy(value
) 
1233         self
._autoCompleteIndex 
= -1 
1234         self
._SetParameters
(**kwargs
) 
1235         self
._ValidateParameters
(**kwargs
) 
1240     def _SetParameters(self
, **kwargs
): 
1242         This function can be used to set individual or multiple parameters for 
1243         a masked edit field parameter after construction. 
1246 ##        dbg('maskededit.Field::_SetParameters', indent=1) 
1247         # Validate keyword arguments: 
1248         for key 
in kwargs
.keys(): 
1249             if key 
not in Field
.valid_params
.keys(): 
1250 ##                dbg(indent=0, suspend=0) 
1251                 raise AttributeError('invalid keyword argument "%s"' % key
) 
1253         if self
._index 
is not None: dbg('field index:', self
._index
) 
1254 ##        dbg('parameters:', indent=1) 
1255         for key
, value 
in kwargs
.items(): 
1256 ##            dbg('%s:' % key, value) 
1261         old_fillChar 
= self
._fillChar   
# store so we can change choice lists accordingly if it changes 
1263         # First, Assign all parameters specified: 
1264         for key 
in Field
.valid_params
.keys(): 
1265             if kwargs
.has_key(key
): 
1266                 setattr(self
, '_' + key
, kwargs
[key
] ) 
1268         if kwargs
.has_key('formatcodes'):   # (set/changed) 
1269             self
._forceupper  
= '!' in self
._formatcodes
 
1270             self
._forcelower  
= '^' in self
._formatcodes
 
1271             self
._groupdigits 
= ',' in self
._formatcodes
 
1272             self
._okSpaces    
= '_' in self
._formatcodes
 
1273             self
._padZero     
= '0' in self
._formatcodes
 
1274             self
._autofit     
= 'F' in self
._formatcodes
 
1275             self
._insertRight 
= 'r' in self
._formatcodes
 
1276             self
._allowInsert 
= '>' in self
._formatcodes
 
1277             self
._alignRight  
= 'R' in self
._formatcodes 
or 'r' in self
._formatcodes
 
1278             self
._moveOnFieldFull 
= not '<' in self
._formatcodes
 
1279             self
._selectOnFieldEntry 
= 'S' in self
._formatcodes
 
1281             if kwargs
.has_key('groupChar'): 
1282                 self
._groupChar 
= kwargs
['groupChar'] 
1283             if kwargs
.has_key('decimalChar'): 
1284                 self
._decimalChar 
= kwargs
['decimalChar'] 
1285             if kwargs
.has_key('shiftDecimalChar'): 
1286                 self
._shiftDecimalChar 
= kwargs
['shiftDecimalChar'] 
1288         if kwargs
.has_key('formatcodes') or kwargs
.has_key('validRegex'): 
1289             self
._regexMask   
= 'V' in self
._formatcodes 
and self
._validRegex
 
1291         if kwargs
.has_key('fillChar'): 
1292             self
._old
_fillChar 
= old_fillChar
 
1293 ####            dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1295         if kwargs
.has_key('mask') or kwargs
.has_key('validRegex'):  # (set/changed) 
1296             self
._isInt 
= isInteger(self
._mask
) 
1297 ##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) 
1299 ##        dbg(indent=0, suspend=0) 
1302     def _ValidateParameters(self
, **kwargs
): 
1304         This function can be used to validate individual or multiple parameters for 
1305         a masked edit field parameter after construction. 
1308 ##        dbg('maskededit.Field::_ValidateParameters', indent=1) 
1309         if self
._index 
is not None: dbg('field index:', self
._index
) 
1310 ####        dbg('parameters:', indent=1) 
1311 ##        for key, value in kwargs.items(): 
1312 ####            dbg('%s:' % key, value) 
1314 ####        dbg("self._old_fillChar: '%s'" % self._old_fillChar) 
1316         # Verify proper numeric format params: 
1317         if self
._groupdigits 
and self
._groupChar 
== self
._decimalChar
: 
1318 ##            dbg(indent=0, suspend=0) 
1319             raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self
._groupChar
, self
._decimalChar
)) 
1322         # Now go do validation, semantic and inter-dependency parameter processing: 
1323         if kwargs
.has_key('choices') or kwargs
.has_key('compareNoCase') or kwargs
.has_key('choiceRequired'): # (set/changed) 
1325             self
._compareChoices 
= [choice
.strip() for choice 
in self
._choices
] 
1327             if self
._compareNoCase 
and self
._choices
: 
1328                 self
._compareChoices 
= [item
.lower() for item 
in self
._compareChoices
] 
1330             if kwargs
.has_key('choices'): 
1331                 self
._autoCompleteIndex 
= -1 
1334         if kwargs
.has_key('validRegex'):    # (set/changed) 
1335             if self
._validRegex
: 
1337                     if self
._compareNoCase
: 
1338                         self
._filter 
= re
.compile(self
._validRegex
, re
.IGNORECASE
) 
1340                         self
._filter 
= re
.compile(self
._validRegex
) 
1342 ##                    dbg(indent=0, suspend=0) 
1343                     raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self
._index
), self
._validRegex
)) 
1347         if kwargs
.has_key('validRange'):    # (set/changed) 
1348             self
._hasRange  
= False 
1351             if self
._validRange
: 
1352                 if type(self
._validRange
) != types
.TupleType 
or len( self
._validRange 
)!= 2 or self
._validRange
[0] > self
._validRange
[1]: 
1353 ##                    dbg(indent=0, suspend=0) 
1354                     raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' 
1355                                     % (str(self
._index
), repr(self
._validRange
)) ) 
1357                 self
._hasRange  
= True 
1358                 self
._rangeLow  
= self
._validRange
[0] 
1359                 self
._rangeHigh 
= self
._validRange
[1] 
1361         if kwargs
.has_key('choices') or (len(self
._choices
) and len(self
._choices
[0]) != len(self
._mask
)):       # (set/changed) 
1362             self
._hasList   
= False 
1363             if self
._choices 
and type(self
._choices
) not in (types
.TupleType
, types
.ListType
): 
1364 ##                dbg(indent=0, suspend=0) 
1365                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1366             elif len( self
._choices
) > 0: 
1367                 for choice 
in self
._choices
: 
1368                     if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
1369 ##                        dbg(indent=0, suspend=0) 
1370                         raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
1372                 length 
= len(self
._mask
) 
1373 ##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) 
1374                 if len(self
._choices
) and length
: 
1375                     if len(self
._choices
[0]) > length
: 
1376                         # changed mask without respecifying choices; readjust the width as appropriate: 
1377                         self
._choices 
= [choice
.strip() for choice 
in self
._choices
] 
1378                     if self
._alignRight
: 
1379                         self
._choices 
= [choice
.rjust( length 
) for choice 
in self
._choices
] 
1381                         self
._choices 
= [choice
.ljust( length 
) for choice 
in self
._choices
] 
1382 ##                    dbg('aligned choices:', self._choices) 
1384                 if hasattr(self
, '_template'): 
1385                     # Verify each choice specified is valid: 
1386                     for choice 
in self
._choices
: 
1387                         if self
.IsEmpty(choice
) and not self
._validRequired
: 
1388                             # allow empty values even if invalid, (just colored differently) 
1390                         if not self
.IsValid(choice
): 
1391 ##                            dbg(indent=0, suspend=0) 
1392                             raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
1393                 self
._hasList 
= True 
1395 ####        dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) 
1396 ####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) 
1397         if kwargs
.has_key('fillChar') and len(self
._choices
) > 0: 
1398             if kwargs
['fillChar'] != ' ': 
1399                 self
._choices 
= [choice
.replace(' ', self
._fillChar
) for choice 
in self
._choices
] 
1401                 self
._choices 
= [choice
.replace(self
._old
_fillChar
, self
._fillChar
) for choice 
in self
._choices
] 
1402 ##            dbg('updated choices:', self._choices) 
1405         if kwargs
.has_key('autoSelect') and kwargs
['autoSelect']: 
1406             if not self
._hasList
: 
1407 ##                dbg('no list to auto complete; ignoring "autoSelect=True"') 
1408                 self
._autoSelect 
= False 
1410         # reset field validity assumption: 
1412 ##        dbg(indent=0, suspend=0) 
1415     def _GetParameter(self
, paramname
): 
1417         Routine for retrieving the value of any given parameter 
1419         if Field
.valid_params
.has_key(paramname
): 
1420             return getattr(self
, '_' + paramname
) 
1422             TypeError('Field._GetParameter: invalid parameter "%s"' % key
) 
1425     def IsEmpty(self
, slice): 
1427         Indicates whether the specified slice is considered empty for the 
1430 ##        dbg('Field::IsEmpty("%s")' % slice, indent=1) 
1431         if not hasattr(self
, '_template'): 
1433             raise AttributeError('_template') 
1435 ##        dbg('self._template: "%s"' % self._template) 
1436 ##        dbg('self._defaultValue: "%s"' % str(self._defaultValue)) 
1437         if slice == self
._template 
and not self
._defaultValue
: 
1441         elif slice == self
._template
: 
1443             for pos 
in range(len(self
._template
)): 
1444 ####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) 
1445                 if slice[pos
] not in (' ', self
._fillChar
): 
1448 ##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) 
1451 ##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) 
1455     def IsValid(self
, slice): 
1457         Indicates whether the specified slice is considered a valid value for the 
1461 ##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) 
1462         valid 
= True    # assume true to start 
1464         if self
.IsEmpty(slice): 
1465 ##            dbg(indent=0, suspend=0) 
1466             if self
._emptyInvalid
: 
1471         elif self
._hasList 
and self
._choiceRequired
: 
1472 ##            dbg("(member of list required)") 
1473             # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): 
1474             if self
._fillChar 
!= ' ': 
1475                 slice = slice.replace(self
._fillChar
, ' ') 
1476 ##                dbg('updated slice:"%s"' % slice) 
1477             compareStr 
= slice.strip() 
1479             if self
._compareNoCase
: 
1480                 compareStr 
= compareStr
.lower() 
1481             valid 
= compareStr 
in self
._compareChoices
 
1483         elif self
._hasRange 
and not self
.IsEmpty(slice): 
1484 ##            dbg('validating against range') 
1486                 # allow float as well as int ranges (int comparisons for free.) 
1487                 valid 
= self
._rangeLow 
<= float(slice) <= self
._rangeHigh
 
1491         elif self
._validRegex 
and self
._filter
: 
1492 ##            dbg('validating against regex') 
1493             valid 
= (re
.match( self
._filter
, slice) is not None) 
1495         if valid 
and self
._validFunc
: 
1496 ##            dbg('validating against supplied function') 
1497             valid 
= self
._validFunc
(slice) 
1498 ##        dbg('valid?', valid, indent=0, suspend=0) 
1502     def _AdjustField(self
, slice): 
1503         """ 'Fixes' an integer field. Right or left-justifies, as required.""" 
1504 ##        dbg('Field::_AdjustField("%s")' % slice, indent=1) 
1505         length 
= len(self
._mask
) 
1506 ####        dbg('length(self._mask):', length) 
1507 ####        dbg('self._useParensForNegatives?', self._useParensForNegatives) 
1509             if self
._useParensForNegatives
: 
1510                 signpos 
= slice.find('(') 
1511                 right_signpos 
= slice.find(')') 
1512                 intStr 
= slice.replace('(', '').replace(')', '')    # drop sign, if any 
1514                 signpos 
= slice.find('-') 
1515                 intStr 
= slice.replace( '-', '' )                   # drop sign, if any 
1518             intStr 
= intStr
.replace(' ', '')                        # drop extra spaces 
1519             intStr 
= string
.replace(intStr
,self
._fillChar
,"")       # drop extra fillchars 
1520             intStr 
= string
.replace(intStr
,"-","")                  # drop sign, if any 
1521             intStr 
= string
.replace(intStr
, self
._groupChar
, "")    # lose commas/dots 
1522 ####            dbg('intStr:"%s"' % intStr) 
1523             start
, end 
= self
._extent
 
1524             field_len 
= end 
- start
 
1525             if not self
._padZero 
and len(intStr
) != field_len 
and intStr
.strip(): 
1526                 intStr 
= str(long(intStr
)) 
1527 ####            dbg('raw int str: "%s"' % intStr) 
1528 ####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) 
1529             if self
._groupdigits
: 
1532                 for i 
in range(len(intStr
)-1, -1, -1): 
1533                     new 
= intStr
[i
] + new
 
1535                         new 
= self
._groupChar 
+ new
 
1537                 if new 
and new
[0] == self
._groupChar
: 
1539                 if len(new
) <= length
: 
1540                     # expanded string will still fit and leave room for sign: 
1542                 # else... leave it without the commas... 
1544 ##            dbg('padzero?', self._padZero) 
1545 ##            dbg('len(intStr):', len(intStr), 'field length:', length) 
1546             if self
._padZero 
and len(intStr
) < length
: 
1547                 intStr 
= '0' * (length 
- len(intStr
)) + intStr
 
1548                 if signpos 
!= -1:   # we had a sign before; restore it 
1549                     if self
._useParensForNegatives
: 
1550                         intStr 
= '(' + intStr
[1:] 
1551                         if right_signpos 
!= -1: 
1554                         intStr 
= '-' + intStr
[1:] 
1555             elif signpos 
!= -1 and slice[0:signpos
].strip() == '':    # - was before digits 
1556                 if self
._useParensForNegatives
: 
1557                     intStr 
= '(' + intStr
 
1558                     if right_signpos 
!= -1: 
1561                     intStr 
= '-' + intStr
 
1562             elif right_signpos 
!= -1: 
1563                 # must have had ')' but '(' was before field; re-add ')' 
1567         slice = slice.strip() # drop extra spaces 
1569         if self
._alignRight
:     ## Only if right-alignment is enabled 
1570             slice = slice.rjust( length 
) 
1572             slice = slice.ljust( length 
) 
1573         if self
._fillChar 
!= ' ': 
1574             slice = slice.replace(' ', self
._fillChar
) 
1575 ##        dbg('adjusted slice: "%s"' % slice, indent=0) 
1579 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
1581 class MaskedEditMixin
: 
1583     This class allows us to abstract the masked edit functionality that could 
1584     be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.) 
1586     valid_ctrl_params 
= { 
1587               'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control 
1588               'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary 
1589               'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask 
1590               'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year 
1591               'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete 
1592               'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control 
1593               'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently, 
1594                                                 ## from field validation constraints 
1595               'emptyBackgroundColour': "White", 
1596               'validBackgroundColour': "White", 
1597               'invalidBackgroundColour': "Yellow", 
1598               'foregroundColour': "Black", 
1599               'signedForegroundColour': "Red", 
1603     def __init__(self
, name 
= 'MaskedEdit', **kwargs
): 
1605         This is the "constructor" for setting up the mixin variable parameters for the composite class. 
1610         # set up flag for doing optional things to base control if possible 
1611         if not hasattr(self
, 'controlInitialized'): 
1612             self
.controlInitialized 
= False 
1614         # Set internal state var for keeping track of whether or not a character 
1615         # action results in a modification of the control, since .SetValue() 
1616         # doesn't modify the base control's internal state: 
1617         self
.modified 
= False 
1618         self
._previous
_mask 
= None 
1620         # Validate legitimate set of parameters: 
1621         for key 
in kwargs
.keys(): 
1622             if key
.replace('Color', 'Colour') not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1623                 raise TypeError('%s: invalid parameter "%s"' % (name
, key
)) 
1625         ## Set up dictionary that can be used by subclasses to override or add to default 
1626         ## behavior for individual characters.  Derived subclasses needing to change 
1627         ## default behavior for keys can either redefine the default functions for the 
1628         ## common keys or add functions for specific keys to this list.  Each function 
1629         ## added should take the key event as argument, and return False if the key 
1630         ## requires no further processing. 
1632         ## Initially populated with navigation and function control keys: 
1633         self
._keyhandlers 
= { 
1634             # default navigation keys and handlers: 
1635             wx
.WXK_BACK
:   self
._OnErase
, 
1636             wx
.WXK_LEFT
:   self
._OnArrow
, 
1637             wx
.WXK_RIGHT
:  self
._OnArrow
, 
1638             wx
.WXK_UP
:     self
._OnAutoCompleteField
, 
1639             wx
.WXK_DOWN
:   self
._OnAutoCompleteField
, 
1640             wx
.WXK_TAB
:    self
._OnChangeField
, 
1641             wx
.WXK_HOME
:   self
._OnHome
, 
1642             wx
.WXK_END
:    self
._OnEnd
, 
1643             wx
.WXK_RETURN
: self
._OnReturn
, 
1644             wx
.WXK_PRIOR
:  self
._OnAutoCompleteField
, 
1645             wx
.WXK_NEXT
:   self
._OnAutoCompleteField
, 
1647             # default function control keys and handlers: 
1648             wx
.WXK_DELETE
: self
._OnErase
, 
1649             WXK_CTRL_A
: self
._OnCtrl
_A
, 
1650             WXK_CTRL_C
: self
._OnCtrl
_C
, 
1651             WXK_CTRL_S
: self
._OnCtrl
_S
, 
1652             WXK_CTRL_V
: self
._OnCtrl
_V
, 
1653             WXK_CTRL_X
: self
._OnCtrl
_X
, 
1654             WXK_CTRL_Z
: self
._OnCtrl
_Z
, 
1657         ## bind standard navigational and control keycodes to this instance, 
1658         ## so that they can be augmented and/or changed in derived classes: 
1659         self
._nav 
= list(nav
) 
1660         self
._control 
= list(control
) 
1662         ## Dynamically evaluate and store string constants for mask chars 
1663         ## so that locale settings can be made after this module is imported 
1664         ## and the controls created after that is done can allow the 
1665         ## appropriate characters: 
1666         self
.maskchardict  
= { 
1668             'A': string
.uppercase
, 
1669             'a': string
.lowercase
, 
1670             'X': string
.letters 
+ string
.punctuation 
+ string
.digits
, 
1671             'C': string
.letters
, 
1672             'N': string
.letters 
+ string
.digits
, 
1673             '&': string
.punctuation
 
1676         ## self._ignoreChange is used by MaskedComboBox, because 
1677         ## of the hack necessary to determine the selection; it causes 
1678         ## EVT_TEXT messages from the combobox to be ignored if set. 
1679         self
._ignoreChange 
= False 
1681         # These are used to keep track of previous value, for undo functionality: 
1682         self
._curValue  
= None 
1683         self
._prevValue 
= None 
1687         # Set defaults for each parameter for this instance, and fully 
1688         # populate initial parameter list for configuration: 
1689         for key
, value 
in MaskedEditMixin
.valid_ctrl_params
.items(): 
1690             setattr(self
, '_' + key
, copy
.copy(value
)) 
1691             if not kwargs
.has_key(key
): 
1692 ####                dbg('%s: "%s"' % (key, repr(value))) 
1693                 kwargs
[key
] = copy
.copy(value
) 
1695         # Create a "field" that holds global parameters for control constraints 
1696         self
._ctrl
_constraints 
= self
._fields
[-1] = Field(index
=-1) 
1697         self
.SetCtrlParameters(**kwargs
) 
1701     def SetCtrlParameters(self
, **kwargs
): 
1703         This public function can be used to set individual or multiple masked edit 
1704         parameters after construction. 
1707 ##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1) 
1708 ####        dbg('kwargs:', indent=1) 
1709 ##        for key, value in kwargs.items(): 
1710 ####            dbg(key, '=', value) 
1713         # Validate keyword arguments: 
1714         constraint_kwargs 
= {} 
1716         for key
, value 
in kwargs
.items(): 
1717             key 
= key
.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling 
1718             if key 
not in MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys(): 
1719 ##                dbg(indent=0, suspend=0) 
1720                 raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key
, self
.name
)) 
1721             elif key 
in Field
.valid_params
.keys(): 
1722                 constraint_kwargs
[key
] = value
 
1724                 ctrl_kwargs
[key
] = value
 
1729         if ctrl_kwargs
.has_key('autoformat'): 
1730             autoformat 
= ctrl_kwargs
['autoformat'] 
1734         # handle "parochial name" backward compatibility: 
1735         if autoformat 
and autoformat
.find('MILTIME') != -1 and autoformat 
not in masktags
.keys(): 
1736             autoformat 
= autoformat
.replace('MILTIME', '24HRTIME') 
1738         if autoformat 
!= self
._autoformat 
and autoformat 
in masktags
.keys(): 
1739 ##            dbg('autoformat:', autoformat) 
1740             self
._autoformat                  
= autoformat
 
1741             mask                              
= masktags
[self
._autoformat
]['mask'] 
1742             # gather rest of any autoformat parameters: 
1743             for param
, value 
in masktags
[self
._autoformat
].items(): 
1744                 if param 
== 'mask': continue    # (must be present; already accounted for) 
1745                 constraint_kwargs
[param
] = value
 
1747         elif autoformat 
and not autoformat 
in masktags
.keys(): 
1748             raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat
)) 
1750 ##            dbg('autoformat not selected') 
1751             if kwargs
.has_key('mask'): 
1752                 mask 
= kwargs
['mask'] 
1753 ##                dbg('mask:', mask) 
1755         ## Assign style flags 
1757 ##            dbg('preserving previous mask') 
1758             mask 
= self
._previous
_mask   
# preserve previous mask 
1760 ##            dbg('mask (re)set') 
1761             reset_args
['reset_mask'] = mask
 
1762             constraint_kwargs
['mask'] = mask
 
1764             # wipe out previous fields; preserve new control-level constraints 
1765             self
._fields 
= {-1: self._ctrl_constraints}
 
1768         if ctrl_kwargs
.has_key('fields'): 
1769             # do field parameter type validation, and conversion to internal dictionary 
1771             fields 
= ctrl_kwargs
['fields'] 
1772             if type(fields
) in (types
.ListType
, types
.TupleType
): 
1773                 for i 
in range(len(fields
)): 
1775                     if not isinstance(field
, Field
): 
1776 ##                        dbg(indent=0, suspend=0) 
1777                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1778                     self
._fields
[i
] = field
 
1780             elif type(fields
) == types
.DictionaryType
: 
1781                 for index
, field 
in fields
.items(): 
1782                     if not isinstance(field
, Field
): 
1783 ##                        dbg(indent=0, suspend=0) 
1784                         raise AttributeError('invalid type for field parameter: %s' % repr(field
)) 
1785                     self
._fields
[index
] = field
 
1787 ##                dbg(indent=0, suspend=0) 
1788                 raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields
)) 
1790         # Assign constraint parameters for entire control: 
1791 ####        dbg('control constraints:', indent=1) 
1792 ##        for key, value in constraint_kwargs.items(): 
1793 ####            dbg('%s:' % key, value) 
1796         # determine if changing parameters that should affect the entire control: 
1797         for key 
in MaskedEditMixin
.valid_ctrl_params
.keys(): 
1798             if key 
in ( 'mask', 'fields' ): continue    # (processed separately) 
1799             if ctrl_kwargs
.has_key(key
): 
1800                 setattr(self
, '_' + key
, ctrl_kwargs
[key
]) 
1802         # Validate color parameters, converting strings to named colors and validating 
1803         # result if appropriate: 
1804         for key 
in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', 
1805                     'foregroundColour', 'signedForegroundColour'): 
1806             if ctrl_kwargs
.has_key(key
): 
1807                 if type(ctrl_kwargs
[key
]) in (types
.StringType
, types
.UnicodeType
): 
1808                     c 
= wx
.NamedColour(ctrl_kwargs
[key
]) 
1809                     if c
.Get() == (-1, -1, -1): 
1810                         raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1812                         # replace attribute with wxColour object: 
1813                         setattr(self
, '_' + key
, c
) 
1814                         # attach a python dynamic attribute to wxColour for debug printouts 
1815                         c
._name 
= ctrl_kwargs
[key
] 
1817                 elif type(ctrl_kwargs
[key
]) != type(wx
.BLACK
): 
1818                     raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs
[key
]), key
)) 
1821 ##        dbg('self._retainFieldValidation:', self._retainFieldValidation) 
1822         if not self
._retainFieldValidation
: 
1823             # Build dictionary of any changing parameters which should be propagated to the 
1825             for arg 
in Field
.propagating_params
: 
1826 ####                dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) 
1827 ####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) 
1828                 reset_args
[arg
] = kwargs
.has_key(arg
) and kwargs
[arg
] != getattr(self
._ctrl
_constraints
, '_'+arg
) 
1829 ####                dbg('reset_args[%s]?' % arg, reset_args[arg]) 
1831         # Set the control-level constraints: 
1832         self
._ctrl
_constraints
._SetParameters
(**constraint_kwargs
) 
1834         # This routine does the bulk of the interdependent parameter processing, determining 
1835         # the field extents of the mask if changed, resetting parameters as appropriate, 
1836         # determining the overall template value for the control, etc. 
1837         self
._configure
(mask
, **reset_args
) 
1839         # now that we've propagated the field constraints and mask portions to the 
1840         # various fields, validate the constraints 
1841         self
._ctrl
_constraints
._ValidateParameters
(**constraint_kwargs
) 
1843         # Validate that all choices for given fields are at least of the 
1844         # necessary length, and that they all would be valid pastes if pasted 
1845         # into their respective fields: 
1846 ####        dbg('validating choices') 
1847         self
._validateChoices
() 
1850         self
._autofit 
= self
._ctrl
_constraints
._autofit
 
1853         self
._isDate     
= 'D' in self
._ctrl
_constraints
._formatcodes 
and isDateType(mask
) 
1854         self
._isTime     
= 'T' in self
._ctrl
_constraints
._formatcodes 
and isTimeType(mask
) 
1856             # Set _dateExtent, used in date validation to locate date in string; 
1857             # always set as though year will be 4 digits, even if mask only has 
1858             # 2 digits, so we can always properly process the intended year for 
1859             # date validation (leap years, etc.) 
1860             if self
._mask
.find('CCC') != -1: self
._dateExtent 
= 11 
1861             else:                            self
._dateExtent 
= 10 
1863             self
._4digityear 
= len(self
._mask
) > 8 and self
._mask
[9] == '#' 
1865         if self
._isDate 
and self
._autoformat
: 
1866             # Auto-decide datestyle: 
1867             if self
._autoformat
.find('MDDY')    != -1: self
._datestyle 
= 'MDY' 
1868             elif self
._autoformat
.find('YMMD')  != -1: self
._datestyle 
= 'YMD' 
1869             elif self
._autoformat
.find('YMMMD') != -1: self
._datestyle 
= 'YMD' 
1870             elif self
._autoformat
.find('DMMY')  != -1: self
._datestyle 
= 'DMY' 
1871             elif self
._autoformat
.find('DMMMY') != -1: self
._datestyle 
= 'DMY' 
1873         # Give derived controls a chance to react to parameter changes before 
1874         # potentially changing current value of the control. 
1875         self
._OnCtrlParametersChanged
() 
1877         if self
.controlInitialized
: 
1878             # Then the base control is available for configuration; 
1879             # take action on base control based on new settings, as appropriate. 
1880             if kwargs
.has_key('useFixedWidthFont'): 
1881                 # Set control font - fixed width by default 
1884             if reset_args
.has_key('reset_mask'): 
1885 ##                dbg('reset mask') 
1886                 curvalue 
= self
._GetValue
() 
1887                 if curvalue
.strip(): 
1889 ##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) 
1890                         self
._SetInitialValue
(self
._GetValue
()) 
1891                     except Exception, e
: 
1892 ##                        dbg('exception caught:', e) 
1893 ##                        dbg("current value doesn't work; attempting to reset to template") 
1894                         self
._SetInitialValue
() 
1896 ##                    dbg('attempting to _SetInitialValue() with template') 
1897                     self
._SetInitialValue
() 
1899             elif kwargs
.has_key('useParensForNegatives'): 
1900                 newvalue 
= self
._getSignedValue
()[0] 
1902                 if newvalue 
is not None: 
1903                     # Adjust for new mask: 
1904                     if len(newvalue
) < len(self
._mask
): 
1906                     elif len(newvalue
) > len(self
._mask
): 
1907                         if newvalue
[-1] in (' ', ')'): 
1908                             newvalue 
= newvalue
[:-1] 
1910 ##                    dbg('reconfiguring value for parens:"%s"' % newvalue) 
1911                     self
._SetValue
(newvalue
) 
1913                     if self
._prevValue 
!= newvalue
: 
1914                         self
._prevValue 
= newvalue  
# disallow undo of sign type 
1917 ##                dbg('setting client size to:', self._CalcSize()) 
1918                 size 
= self
._CalcSize
() 
1919                 self
.SetSizeHints(size
) 
1920                 self
.SetClientSize(size
) 
1922             # Set value/type-specific formatting 
1923             self
._applyFormatting
() 
1924 ##        dbg(indent=0, suspend=0) 
1926     def SetMaskParameters(self
, **kwargs
): 
1927         """ old name for this function """ 
1928         return self
.SetCtrlParameters(**kwargs
) 
1931     def GetCtrlParameter(self
, paramname
): 
1933         Routine for retrieving the value of any given parameter 
1935         if MaskedEditMixin
.valid_ctrl_params
.has_key(paramname
.replace('Color','Colour')): 
1936             return getattr(self
, '_' + paramname
.replace('Color', 'Colour')) 
1937         elif Field
.valid_params
.has_key(paramname
): 
1938             return self
._ctrl
_constraints
._GetParameter
(paramname
) 
1940             TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
1942     def GetMaskParameter(self
, paramname
): 
1943         """ old name for this function """ 
1944         return self
.GetCtrlParameter(paramname
) 
1947 ## This idea worked, but Boa was unable to use this solution... 
1948 ##    def _attachMethod(self, func): 
1950 ##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) 
1953 ##    def _DefinePropertyFunctions(exposed_params): 
1954 ##        for param in exposed_params: 
1955 ##            propname = param[0].upper() + param[1:] 
1957 ##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
1958 ##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
1959 ##            self._attachMethod(locals()['Set%s' % propname]) 
1960 ##            self._attachMethod(locals()['Get%s' % propname]) 
1962 ##            if param.find('Colour') != -1: 
1963 ##                # add non-british spellings, for backward-compatibility 
1964 ##                propname.replace('Colour', 'Color') 
1966 ##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) 
1967 ##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
1968 ##                self._attachMethod(locals()['Set%s' % propname]) 
1969 ##                self._attachMethod(locals()['Get%s' % propname]) 
1973     def SetFieldParameters(self
, field_index
, **kwargs
): 
1975         Routine provided to modify the parameters of a given field. 
1976         Because changes to fields can affect the overall control, 
1977         direct access to the fields is prevented, and the control 
1978         is always "reconfigured" after setting a field parameter. 
1980         if field_index 
not in self
._field
_indices
: 
1981             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
1982         # set parameters as requested: 
1983         self
._fields
[field_index
]._SetParameters
(**kwargs
) 
1985         # Possibly reprogram control template due to resulting changes, and ensure 
1986         # control-level params are still propagated to fields: 
1987         self
._configure
(self
._previous
_mask
) 
1988         self
._fields
[field_index
]._ValidateParameters
(**kwargs
) 
1990         if self
.controlInitialized
: 
1991             if kwargs
.has_key('fillChar') or kwargs
.has_key('defaultValue'): 
1992                 self
._SetInitialValue
() 
1995                     size 
= self
._CalcSize
() 
1996                     self
.SetSizeHints(size
) 
1997                     self
.SetClientSize(size
) 
1999             # Set value/type-specific formatting 
2000             self
._applyFormatting
() 
2003     def GetFieldParameter(self
, field_index
, paramname
): 
2005         Routine provided for getting a parameter of an individual field. 
2007         if field_index 
not in self
._field
_indices
: 
2008             raise IndexError('%s is not a valid field for control "%s".' % (str(field_index
), self
.name
)) 
2009         elif Field
.valid_params
.has_key(paramname
): 
2010             return self
._fields
[field_index
]._GetParameter
(paramname
) 
2012             TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self
.name
, paramname
)) 
2015     def _SetKeycodeHandler(self
, keycode
, func
): 
2017         This function adds and/or replaces key event handling functions 
2018         used by the control.  <func> should take the event as argument 
2019         and return False if no further action on the key is necessary. 
2021         self
._keyhandlers
[keycode
] = func
 
2024     def _SetKeyHandler(self
, char
, func
): 
2026         This function adds and/or replaces key event handling functions 
2027         for ascii characters.  <func> should take the event as argument 
2028         and return False if no further action on the key is necessary. 
2030         self
._SetKeycodeHandler
(ord(char
), func
) 
2033     def _AddNavKeycode(self
, keycode
, handler
=None): 
2035         This function allows a derived subclass to augment the list of 
2036         keycodes that are considered "navigational" keys. 
2038         self
._nav
.append(keycode
) 
2040             self
._keyhandlers
[keycode
] = handler
 
2043     def _AddNavKey(self
, char
, handler
=None): 
2045         This function is a convenience function so you don't have to 
2046         remember to call ord() for ascii chars to be used for navigation. 
2048         self
._AddNavKeycode
(ord(char
), handler
) 
2051     def _GetNavKeycodes(self
): 
2053         This function retrieves the current list of navigational keycodes for 
2059     def _SetNavKeycodes(self
, keycode_func_tuples
): 
2061         This function allows you to replace the current list of keycode processed 
2062         as navigation keys, and bind associated optional keyhandlers. 
2065         for keycode
, func 
in keycode_func_tuples
: 
2066             self
._nav
.append(keycode
) 
2068                 self
._keyhandlers
[keycode
] = func
 
2071     def _processMask(self
, mask
): 
2073         This subroutine expands {n} syntax in mask strings, and looks for escaped 
2074         special characters and returns the expanded mask, and an dictionary 
2075         of booleans indicating whether or not a given position in the mask is 
2076         a mask character or not. 
2078 ##        dbg('_processMask: mask', mask, indent=1) 
2079         # regular expression for parsing c{n} syntax: 
2080         rex 
= re
.compile('([' +string
.join(maskchars
,"") + '])\{(\d+)\}') 
2082         match 
= rex
.search(s
) 
2083         while match
:    # found an(other) occurrence 
2084             maskchr 
= s
[match
.start(1):match
.end(1)]            # char to be repeated 
2085             repcount 
= int(s
[match
.start(2):match
.end(2)])      # the number of times 
2086             replacement 
= string
.join( maskchr 
* repcount
, "")  # the resulting substr 
2087             s 
= s
[:match
.start(1)] + replacement 
+ s
[match
.end(2)+1:]   #account for trailing '}' 
2088             match 
= rex
.search(s
)                               # look for another such entry in mask 
2090         self
._decimalChar 
= self
._ctrl
_constraints
._decimalChar
 
2091         self
._shiftDecimalChar 
= self
._ctrl
_constraints
._shiftDecimalChar
 
2093         self
._isFloat      
= isFloatingPoint(s
) and not self
._ctrl
_constraints
._validRegex
 
2094         self
._isInt      
= isInteger(s
) and not self
._ctrl
_constraints
._validRegex
 
2095         self
._signOk     
= '-' in self
._ctrl
_constraints
._formatcodes 
and (self
._isFloat 
or self
._isInt
) 
2096         self
._useParens  
= self
._ctrl
_constraints
._useParensForNegatives
 
2098 ####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) 
2099 ####        dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s), 
2100 ##            'ctrl regex:', self._ctrl_constraints._validRegex) 
2102         if self
._signOk 
and s
[0] != ' ': 
2104             if self
._ctrl
_constraints
._defaultValue 
and self
._ctrl
_constraints
._defaultValue
[0] != ' ': 
2105                 self
._ctrl
_constraints
._defaultValue 
= ' ' + self
._ctrl
_constraints
._defaultValue
 
2110                 self
._ctrl
_constraints
._defaultValue 
+= ' ' 
2112         # Now, go build up a dictionary of booleans, indexed by position, 
2113         # indicating whether or not a given position is masked or not 
2117             if s
[i
] == '\\':            # if escaped character: 
2118                 ismasked
[i
] = False     #     mark position as not a mask char 
2119                 if i
+1 < len(s
):        #     if another char follows... 
2120                     s 
= s
[:i
] + s
[i
+1:] #         elide the '\' 
2121                     if i
+2 < len(s
) and s
[i
+1] == '\\': 
2122                         # if next char also a '\', char is a literal '\' 
2123                         s 
= s
[:i
] + s
[i
+1:]     # elide the 2nd '\' as well 
2124             else:                       # else if special char, mark position accordingly 
2125                 ismasked
[i
] = s
[i
] in maskchars
 
2126 ####            dbg('ismasked[%d]:' % i, ismasked[i], s) 
2127             i 
+= 1                      # increment to next char 
2128 ####        dbg('ismasked:', ismasked) 
2129 ##        dbg('new mask: "%s"' % s, indent=0) 
2134     def _calcFieldExtents(self
): 
2136         Subroutine responsible for establishing/configuring field instances with 
2137         indices and editable extents appropriate to the specified mask, and building 
2138         the lookup table mapping each position to the corresponding field. 
2140         self
._lookupField 
= {} 
2143             ## Create dictionary of positions,characters in mask 
2145             for charnum 
in range( len( self
._mask
)): 
2146                 self
.maskdict
[charnum
] = self
._mask
[charnum
:charnum
+1] 
2148             # For the current mask, create an ordered list of field extents 
2149             # and a dictionary of positions that map to field indices: 
2151             if self
._signOk
: start 
= 1 
2155                 # Skip field "discovery", and just construct a 2-field control with appropriate 
2156                 # constraints for a floating-point entry. 
2158                 # .setdefault always constructs 2nd argument even if not needed, so we do this 
2159                 # the old-fashioned way... 
2160                 if not self
._fields
.has_key(0): 
2161                     self
._fields
[0] = Field() 
2162                 if not self
._fields
.has_key(1): 
2163                     self
._fields
[1] = Field() 
2165                 self
._decimalpos 
= string
.find( self
._mask
, '.') 
2166 ##                dbg('decimal pos =', self._decimalpos) 
2168                 formatcodes 
= self
._fields
[0]._GetParameter
('formatcodes') 
2169                 if 'R' not in formatcodes
: formatcodes 
+= 'R' 
2170                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, self
._decimalpos
), 
2171                                                mask
=self
._mask
[start
:self
._decimalpos
], formatcodes
=formatcodes
) 
2172                 end 
= len(self
._mask
) 
2173                 if self
._signOk 
and self
._useParens
: 
2175                 self
._fields
[1]._SetParameters
(index
=1, extent
=(self
._decimalpos
+1, end
), 
2176                                                mask
=self
._mask
[self
._decimalpos
+1:end
]) 
2178                 for i 
in range(self
._decimalpos
+1): 
2179                     self
._lookupField
[i
] = 0 
2181                 for i 
in range(self
._decimalpos
+1, len(self
._mask
)+1): 
2182                     self
._lookupField
[i
] = 1 
2185                 # Skip field "discovery", and just construct a 1-field control with appropriate 
2186                 # constraints for a integer entry. 
2187                 if not self
._fields
.has_key(0): 
2188                     self
._fields
[0] = Field(index
=0) 
2189                 end 
= len(self
._mask
) 
2190                 if self
._signOk 
and self
._useParens
: 
2192                 self
._fields
[0]._SetParameters
(index
=0, extent
=(start
, end
), 
2193                                                mask
=self
._mask
[start
:end
]) 
2194                 for i 
in range(len(self
._mask
)+1): 
2195                     self
._lookupField
[i
] = 0 
2197                 # generic control; parse mask to figure out where the fields are: 
2200                 i 
= self
._findNextEntry
(pos
,adjustInsert
=False)  # go to 1st entry point: 
2201                 if i 
< len(self
._mask
):   # no editable chars! 
2202                     for j 
in range(pos
, i
+1): 
2203                         self
._lookupField
[j
] = field_index
 
2204                     pos 
= i       
# figure out field for 1st editable space: 
2206                 while i 
<= len(self
._mask
): 
2207 ####                    dbg('searching: outer field loop: i = ', i) 
2208                     if self
._isMaskChar
(i
): 
2209 ####                        dbg('1st char is mask char; recording edit_start=', i) 
2211                         # Skip to end of editable part of current field: 
2212                         while i 
< len(self
._mask
) and self
._isMaskChar
(i
): 
2213                             self
._lookupField
[i
] = field_index
 
2215 ####                        dbg('edit_end =', i) 
2217                         self
._lookupField
[i
] = field_index
 
2218 ####                        dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) 
2219                         if not self
._fields
.has_key(field_index
): 
2220                             kwargs 
= Field
.valid_params
.copy() 
2221                             kwargs
['index'] = field_index
 
2222                             kwargs
['extent'] = (edit_start
, edit_end
) 
2223                             kwargs
['mask'] = self
._mask
[edit_start
:edit_end
] 
2224                             self
._fields
[field_index
] = Field(**kwargs
) 
2226                             self
._fields
[field_index
]._SetParameters
( 
2228                                                                 extent
=(edit_start
, edit_end
), 
2229                                                                 mask
=self
._mask
[edit_start
:edit_end
]) 
2231                     i 
= self
._findNextEntry
(pos
, adjustInsert
=False)  # go to next field: 
2233                         for j 
in range(pos
, i
+1): 
2234                             self
._lookupField
[j
] = field_index
 
2235                     if i 
>= len(self
._mask
): 
2236                         break           # if past end, we're done 
2239 ####                        dbg('next field:', field_index) 
2241         indices 
= self
._fields
.keys() 
2243         self
._field
_indices 
= indices
[1:] 
2244 ####        dbg('lookupField map:', indent=1) 
2245 ##        for i in range(len(self._mask)): 
2246 ####            dbg('pos %d:' % i, self._lookupField[i]) 
2249         # Verify that all field indices specified are valid for mask: 
2250         for index 
in self
._fields
.keys(): 
2251             if index 
not in [-1] + self
._lookupField
.values(): 
2252                 raise IndexError('field %d is not a valid field for mask "%s"' % (index
, self
._mask
)) 
2255     def _calcTemplate(self
, reset_fillchar
, reset_default
): 
2257         Subroutine for processing current fillchars and default values for 
2258         whole control and individual fields, constructing the resulting 
2259         overall template, and adjusting the current value as necessary. 
2262         if self
._ctrl
_constraints
._defaultValue
: 
2265             for field 
in self
._fields
.values(): 
2266                 if field
._defaultValue 
and not reset_default
: 
2268 ##        dbg('default set?', default_set) 
2270         # Determine overall new template for control, and keep track of previous 
2271         # values, so that current control value can be modified as appropriate: 
2272         if self
.controlInitialized
: curvalue 
= list(self
._GetValue
()) 
2273         else:                       curvalue 
= None 
2275         if hasattr(self
, '_fillChar'): old_fillchars 
= self
._fillChar
 
2276         else:                          old_fillchars 
= None 
2278         if hasattr(self
, '_template'): old_template 
= self
._template
 
2279         else:                          old_template 
= None 
2286         for field 
in self
._fields
.values(): 
2287             field
._template 
= "" 
2289         for pos 
in range(len(self
._mask
)): 
2290 ####            dbg('pos:', pos) 
2291             field 
= self
._FindField
(pos
) 
2292 ####            dbg('field:', field._index) 
2293             start
, end 
= field
._extent
 
2295             if pos 
== 0 and self
._signOk
: 
2296                 self
._template 
= ' ' # always make 1st 1st position blank, regardless of fillchar 
2297             elif self
._isFloat 
and pos 
== self
._decimalpos
: 
2298                 self
._template 
+= self
._decimalChar
 
2299             elif self
._isMaskChar
(pos
): 
2300                 if field
._fillChar 
!= self
._ctrl
_constraints
._fillChar 
and not reset_fillchar
: 
2301                     fillChar 
= field
._fillChar
 
2303                     fillChar 
= self
._ctrl
_constraints
._fillChar
 
2304                 self
._fillChar
[pos
] = fillChar
 
2306                 # Replace any current old fillchar with new one in current value; 
2307                 # if action required, set reset_value flag so we can take that action 
2308                 # after we're all done 
2309                 if self
.controlInitialized 
and old_fillchars 
and old_fillchars
.has_key(pos
) and curvalue
: 
2310                     if curvalue
[pos
] == old_fillchars
[pos
] and old_fillchars
[pos
] != fillChar
: 
2312                         curvalue
[pos
] = fillChar
 
2314                 if not field
._defaultValue 
and not self
._ctrl
_constraints
._defaultValue
: 
2315 ####                    dbg('no default value') 
2316                     self
._template 
+= fillChar
 
2317                     field
._template 
+= fillChar
 
2319                 elif field
._defaultValue 
and not reset_default
: 
2320 ####                    dbg('len(field._defaultValue):', len(field._defaultValue)) 
2321 ####                    dbg('pos-start:', pos-start) 
2322                     if len(field
._defaultValue
) > pos
-start
: 
2323 ####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) 
2324                         self
._template 
+= field
._defaultValue
[pos
-start
] 
2325                         field
._template 
+= field
._defaultValue
[pos
-start
] 
2327 ####                        dbg('field default not long enough; using fillChar') 
2328                         self
._template 
+= fillChar
 
2329                         field
._template 
+= fillChar
 
2331                     if len(self
._ctrl
_constraints
._defaultValue
) > pos
: 
2332 ####                        dbg('using control default') 
2333                         self
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2334                         field
._template 
+= self
._ctrl
_constraints
._defaultValue
[pos
] 
2336 ####                        dbg('ctrl default not long enough; using fillChar') 
2337                         self
._template 
+= fillChar
 
2338                         field
._template 
+= fillChar
 
2339 ####                dbg('field[%d]._template now "%s"' % (field._index, field._template)) 
2340 ####                dbg('self._template now "%s"' % self._template) 
2342                 self
._template 
+= self
._mask
[pos
] 
2344         self
._fields
[-1]._template 
= self
._template     
# (for consistency) 
2346         if curvalue
:    # had an old value, put new one back together 
2347             newvalue 
= string
.join(curvalue
, "") 
2352             self
._defaultValue 
= self
._template
 
2353 ##            dbg('self._defaultValue:', self._defaultValue) 
2354             if not self
.IsEmpty(self
._defaultValue
) and not self
.IsValid(self
._defaultValue
): 
2356                 raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self
._defaultValue
, self
.name
)) 
2358             # if no fillchar change, but old value == old template, replace it: 
2359             if newvalue 
== old_template
: 
2360                 newvalue 
= self
._template
 
2363             self
._defaultValue 
= None 
2366 ##            dbg('resetting value to: "%s"' % newvalue) 
2367             pos 
= self
._GetInsertionPoint
() 
2368             sel_start
, sel_to 
= self
._GetSelection
() 
2369             self
._SetValue
(newvalue
) 
2370             self
._SetInsertionPoint
(pos
) 
2371             self
._SetSelection
(sel_start
, sel_to
) 
2374     def _propagateConstraints(self
, **reset_args
): 
2376         Subroutine for propagating changes to control-level constraints and 
2377         formatting to the individual fields as appropriate. 
2379         parent_codes 
= self
._ctrl
_constraints
._formatcodes
 
2380         parent_includes 
= self
._ctrl
_constraints
._includeChars
 
2381         parent_excludes 
= self
._ctrl
_constraints
._excludeChars
 
2382         for i 
in self
._field
_indices
: 
2383             field 
= self
._fields
[i
] 
2385             if len(self
._field
_indices
) == 1: 
2386                 inherit_args
['formatcodes'] = parent_codes
 
2387                 inherit_args
['includeChars'] = parent_includes
 
2388                 inherit_args
['excludeChars'] = parent_excludes
 
2390                 field_codes 
= current_codes 
= field
._GetParameter
('formatcodes') 
2391                 for c 
in parent_codes
: 
2392                     if c 
not in field_codes
: field_codes 
+= c
 
2393                 if field_codes 
!= current_codes
: 
2394                     inherit_args
['formatcodes'] = field_codes
 
2396                 include_chars 
= current_includes 
= field
._GetParameter
('includeChars') 
2397                 for c 
in parent_includes
: 
2398                     if not c 
in include_chars
: include_chars 
+= c
 
2399                 if include_chars 
!= current_includes
: 
2400                     inherit_args
['includeChars'] = include_chars
 
2402                 exclude_chars 
= current_excludes 
= field
._GetParameter
('excludeChars') 
2403                 for c 
in parent_excludes
: 
2404                     if not c 
in exclude_chars
: exclude_chars 
+= c
 
2405                 if exclude_chars 
!= current_excludes
: 
2406                     inherit_args
['excludeChars'] = exclude_chars
 
2408             if reset_args
.has_key('defaultValue') and reset_args
['defaultValue']: 
2409                 inherit_args
['defaultValue'] = ""   # (reset for field) 
2411             for param 
in Field
.propagating_params
: 
2412 ####                dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) 
2413 ####                dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) 
2414                 if reset_args
.has_key(param
): 
2415                     inherit_args
[param
] = self
.GetCtrlParameter(param
) 
2416 ####                    dbg('inherit_args[%s]' % param, inherit_args[param]) 
2419                 field
._SetParameters
(**inherit_args
) 
2420                 field
._ValidateParameters
(**inherit_args
) 
2423     def _validateChoices(self
): 
2425         Subroutine that validates that all choices for given fields are at 
2426         least of the necessary length, and that they all would be valid pastes 
2427         if pasted into their respective fields. 
2429         for field 
in self
._fields
.values(): 
2431                 index 
= field
._index
 
2432                 if len(self
._field
_indices
) == 1 and index 
== 0 and field
._choices 
== self
._ctrl
_constraints
._choices
: 
2433 ##                    dbg('skipping (duplicate) choice validation of field 0') 
2435 ####                dbg('checking for choices for field', field._index) 
2436                 start
, end 
= field
._extent
 
2437                 field_length 
= end 
- start
 
2438 ####                dbg('start, end, length:', start, end, field_length) 
2439                 for choice 
in field
._choices
: 
2440 ####                    dbg('testing "%s"' % choice) 
2441                     valid_paste
, ignore
, replace_to 
= self
._validatePaste
(choice
, start
, end
) 
2444                         raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice
, index
, self
.name
)) 
2445                     elif replace_to 
> end
: 
2447                         raise ValueError('"%s" will not fit into field %d of control "%s"' (choice
, index
, self
.name
)) 
2448 ####                    dbg(choice, 'valid in field', index) 
2451     def _configure(self
, mask
, **reset_args
): 
2453         This function sets flags for automatic styling options.  It is 
2454         called whenever a control or field-level parameter is set/changed. 
2456         This routine does the bulk of the interdependent parameter processing, determining 
2457         the field extents of the mask if changed, resetting parameters as appropriate, 
2458         determining the overall template value for the control, etc. 
2460         reset_args is supplied if called from control's .SetCtrlParameters() 
2461         routine, and indicates which if any parameters which can be 
2462         overridden by individual fields have been reset by request for the 
2467 ##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) 
2469         # Preprocess specified mask to expand {n} syntax, handle escaped 
2470         # mask characters, etc and build the resulting positionally keyed 
2471         # dictionary for which positions are mask vs. template characters: 
2472         self
._mask
, self
.ismasked 
= self
._processMask
(mask
) 
2473         self
._masklength 
= len(self
._mask
) 
2474 ####        dbg('processed mask:', self._mask) 
2476         # Preserve original mask specified, for subsequent reprocessing 
2477         # if parameters change. 
2478 ##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) 
2479         self
._previous
_mask 
= mask    
# save unexpanded mask for next time 
2480             # Set expanded mask and extent of field -1 to width of entire control: 
2481         self
._ctrl
_constraints
._SetParameters
(mask 
= self
._mask
, extent
=(0,self
._masklength
)) 
2483         # Go parse mask to determine where each field is, construct field 
2484         # instances as necessary, configure them with those extents, and 
2485         # build lookup table mapping each position for control to its corresponding 
2487 ####        dbg('calculating field extents') 
2489         self
._calcFieldExtents
() 
2492         # Go process defaultValues and fillchars to construct the overall 
2493         # template, and adjust the current value as necessary: 
2494         reset_fillchar 
= reset_args
.has_key('fillChar') and reset_args
['fillChar'] 
2495         reset_default 
= reset_args
.has_key('defaultValue') and reset_args
['defaultValue'] 
2497 ####        dbg('calculating template') 
2498         self
._calcTemplate
(reset_fillchar
, reset_default
) 
2500         # Propagate control-level formatting and character constraints to each 
2501         # field if they don't already have them; if only one field, propagate 
2502         # control-level validation constraints to field as well: 
2503 ####        dbg('propagating constraints') 
2504         self
._propagateConstraints
(**reset_args
) 
2507         if self
._isFloat 
and self
._fields
[0]._groupChar 
== self
._decimalChar
: 
2508             raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % 
2509                                  (self
._fields
[0]._groupChar
, self
._decimalChar
) ) 
2511 ####        dbg('fields:', indent=1) 
2512 ##        for i in [-1] + self._field_indices: 
2513 ####            dbg('field %d:' % i, self._fields[i].__dict__) 
2516         # Set up special parameters for numeric control, if appropriate: 
2518             self
._signpos 
= 0   # assume it starts here, but it will move around on floats 
2519             signkeys 
= ['-', '+', ' '] 
2521                 signkeys 
+= ['(', ')'] 
2522             for key 
in signkeys
: 
2524                 if not self
._keyhandlers
.has_key(keycode
): 
2525                     self
._SetKeyHandler
(key
, self
._OnChangeSign
) 
2529         if self
._isFloat 
or self
._isInt
: 
2530             if self
.controlInitialized
: 
2531                 value 
= self
._GetValue
() 
2532 ####                dbg('value: "%s"' % value, 'len(value):', len(value), 
2533 ##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) 
2534                 if len(value
) < len(self
._ctrl
_constraints
._mask
): 
2536                     if self
._useParens 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find('(') == -1: 
2538                     if self
._signOk 
and len(newvalue
) < len(self
._ctrl
_constraints
._mask
) and newvalue
.find(')') == -1: 
2539                         newvalue 
= ' ' + newvalue
 
2540                     if len(newvalue
) < len(self
._ctrl
_constraints
._mask
): 
2541                         if self
._ctrl
_constraints
._alignRight
: 
2542                             newvalue 
= newvalue
.rjust(len(self
._ctrl
_constraints
._mask
)) 
2544                             newvalue 
= newvalue
.ljust(len(self
._ctrl
_constraints
._mask
)) 
2545 ##                    dbg('old value: "%s"' % value) 
2546 ##                    dbg('new value: "%s"' % newvalue) 
2548                         self
._SetValue
(newvalue
) 
2549                     except Exception, e
: 
2550 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2551                         self
._SetInitialValue
() 
2553                 elif len(value
) > len(self
._ctrl
_constraints
._mask
): 
2555                     if not self
._useParens 
and newvalue
[-1] == ' ': 
2556                         newvalue 
= newvalue
[:-1] 
2557                     if not self
._signOk 
and len(newvalue
) > len(self
._ctrl
_constraints
._mask
): 
2558                         newvalue 
= newvalue
[1:] 
2559                     if not self
._signOk
: 
2560                         newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(newvalue
) 
2562 ##                    dbg('old value: "%s"' % value) 
2563 ##                    dbg('new value: "%s"' % newvalue) 
2565                         self
._SetValue
(newvalue
) 
2566                     except Exception, e
: 
2567 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2568                         self
._SetInitialValue
() 
2569                 elif not self
._signOk 
and ('(' in value 
or '-' in value
): 
2570                     newvalue
, signpos
, right_signpos 
= self
._getSignedValue
(value
) 
2571 ##                    dbg('old value: "%s"' % value) 
2572 ##                    dbg('new value: "%s"' % newvalue) 
2574                         self
._SetValue
(newvalue
) 
2576 ##                        dbg('exception raised:', e, 'resetting to initial value') 
2577                         self
._SetInitialValue
() 
2579             # Replace up/down arrow default handling: 
2580             # make down act like tab, up act like shift-tab: 
2582 ####            dbg('Registering numeric navigation and control handlers (if not already set)') 
2583             if not self
._keyhandlers
.has_key(wx
.WXK_DOWN
): 
2584                 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnChangeField
) 
2585             if not self
._keyhandlers
.has_key(wx
.WXK_UP
): 
2586                 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnUpNumeric
)  # (adds "shift" to up arrow, and calls _OnChangeField) 
2588             # On ., truncate contents right of cursor to decimal point (if any) 
2589             # leaves cusor after decimal point if floating point, otherwise at 0. 
2590             if not self
._keyhandlers
.has_key(ord(self
._decimalChar
)): 
2591                 self
._SetKeyHandler
(self
._decimalChar
, self
._OnDecimalPoint
) 
2592             if not self
._keyhandlers
.has_key(ord(self
._shiftDecimalChar
)): 
2593                 self
._SetKeyHandler
(self
._shiftDecimalChar
, self
._OnChangeField
)   # (Shift-'.' == '>' on US keyboards) 
2595             # Allow selective insert of groupchar in numbers: 
2596             if not self
._keyhandlers
.has_key(ord(self
._fields
[0]._groupChar
)): 
2597                 self
._SetKeyHandler
(self
._fields
[0]._groupChar
, self
._OnGroupChar
) 
2599 ##        dbg(indent=0, suspend=0) 
2602     def _SetInitialValue(self
, value
=""): 
2604         fills the control with the generated or supplied default value. 
2605         It will also set/reset the font if necessary and apply 
2606         formatting to the control at this time. 
2608 ##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) 
2610             self
._prevValue 
= self
._curValue 
= self
._template
 
2611             # don't apply external validation rules in this case, as template may 
2612             # not coincide with "legal" value... 
2614                 self
._SetValue
(self
._curValue
)  # note the use of "raw" ._SetValue()... 
2615             except Exception, e
: 
2616 ##                dbg('exception thrown:', e, indent=0) 
2619             # Otherwise apply validation as appropriate to passed value: 
2620 ####            dbg('value = "%s", length:' % value, len(value)) 
2621             self
._prevValue 
= self
._curValue 
= value
 
2623                 self
.SetValue(value
)            # use public (validating) .SetValue() 
2624             except Exception, e
: 
2625 ##                dbg('exception thrown:', e, indent=0) 
2629         # Set value/type-specific formatting 
2630         self
._applyFormatting
() 
2634     def _calcSize(self
, size
=None): 
2635         """ Calculate automatic size if allowed; must be called after the base control is instantiated""" 
2636 ####        dbg('MaskedEditMixin::_calcSize', indent=1) 
2637         cont 
= (size 
is None or size 
== wx
.DefaultSize
) 
2639         if cont 
and self
._autofit
: 
2640             sizing_text 
= 'M' * self
._masklength
 
2641             if wx
.Platform 
!= "__WXMSW__":   # give it a little extra space 
2643             if wx
.Platform 
== "__WXMAC__":   # give it even a little more... 
2645 ####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) 
2646             w
, h 
= self
.GetTextExtent(sizing_text
) 
2647             size 
= (w
+4, self
.GetClientSize().height
) 
2648 ####            dbg('size:', size, indent=0) 
2653         """ Set the control's font typeface -- pass the font name as str.""" 
2654 ####        dbg('MaskedEditMixin::_setFont', indent=1) 
2655         if not self
._useFixedWidthFont
: 
2656             self
._font 
= wx
.SystemSettings_GetFont(wx
.SYS_DEFAULT_GUI_FONT
) 
2658             font 
= self
.GetFont()   # get size, weight, etc from current font 
2660             # Set to teletype font (guaranteed to be mappable to all wxWindows 
2662             self
._font 
= wx
.Font( font
.GetPointSize(), wx
.TELETYPE
, font
.GetStyle(), 
2663                                  font
.GetWeight(), font
.GetUnderlined()) 
2664 ####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) 
2666         self
.SetFont(self
._font
) 
2670     def _OnTextChange(self
, event
): 
2672         Handler for EVT_TEXT event. 
2673         self._Change() is provided for subclasses, and may return False to 
2674         skip this method logic.  This function returns True if the event 
2675         detected was a legitimate event, or False if it was a "bogus" 
2676         EVT_TEXT event.  (NOTE: There is currently an issue with calling 
2677         .SetValue from within the EVT_CHAR handler that causes duplicate 
2678         EVT_TEXT events for the same change.) 
2680         newvalue 
= self
._GetValue
() 
2681 ##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) 
2683         if self
._ignoreChange
:      # ie. if an "intermediate text change event" 
2687         ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue 
2688         ## call is generating two (2) EVT_TEXT events.  On certain platforms, 
2689         ## (eg. linux/GTK) the 1st is an empty string value. 
2690         ## This is the only mechanism I can find to mask this problem: 
2691         if newvalue 
== self
._curValue 
or len(newvalue
) == 0: 
2692 ##            dbg('ignoring bogus text change event', indent=0) 
2695 ##            dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue)) 
2697                 if self
._signOk 
and self
._isNeg 
and newvalue
.find('-') == -1 and newvalue
.find('(') == -1: 
2698 ##                    dbg('clearing self._isNeg') 
2700                     text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
() 
2701                 self
._CheckValid
()  # Recolor control as appropriate 
2702 ##            dbg('calling event.Skip()') 
2705         self
._prevValue 
= self
._curValue    
# save for undo 
2706         self
._curValue 
= newvalue           
# Save last seen value for next iteration 
2711     def _OnKeyDown(self
, event
): 
2713         This function allows the control to capture Ctrl-events like Ctrl-tab, 
2714         that are not normally seen by the "cooked" EVT_CHAR routine. 
2716         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2717         key    
= event
.GetKeyCode() 
2718         if key 
in self
._nav 
and event
.ControlDown(): 
2719             # then this is the only place we will likely see these events; 
2721 ##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') 
2724         # else allow regular EVT_CHAR key processing 
2728     def _OnChar(self
, event
): 
2730         This is the engine of MaskedEdit controls.  It examines each keystroke, 
2731         decides if it's allowed, where it should go or what action to take. 
2733 ##        dbg('MaskedEditMixin::_OnChar', indent=1) 
2735         # Get keypress value, adjusted by control options (e.g. convert to upper etc) 
2736         key 
= event
.GetKeyCode() 
2737         orig_pos 
= self
._GetInsertionPoint
() 
2738         orig_value 
= self
._GetValue
() 
2739 ##        dbg('keycode = ', key) 
2740 ##        dbg('current pos = ', orig_pos) 
2741 ##        dbg('current selection = ', self._GetSelection()) 
2743         if not self
._Keypress
(key
): 
2747         # If no format string for this control, or the control is marked as "read-only", 
2748         # skip the rest of the special processing, and just "do the standard thing:" 
2749         if not self
._mask 
or not self
._IsEditable
(): 
2754         # Process navigation and control keys first, with 
2755         # position/selection unadulterated: 
2756         if key 
in self
._nav 
+ self
._control
: 
2757             if self
._keyhandlers
.has_key(key
): 
2758                 keep_processing 
= self
._keyhandlers
[key
](event
) 
2759                 if self
._GetValue
() != orig_value
: 
2760                     self
.modified 
= True 
2761                 if not keep_processing
: 
2764                 self
._applyFormatting
() 
2768         # Else... adjust the position as necessary for next input key, 
2769         # and determine resulting selection: 
2770         pos 
= self
._adjustPos
( orig_pos
, key 
)    ## get insertion position, adjusted as needed 
2771         sel_start
, sel_to 
= self
._GetSelection
()                ## check for a range of selected text 
2772 ##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) 
2774         keep_processing 
= True 
2775         # Capture user past end of format field 
2776         if pos 
> len(self
.maskdict
): 
2777 ##            dbg("field length exceeded:",pos) 
2778             keep_processing 
= False 
2781             if self
._isMaskChar
(pos
):  ## Get string of allowed characters for validation 
2782                 okchars 
= self
._getAllowedChars
(pos
) 
2784 ##                dbg('Not a valid position: pos = ', pos,"chars=",maskchars) 
2787         key 
= self
._adjustKey
(pos
, key
)     # apply formatting constraints to key: 
2789         if self
._keyhandlers
.has_key(key
): 
2790             # there's an override for default behavior; use override function instead 
2791 ##            dbg('using supplied key handler:', self._keyhandlers[key]) 
2792             keep_processing 
= self
._keyhandlers
[key
](event
) 
2793             if self
._GetValue
() != orig_value
: 
2794                 self
.modified 
= True 
2795             if not keep_processing
: 
2798             # else skip default processing, but do final formatting 
2799         if key 
< wx
.WXK_SPACE 
or key 
> 255: 
2800 ##            dbg('key < WXK_SPACE or key > 255') 
2801             event
.Skip()                # non alphanumeric 
2802             keep_processing 
= False 
2804             field 
= self
._FindField
(pos
) 
2805 ##            dbg("key ='%s'" % chr(key)) 
2807 ##                dbg('okSpaces?', field._okSpaces) 
2811             if chr(key
) in field
._excludeChars 
+ self
._ctrl
_constraints
._excludeChars
: 
2812                 keep_processing 
= False 
2814             if keep_processing 
and self
._isCharAllowed
( chr(key
), pos
, checkRegex 
= True ): 
2815 ##                dbg("key allowed by mask") 
2816                 # insert key into candidate new value, but don't change control yet: 
2817                 oldstr 
= self
._GetValue
() 
2818                 newstr
, newpos
, new_select_to
, match_field
, match_index 
= self
._insertKey
( 
2819                                 chr(key
), pos
, sel_start
, sel_to
, self
._GetValue
(), allowAutoSelect 
= True) 
2820 ##                dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr) 
2821                 if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
2822 ##                    dbg('not valid; checking to see if adjusted string is:') 
2823                     keep_processing 
= False 
2824                     if self
._isFloat 
and newstr 
!= self
._template
: 
2825                         newstr 
= self
._adjustFloat
(newstr
) 
2826 ##                        dbg('adjusted str:', newstr) 
2827                         if self
.IsValid(newstr
): 
2829                             keep_processing 
= True 
2830                             wx
.CallAfter(self
._SetInsertionPoint
, self
._decimalpos
) 
2831                     if not keep_processing
: 
2832 ##                        dbg("key disallowed by validation") 
2833                         if not wx
.Validator_IsSilent() and orig_pos 
== pos
: 
2839                     # special case: adjust date value as necessary: 
2840                     if self
._isDate 
and newstr 
!= self
._template
: 
2841                         newstr 
= self
._adjustDate
(newstr
) 
2842 ##                    dbg('adjusted newstr:', newstr) 
2844                     if newstr 
!= orig_value
: 
2845                         self
.modified 
= True 
2847                     wx
.CallAfter(self
._SetValue
, newstr
) 
2849                     # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: 
2850                     if not self
.IsDefault() and self
._isDate 
and self
._4digityear
: 
2851                         year2dig 
= self
._dateExtent 
- 2 
2852                         if pos 
== year2dig 
and unadjusted
[year2dig
] != newstr
[year2dig
]: 
2855                     wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
2857                     if match_field 
is not None: 
2858 ##                        dbg('matched field') 
2859                         self
._OnAutoSelect
(match_field
, match_index
) 
2861                     if new_select_to 
!= newpos
: 
2862 ##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) 
2863                         wx
.CallAfter(self
._SetSelection
, newpos
, new_select_to
) 
2865                         newfield 
= self
._FindField
(newpos
) 
2866                         if newfield 
!= field 
and newfield
._selectOnFieldEntry
: 
2867 ##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) 
2868                             wx
.CallAfter(self
._SetSelection
, newfield
._extent
[0], newfield
._extent
[1]) 
2869                     keep_processing 
= False 
2871             elif keep_processing
: 
2872 ##                dbg('char not allowed') 
2873                 keep_processing 
= False 
2874                 if (not wx
.Validator_IsSilent()) and orig_pos 
== pos
: 
2877         self
._applyFormatting
() 
2879         # Move to next insertion point 
2880         if keep_processing 
and key 
not in self
._nav
: 
2881             pos 
= self
._GetInsertionPoint
() 
2882             next_entry 
= self
._findNextEntry
( pos 
) 
2883             if pos 
!= next_entry
: 
2884 ##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) 
2885                 wx
.CallAfter(self
._SetInsertionPoint
, next_entry 
) 
2887             if self
._isTemplateChar
(pos
): 
2888                 self
._AdjustField
(pos
) 
2892     def _FindFieldExtent(self
, pos
=None, getslice
=False, value
=None): 
2893         """ returns editable extent of field corresponding to 
2894         position pos, and, optionally, the contents of that field 
2895         in the control or the value specified. 
2896         Template chars are bound to the preceding field. 
2897         For masks beginning with template chars, these chars are ignored 
2898         when calculating the current field. 
2900         Eg: with template (###) ###-####, 
2901         >>> self._FindFieldExtent(pos=0) 
2903         >>> self._FindFieldExtent(pos=1) 
2905         >>> self._FindFieldExtent(pos=5) 
2907         >>> self._FindFieldExtent(pos=6) 
2909         >>> self._FindFieldExtent(pos=10) 
2913 ##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) 
2915         field 
= self
._FindField
(pos
) 
2918                 return None, None, "" 
2921         edit_start
, edit_end 
= field
._extent
 
2923             if value 
is None: value 
= self
._GetValue
() 
2924             slice = value
[edit_start
:edit_end
] 
2925 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) 
2927             return edit_start
, edit_end
, slice 
2929 ##            dbg('edit_start:', edit_start, 'edit_end:', edit_end) 
2931             return edit_start
, edit_end
 
2934     def _FindField(self
, pos
=None): 
2936         Returns the field instance in which pos resides. 
2937         Template chars are bound to the preceding field. 
2938         For masks beginning with template chars, these chars are ignored 
2939         when calculating the current field. 
2942 ####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) 
2943         if pos 
is None: pos 
= self
._GetInsertionPoint
() 
2944         elif pos 
< 0 or pos 
> self
._masklength
: 
2945             raise IndexError('position %s out of range of control' % str(pos
)) 
2947         if len(self
._fields
) == 0: 
2953         return self
._fields
[self
._lookupField
[pos
]] 
2956     def ClearValue(self
): 
2957         """ Blanks the current control value by replacing it with the default value.""" 
2958 ##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") 
2959         self
._SetValue
( self
._template 
) 
2960         self
._SetInsertionPoint
(0) 
2964     def _baseCtrlEventHandler(self
, event
): 
2966         This function is used whenever a key should be handled by the base control. 
2972     def _OnUpNumeric(self
, event
): 
2974         Makes up-arrow act like shift-tab should; ie. take you to start of 
2977 ##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1) 
2978         event
.m_shiftDown 
= 1 
2979 ##        dbg('event.ShiftDown()?', event.ShiftDown()) 
2980         self
._OnChangeField
(event
) 
2984     def _OnArrow(self
, event
): 
2986         Used in response to left/right navigation keys; makes these actions skip 
2987         over mask template chars. 
2989 ##        dbg("MaskedEditMixin::_OnArrow", indent=1) 
2990         pos 
= self
._GetInsertionPoint
() 
2991         keycode 
= event
.GetKeyCode() 
2992         sel_start
, sel_to 
= self
._GetSelection
() 
2993         entry_end 
= self
._goEnd
(getPosOnly
=True) 
2994         if keycode 
in (wx
.WXK_RIGHT
, wx
.WXK_DOWN
): 
2995             if( ( not self
._isTemplateChar
(pos
) and pos
+1 > entry_end
) 
2996                 or ( self
._isTemplateChar
(pos
) and pos 
>= entry_end
) ): 
2997 ##                dbg("can't advance", indent=0) 
2999             elif self
._isTemplateChar
(pos
): 
3000                 self
._AdjustField
(pos
) 
3001         elif keycode 
in (wx
.WXK_LEFT
,wx
.WXK_UP
) and sel_start 
== sel_to 
and pos 
> 0 and self
._isTemplateChar
(pos
-1): 
3002 ##            dbg('adjusting field') 
3003             self
._AdjustField
(pos
) 
3005         # treat as shifted up/down arrows as tab/reverse tab: 
3006         if event
.ShiftDown() and keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
): 
3007             # remove "shifting" and treat as (forward) tab: 
3008             event
.m_shiftDown 
= False 
3009             keep_processing 
= self
._OnChangeField
(event
) 
3011         elif self
._FindField
(pos
)._selectOnFieldEntry
: 
3012             if( keycode 
in (wx
.WXK_UP
, wx
.WXK_LEFT
) 
3014                 and self
._isTemplateChar
(sel_start
-1) 
3015                 and sel_start 
!= self
._masklength
 
3016                 and not self
._signOk 
and not self
._useParens
): 
3018                 # call _OnChangeField to handle "ctrl-shifted event" 
3019                 # (which moves to previous field and selects it.) 
3020                 event
.m_shiftDown 
= True 
3021                 event
.m_ControlDown 
= True 
3022                 keep_processing 
= self
._OnChangeField
(event
) 
3023             elif( keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
) 
3024                   and sel_to 
!= self
._masklength
 
3025                   and self
._isTemplateChar
(sel_to
)): 
3027                 # when changing field to the right, ensure don't accidentally go left instead 
3028                 event
.m_shiftDown 
= False 
3029                 keep_processing 
= self
._OnChangeField
(event
) 
3031                 # treat arrows as normal, allowing selection 
3033 ##                dbg('using base ctrl event processing') 
3036             if( (sel_to 
== self
._fields
[0]._extent
[0] and keycode 
== wx
.WXK_LEFT
) 
3037                 or (sel_to 
== self
._masklength 
and keycode 
== wx
.WXK_RIGHT
) ): 
3038                 if not wx
.Validator_IsSilent(): 
3041                 # treat arrows as normal, allowing selection 
3043 ##                dbg('using base event processing') 
3046         keep_processing 
= False 
3048         return keep_processing
 
3051     def _OnCtrl_S(self
, event
): 
3052         """ Default Ctrl-S handler; prints value information if demo enabled. """ 
3053 ##        dbg("MaskedEditMixin::_OnCtrl_S") 
3055             print 'MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self
.GetValue(), self
.GetPlainValue()) 
3056             print "Valid? => " + str(self
.IsValid()) 
3057             print "Current field, start, end, value =", str( self
._FindFieldExtent
(getslice
=True)) 
3061     def _OnCtrl_X(self
, event
=None): 
3062         """ Handles ctrl-x keypress in control and Cut operation on context menu. 
3063             Should return False to skip other processing. """ 
3064 ##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1) 
3069     def _OnCtrl_C(self
, event
=None): 
3070         """ Handles ctrl-C keypress in control and Copy operation on context menu. 
3071             Uses base control handling. Should return False to skip other processing.""" 
3075     def _OnCtrl_V(self
, event
=None): 
3076         """ Handles ctrl-V keypress in control and Paste operation on context menu. 
3077             Should return False to skip other processing. """ 
3078 ##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1) 
3083     def _OnCtrl_Z(self
, event
=None): 
3084         """ Handles ctrl-Z keypress in control and Undo operation on context menu. 
3085             Should return False to skip other processing. """ 
3086 ##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) 
3091     def _OnCtrl_A(self
,event
=None): 
3092         """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ 
3093         end 
= self
._goEnd
(getPosOnly
=True) 
3094         if not event 
or (isinstance(event
, wx
.KeyEvent
) and event
.ShiftDown()): 
3095             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3096             wx
.CallAfter(self
._SetSelection
, 0, self
._masklength
) 
3098             wx
.CallAfter(self
._SetInsertionPoint
, 0) 
3099             wx
.CallAfter(self
._SetSelection
, 0, end
) 
3103     def _OnErase(self
, event
=None): 
3104         """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" 
3105 ##        dbg("MaskedEditMixin::_OnErase", indent=1) 
3106         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
3108         if event 
is None:   # called as action routine from Cut() operation. 
3111             key 
= event
.GetKeyCode() 
3113         field 
= self
._FindField
(sel_to
) 
3114         start
, end 
= field
._extent
 
3115         value 
= self
._GetValue
() 
3116         oldstart 
= sel_start
 
3118         # If trying to erase beyond "legal" bounds, disallow operation: 
3119         if( (sel_to 
== 0 and key 
== wx
.WXK_BACK
) 
3120             or (self
._signOk 
and sel_to 
== 1 and value
[0] == ' ' and key 
== wx
.WXK_BACK
) 
3121             or (sel_to 
== self
._masklength 
and sel_start 
== sel_to 
and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) 
3122             or (self
._signOk 
and self
._useParens
 
3123                 and sel_start 
== sel_to
 
3124                 and sel_to 
== self
._masklength 
- 1 
3125                 and value
[sel_to
] == ' ' and key 
== wx
.WXK_DELETE 
and not field
._insertRight
) ): 
3126             if not wx
.Validator_IsSilent(): 
3132         if( field
._insertRight                                  
# an insert-right field 
3133             and value
[start
:end
] != self
._template
[start
:end
]   # and field not empty 
3134             and sel_start 
>= start                              
# and selection starts in field 
3135             and ((sel_to 
== sel_start                           
# and no selection 
3136                   and sel_to 
== end                             
# and cursor at right edge 
3137                   and key 
in (wx
.WXK_BACK
, wx
.WXK_DELETE
))            # and either delete or backspace key 
3139                  (key 
== wx
.WXK_BACK                               
# backspacing 
3140                     and (sel_to 
== end                          
# and selection ends at right edge 
3141                          or sel_to 
< end 
and field
._allowInsert
)) ) ):  # or allow right insert at any point in field 
3143 ##            dbg('delete left') 
3144             # if backspace but left of cursor is empty, adjust cursor right before deleting 
3145             while( key 
== wx
.WXK_BACK
 
3146                    and sel_start 
== sel_to
 
3148                    and value
[start
:sel_start
] == self
._template
[start
:sel_start
]): 
3152 ##            dbg('sel_start, start:', sel_start, start) 
3154             if sel_start 
== sel_to
: 
3158             newfield 
= value
[start
:keep
] + value
[sel_to
:end
] 
3160             # handle sign char moving from outside field into the field: 
3161             move_sign_into_field 
= False 
3162             if not field
._padZero 
and self
._signOk 
and self
._isNeg 
and value
[0] in ('-', '('): 
3164                 newfield 
= signchar 
+ newfield
 
3165                 move_sign_into_field 
= True 
3166 ##            dbg('cut newfield: "%s"' % newfield) 
3168             # handle what should fill in from the left: 
3170             for i 
in range(start
, end 
- len(newfield
)): 
3173                 elif( self
._signOk 
and self
._isNeg 
and i 
== 1 
3174                       and ((self
._useParens 
and newfield
.find('(') == -1) 
3175                            or (not self
._useParens 
and newfield
.find('-') == -1)) ): 
3178                     left 
+= self
._template
[i
]   # this can produce strange results in combination with default values... 
3179             newfield 
= left 
+ newfield
 
3180 ##            dbg('filled newfield: "%s"' % newfield) 
3182             newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3184             # (handle sign located in "mask position" in front of field prior to delete) 
3185             if move_sign_into_field
: 
3186                 newstr 
= ' ' + newstr
[1:] 
3189             # handle erasure of (left) sign, moving selection accordingly... 
3190             if self
._signOk 
and sel_start 
== 0: 
3191                 newstr 
= value 
= ' ' + value
[1:] 
3194             if field
._allowInsert 
and sel_start 
>= start
: 
3195                 # selection (if any) falls within current insert-capable field: 
3196                 select_len 
= sel_to 
- sel_start
 
3197                 # determine where cursor should end up: 
3198                 if key 
== wx
.WXK_BACK
: 
3200                         newpos 
= sel_start 
-1 
3206                     if sel_to 
== sel_start
: 
3207                         erase_to 
= sel_to 
+ 1 
3211                 if self
._isTemplateChar
(newpos
) and select_len 
== 0: 
3213                         if value
[newpos
] in ('(', '-'): 
3214                             newpos 
+= 1     # don't move cusor 
3215                             newstr 
= ' ' + value
[newpos
:] 
3216                         elif value
[newpos
] == ')': 
3217                             # erase right sign, but don't move cursor; (matching left sign handled later) 
3218                             newstr 
= value
[:newpos
] + ' ' 
3220                             # no deletion; just move cursor 
3223                         # no deletion; just move cursor 
3226                     if erase_to 
> end
: erase_to 
= end
 
3227                     erase_len 
= erase_to 
- newpos
 
3229                     left 
= value
[start
:newpos
] 
3230 ##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) 
3231                     right 
= value
[erase_to
:end
] + self
._template
[end
-erase_len
:end
] 
3233                     if field
._alignRight
: 
3234                         rstripped 
= right
.rstrip() 
3235                         if rstripped 
!= right
: 
3236                             pos_adjust 
= len(right
) - len(rstripped
) 
3239                     if not field
._insertRight 
and value
[-1] == ')' and end 
== self
._masklength 
- 1: 
3240                         # need to shift ) into the field: 
3241                         right 
= right
[:-1] + ')' 
3242                         value 
= value
[:-1] + ' ' 
3244                     newfield 
= left
+right
 
3246                         newfield 
= newfield
.rjust(end
-start
) 
3247                         newpos 
+= pos_adjust
 
3248 ##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) 
3249                     newstr 
= value
[:start
] + newfield 
+ value
[end
:] 
3254                 if sel_start 
== sel_to
: 
3255 ##                    dbg("current sel_start, sel_to:", sel_start, sel_to) 
3256                     if key 
== wx
.WXK_BACK
: 
3257                         sel_start
, sel_to 
= sel_to
-1, sel_to
-1 
3258 ##                        dbg("new sel_start, sel_to:", sel_start, sel_to) 
3260                     if field
._padZero 
and not value
[start
:sel_to
].replace('0', '').replace(' ','').replace(field
._fillChar
, ''): 
3261                         # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: 
3264                         newchar 
= self
._template
[sel_to
] ## get an original template character to "clear" the current char 
3265 ##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) 
3267                     if self
._isTemplateChar
(sel_to
): 
3268                         if sel_to 
== 0 and self
._signOk 
and value
[sel_to
] == '-':   # erasing "template" sign char 
3269                             newstr 
= ' ' + value
[1:] 
3271                         elif self
._signOk 
and self
._useParens 
and (value
[sel_to
] == ')' or value
[sel_to
] == '('): 
3272                             # allow "change sign" by removing both parens: 
3273                             newstr 
= value
[:self
._signpos
] + ' ' + value
[self
._signpos
+1:-1] + ' ' 
3278                         if field
._insertRight 
and sel_start 
== sel_to
: 
3279                             # force non-insert-right behavior, by selecting char to be replaced: 
3281                         newstr
, ignore 
= self
._insertKey
(newchar
, sel_start
, sel_start
, sel_to
, value
) 
3285                     newstr 
= self
._eraseSelection
(value
, sel_start
, sel_to
) 
3287                 pos 
= sel_start  
# put cursor back at beginning of selection 
3289         if self
._signOk 
and self
._useParens
: 
3290             # account for resultant unbalanced parentheses: 
3291             left_signpos 
= newstr
.find('(') 
3292             right_signpos 
= newstr
.find(')') 
3294             if left_signpos 
== -1 and right_signpos 
!= -1: 
3295                 # erased left-sign marker; get rid of right sign marker: 
3296                 newstr 
= newstr
[:right_signpos
] + ' ' + newstr
[right_signpos
+1:] 
3298             elif left_signpos 
!= -1 and right_signpos 
== -1: 
3299                 # erased right-sign marker; get rid of left-sign marker: 
3300                 newstr 
= newstr
[:left_signpos
] + ' ' + newstr
[left_signpos
+1:] 
3302 ##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) 
3303 ##        dbg("newstr:'%s'" % newstr, 'pos:', pos) 
3305         # if erasure results in an invalid field, disallow it: 
3306 ##        dbg('field._validRequired?', field._validRequired) 
3307 ##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) 
3308         if field
._validRequired 
and not field
.IsValid(newstr
[start
:end
]): 
3309             if not wx
.Validator_IsSilent(): 
3314         # if erasure results in an invalid value, disallow it: 
3315         if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3316             if not wx
.Validator_IsSilent(): 
3321 ##        dbg('setting value (later) to', newstr) 
3322         wx
.CallAfter(self
._SetValue
, newstr
) 
3323 ##        dbg('setting insertion point (later) to', pos) 
3324         wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3327             self
.modified 
= True 
3331     def _OnEnd(self
,event
): 
3332         """ Handles End keypress in control. Should return False to skip other processing. """ 
3333 ##        dbg("MaskedEditMixin::_OnEnd", indent=1) 
3334         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3335         if not event
.ControlDown(): 
3336             end 
= self
._masklength  
# go to end of control 
3337             if self
._signOk 
and self
._useParens
: 
3338                 end 
= end 
- 1       # account for reserved char at end 
3340             end_of_input 
= self
._goEnd
(getPosOnly
=True) 
3341             sel_start
, sel_to 
= self
._GetSelection
() 
3342             if sel_to 
< pos
: sel_to 
= pos
 
3343             field 
= self
._FindField
(sel_to
) 
3344             field_end 
= self
._FindField
(end_of_input
) 
3346             # pick different end point if either: 
3347             # - cursor not in same field 
3348             # - or at or past last input already 
3349             # - or current selection = end of current field: 
3350 ####            dbg('field != field_end?', field != field_end) 
3351 ####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input) 
3352             if field 
!= field_end 
or sel_to 
>= end_of_input
: 
3353                 edit_start
, edit_end 
= field
._extent
 
3354 ####                dbg('edit_end:', edit_end) 
3355 ####                dbg('sel_to:', sel_to) 
3356 ####                dbg('sel_to == edit_end?', sel_to == edit_end) 
3357 ####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) 
3359                 if sel_to 
== edit_end 
and field
._index 
< self
._field
_indices
[-1]: 
3360                     edit_start
, edit_end 
= self
._FindFieldExtent
(self
._findNextEntry
(edit_end
))  # go to end of next field: 
3362 ##                    dbg('end moved to', end) 
3364                 elif sel_to 
== edit_end 
and field
._index 
== self
._field
_indices
[-1]: 
3365                     # already at edit end of last field; select to end of control: 
3366                     end 
= self
._masklength
 
3367 ##                    dbg('end moved to', end) 
3369                     end 
= edit_end  
# select to end of current field 
3370 ##                    dbg('end moved to ', end) 
3372                 # select to current end of input 
3376 ####        dbg('pos:', pos, 'end:', end) 
3378         if event
.ShiftDown(): 
3379             if not event
.ControlDown(): 
3380 ##                dbg("shift-end; select to end of control") 
3383 ##                dbg("shift-ctrl-end; select to end of non-whitespace") 
3385             wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3386             wx
.CallAfter(self
._SetSelection
, pos
, end
) 
3388             if not event
.ControlDown(): 
3389 ##                dbg('go to end of control:') 
3391             wx
.CallAfter(self
._SetInsertionPoint
, end
) 
3392             wx
.CallAfter(self
._SetSelection
, end
, end
) 
3398     def _OnReturn(self
, event
): 
3400          Changes the event to look like a tab event, so we can then call 
3401          event.Skip() on it, and have the parent form "do the right thing." 
3403 ##         dbg('MaskedEditMixin::OnReturn') 
3404          event
.m_keyCode 
= wx
.WXK_TAB
 
3408     def _OnHome(self
,event
): 
3409         """ Handles Home keypress in control. Should return False to skip other processing.""" 
3410 ##        dbg("MaskedEditMixin::_OnHome", indent=1) 
3411         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3412         sel_start
, sel_to 
= self
._GetSelection
() 
3414         # There are 5 cases here: 
3416         # 1) shift: select from start of control to end of current 
3418         if event
.ShiftDown() and not event
.ControlDown(): 
3419 ##            dbg("shift-home; select to start of control") 
3423         # 2) no shift, no control: move cursor to beginning of control. 
3424         elif not event
.ControlDown(): 
3425 ##            dbg("home; move to start of control") 
3429         # 3) No shift, control: move cursor back to beginning of field; if 
3430         #    there already, go to beginning of previous field. 
3431         # 4) shift, control, start of selection not at beginning of control: 
3432         #    move sel_start back to start of field; if already there, go to 
3433         #    start of previous field. 
3434         elif( event
.ControlDown() 
3435               and (not event
.ShiftDown() 
3436                    or (event
.ShiftDown() and sel_start 
> 0) ) ): 
3437             if len(self
._field
_indices
) > 1: 
3438                 field 
= self
._FindField
(sel_start
) 
3439                 start
, ignore 
= field
._extent
 
3440                 if sel_start 
== start 
and field
._index 
!= self
._field
_indices
[0]:  # go to start of previous field: 
3441                     start
, ignore 
= self
._FindFieldExtent
(sel_start
-1) 
3442                 elif sel_start 
== start
: 
3443                     start 
= 0   # go to literal beginning if edit start 
3450             if not event
.ShiftDown(): 
3451 ##                dbg("ctrl-home; move to beginning of field") 
3454 ##                dbg("shift-ctrl-home; select to beginning of field") 
3458         # 5) shift, control, start of selection at beginning of control: 
3459         #    unselect by moving sel_to backward to beginning of current field; 
3460         #    if already there, move to start of previous field. 
3462             if len(self
._field
_indices
) > 1: 
3463                 # find end of previous field: 
3464                 field 
= self
._FindField
(sel_to
) 
3465                 if sel_to 
> start 
and field
._index 
!= self
._field
_indices
[0]: 
3466                     ignore
, end 
= self
._FindFieldExtent
(field
._extent
[0]-1) 
3472                 end_of_field 
= False 
3473 ##            dbg("shift-ctrl-home; unselect to beginning of field") 
3475 ##        dbg('queuing new sel_start, sel_to:', (start, end)) 
3476         wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3477         wx
.CallAfter(self
._SetSelection
, start
, end
) 
3482     def _OnChangeField(self
, event
): 
3484         Primarily handles TAB events, but can be used for any key that 
3485         designer wants to change fields within a masked edit control. 
3486         NOTE: at the moment, although coded to handle shift-TAB and 
3487         control-shift-TAB, these events are not sent to the controls 
3490 ##        dbg('MaskedEditMixin::_OnChangeField', indent = 1) 
3491         # determine end of current field: 
3492         pos 
= self
._GetInsertionPoint
() 
3493 ##        dbg('current pos:', pos) 
3494         sel_start
, sel_to 
= self
._GetSelection
() 
3496         if self
._masklength 
< 0:   # no fields; process tab normally 
3497             self
._AdjustField
(pos
) 
3498             if event
.GetKeyCode() == wx
.WXK_TAB
: 
3499 ##                dbg('tab to next ctrl') 
3506         if event
.ShiftDown(): 
3510             # NOTE: doesn't yet work with SHIFT-tab under wx; the control 
3511             # never sees this event! (But I've coded for it should it ever work, 
3512             # and it *does* work for '.' in IpAddrCtrl.) 
3513             field 
= self
._FindField
(pos
) 
3514             index 
= field
._index
 
3515             field_start 
= field
._extent
[0] 
3516             if pos 
< field_start
: 
3517 ##                dbg('cursor before 1st field; cannot change to a previous field') 
3518                 if not wx
.Validator_IsSilent(): 
3522             if event
.ControlDown(): 
3523 ##                dbg('queuing select to beginning of field:', field_start, pos) 
3524                 wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3525                 wx
.CallAfter(self
._SetSelection
, field_start
, pos
) 
3530                   # We're already in the 1st field; process shift-tab normally: 
3531                 self
._AdjustField
(pos
) 
3532                 if event
.GetKeyCode() == wx
.WXK_TAB
: 
3533 ##                    dbg('tab to previous ctrl') 
3536 ##                    dbg('position at beginning') 
3537                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3541                 # find beginning of previous field: 
3542                 begin_prev 
= self
._FindField
(field_start
-1)._extent
[0] 
3543                 self
._AdjustField
(pos
) 
3544 ##                dbg('repositioning to', begin_prev) 
3545                 wx
.CallAfter(self
._SetInsertionPoint
, begin_prev
) 
3546                 if self
._FindField
(begin_prev
)._selectOnFieldEntry
: 
3547                     edit_start
, edit_end 
= self
._FindFieldExtent
(begin_prev
) 
3548 ##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) 
3549                     wx
.CallAfter(self
._SetInsertionPoint
, edit_start
) 
3550                     wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3556             field 
= self
._FindField
(sel_to
) 
3557             field_start
, field_end 
= field
._extent
 
3558             if event
.ControlDown(): 
3559 ##                dbg('queuing select to end of field:', pos, field_end) 
3560                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3561                 wx
.CallAfter(self
._SetSelection
, pos
, field_end
) 
3565                 if pos 
< field_start
: 
3566 ##                    dbg('cursor before 1st field; go to start of field') 
3567                     wx
.CallAfter(self
._SetInsertionPoint
, field_start
) 
3568                     if field
._selectOnFieldEntry
: 
3569                         wx
.CallAfter(self
._SetSelection
, field_start
, field_end
) 
3571                         wx
.CallAfter(self
._SetSelection
, field_start
, field_start
) 
3574 ##                dbg('end of current field:', field_end) 
3575 ##                dbg('go to next field') 
3576                 if field_end 
== self
._fields
[self
._field
_indices
[-1]]._extent
[1]: 
3577                     self
._AdjustField
(pos
) 
3578                     if event
.GetKeyCode() == wx
.WXK_TAB
: 
3579 ##                        dbg('tab to next ctrl') 
3582 ##                        dbg('position at end') 
3583                         wx
.CallAfter(self
._SetInsertionPoint
, field_end
) 
3587                     # we have to find the start of the next field 
3588                     next_pos 
= self
._findNextEntry
(field_end
) 
3589                     if next_pos 
== field_end
: 
3590 ##                        dbg('already in last field') 
3591                         self
._AdjustField
(pos
) 
3592                         if event
.GetKeyCode() == wx
.WXK_TAB
: 
3593 ##                            dbg('tab to next ctrl') 
3599                         self
._AdjustField
( pos 
) 
3601                         # move cursor to appropriate point in the next field and select as necessary: 
3602                         field 
= self
._FindField
(next_pos
) 
3603                         edit_start
, edit_end 
= field
._extent
 
3604                         if field
._selectOnFieldEntry
: 
3605 ##                            dbg('move to ', next_pos) 
3606                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3607                             edit_start
, edit_end 
= self
._FindFieldExtent
(next_pos
) 
3608 ##                            dbg('queuing select', edit_start, edit_end) 
3609                             wx
.CallAfter(self
._SetSelection
, edit_start
, edit_end
) 
3611                             if field
._insertRight
: 
3612                                 next_pos 
= field
._extent
[1] 
3613 ##                            dbg('move to ', next_pos) 
3614                             wx
.CallAfter(self
._SetInsertionPoint
, next_pos
) 
3619     def _OnDecimalPoint(self
, event
): 
3620 ##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) 
3622         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3624         if self
._isFloat
:       ## handle float value, move to decimal place 
3625 ##            dbg('key == Decimal tab; decimal pos:', self._decimalpos) 
3626             value 
= self
._GetValue
() 
3627             if pos 
< self
._decimalpos
: 
3628                 clipped_text 
= value
[0:pos
] + self
._decimalChar 
+ value
[self
._decimalpos
+1:] 
3629 ##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3630                 newstr 
= self
._adjustFloat
(clipped_text
) 
3632                 newstr 
= self
._adjustFloat
(value
) 
3633             wx
.CallAfter(self
._SetValue
, newstr
) 
3634             fraction 
= self
._fields
[1] 
3635             start
, end 
= fraction
._extent
 
3636             wx
.CallAfter(self
._SetInsertionPoint
, start
) 
3637             if fraction
._selectOnFieldEntry
: 
3638 ##                dbg('queuing selection after decimal point to:', (start, end)) 
3639                 wx
.CallAfter(self
._SetSelection
, start
, end
) 
3640             keep_processing 
= False 
3642         if self
._isInt
:      ## handle integer value, truncate from current position 
3643 ##            dbg('key == Integer decimal event') 
3644             value 
= self
._GetValue
() 
3645             clipped_text 
= value
[0:pos
] 
3646 ##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) 
3647             newstr 
= self
._adjustInt
(clipped_text
) 
3648 ##            dbg('newstr: "%s"' % newstr) 
3649             wx
.CallAfter(self
._SetValue
, newstr
) 
3650             newpos 
= len(newstr
.rstrip()) 
3651             if newstr
.find(')') != -1: 
3652                 newpos 
-= 1     # (don't move past right paren) 
3653             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3654             keep_processing 
= False 
3658     def _OnChangeSign(self
, event
): 
3659 ##        dbg('MaskedEditMixin::_OnChangeSign', indent=1) 
3660         key 
= event
.GetKeyCode() 
3661         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), key
) 
3662         value 
= self
._eraseSelection
() 
3663         integer 
= self
._fields
[0] 
3664         start
, end 
= integer
._extent
 
3666 ####        dbg('adjusted pos:', pos) 
3667         if chr(key
) in ('-','+','(', ')') or (chr(key
) == " " and pos 
== self
._signpos
): 
3668             cursign 
= self
._isNeg
 
3669 ##            dbg('cursign:', cursign) 
3670             if chr(key
) in ('-','(', ')'): 
3671                 self
._isNeg 
= (not self
._isNeg
)   ## flip value 
3674 ##            dbg('isNeg?', self._isNeg) 
3676             text
, self
._signpos
, self
._right
_signpos 
= self
._getSignedValue
(candidate
=value
) 
3677 ##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) 
3681             if self
._isNeg 
and self
._signpos 
is not None and self
._signpos 
!= -1: 
3682                 if self
._useParens 
and self
._right
_signpos 
is not None: 
3683                     text 
= text
[:self
._signpos
] + '(' + text
[self
._signpos
+1:self
._right
_signpos
] + ')' + text
[self
._right
_signpos
+1:] 
3685                     text 
= text
[:self
._signpos
] + '-' + text
[self
._signpos
+1:] 
3687 ####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) 
3689                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:self
._right
_signpos
] + ' ' + text
[self
._right
_signpos
+1:] 
3691                     text 
= text
[:self
._signpos
] + ' ' + text
[self
._signpos
+1:] 
3692 ##                dbg('clearing self._isNeg') 
3695             wx
.CallAfter(self
._SetValue
, text
) 
3696             wx
.CallAfter(self
._applyFormatting
) 
3697 ##            dbg('pos:', pos, 'signpos:', self._signpos) 
3698             if pos 
== self
._signpos 
or integer
.IsEmpty(text
[start
:end
]): 
3699                 wx
.CallAfter(self
._SetInsertionPoint
, self
._signpos
+1) 
3701                 wx
.CallAfter(self
._SetInsertionPoint
, pos
) 
3703             keep_processing 
= False 
3705             keep_processing 
= True 
3707         return keep_processing
 
3710     def _OnGroupChar(self
, event
): 
3712         This handler is only registered if the mask is a numeric mask. 
3713         It allows the insertion of ',' or '.' if appropriate. 
3715 ##        dbg('MaskedEditMixin::_OnGroupChar', indent=1) 
3716         keep_processing 
= True 
3717         pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
3718         sel_start
, sel_to 
= self
._GetSelection
() 
3719         groupchar 
= self
._fields
[0]._groupChar
 
3720         if not self
._isCharAllowed
(groupchar
, pos
, checkRegex
=True): 
3721             keep_processing 
= False 
3722             if not wx
.Validator_IsSilent(): 
3726             newstr
, newpos 
= self
._insertKey
(groupchar
, pos
, sel_start
, sel_to
, self
._GetValue
() ) 
3727 ##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) 
3728             if self
._ctrl
_constraints
._validRequired 
and not self
.IsValid(newstr
): 
3729                 keep_processing 
= False 
3730                 if not wx
.Validator_IsSilent(): 
3734             wx
.CallAfter(self
._SetValue
, newstr
) 
3735             wx
.CallAfter(self
._SetInsertionPoint
, newpos
) 
3736         keep_processing 
= False 
3738         return keep_processing
 
3741     def _findNextEntry(self
,pos
, adjustInsert
=True): 
3742         """ Find the insertion point for the next valid entry character position.""" 
3743         if self
._isTemplateChar
(pos
):   # if changing fields, pay attn to flag 
3744             adjustInsert 
= adjustInsert
 
3745         else:                           # else within a field; flag not relevant 
3746             adjustInsert 
= False 
3748         while self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3751         # if changing fields, and we've been told to adjust insert point, 
3752         # look at new field; if empty and right-insert field, 
3753         # adjust to right edge: 
3754         if adjustInsert 
and pos 
< self
._masklength
: 
3755             field 
= self
._FindField
(pos
) 
3756             start
, end 
= field
._extent
 
3757             slice = self
._GetValue
()[start
:end
] 
3758             if field
._insertRight 
and field
.IsEmpty(slice): 
3763     def _findNextTemplateChar(self
, pos
): 
3764         """ Find the position of the next non-editable character in the mask.""" 
3765         while not self
._isTemplateChar
(pos
) and pos 
< self
._masklength
: 
3770     def _OnAutoCompleteField(self
, event
): 
3771 ##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) 
3772         pos 
= self
._GetInsertionPoint
() 
3773         field 
= self
._FindField
(pos
) 
3774         edit_start
, edit_end
, slice = self
._FindFieldExtent
(pos
, getslice
=True) 
3777         keycode 
= event
.GetKeyCode() 
3779         if field
._fillChar 
!= ' ': 
3780             text 
= slice.replace(field
._fillChar
, '') 
3784         keep_processing 
= True  # (assume True to start) 
3785 ##        dbg('field._hasList?', field._hasList) 
3787 ##            dbg('choices:', field._choices) 
3788 ##            dbg('compareChoices:', field._compareChoices) 
3789             choices
, choice_required 
= field
._compareChoices
, field
._choiceRequired
 
3790             if keycode 
in (wx
.WXK_PRIOR
, wx
.WXK_UP
): 
3794             match_index
, partial_match 
= self
._autoComplete
(direction
, choices
, text
, compareNoCase
=field
._compareNoCase
, current_index 
= field
._autoCompleteIndex
) 
3795             if( match_index 
is None 
3796                 and (keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3797                      or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown() ) ) ): 
3798                 # Select the 1st thing from the list: 
3801             if( match_index 
is not None 
3802                 and ( keycode 
in self
._autoCompleteKeycodes 
+ [wx
.WXK_PRIOR
, wx
.WXK_NEXT
] 
3803                       or (keycode 
in [wx
.WXK_UP
, wx
.WXK_DOWN
] and event
.ShiftDown()) 
3804                       or (keycode 
== wx
.WXK_DOWN 
and partial_match
) ) ): 
3806                 # We're allowed to auto-complete: 
3807 ##                dbg('match found') 
3808                 value 
= self
._GetValue
() 
3809                 newvalue 
= value
[:edit_start
] + field
._choices
[match_index
] + value
[edit_end
:] 
3810 ##                dbg('setting value to "%s"' % newvalue) 
3811                 self
._SetValue
(newvalue
) 
3812                 self
._SetInsertionPoint
(min(edit_end
, len(newvalue
.rstrip()))) 
3813                 self
._OnAutoSelect
(field
, match_index
) 
3814                 self
._CheckValid
()  # recolor as appopriate 
3817         if keycode 
in (wx
.WXK_UP
, wx
.WXK_DOWN
, wx
.WXK_LEFT
, wx
.WXK_RIGHT
): 
3818             # treat as left right arrow if unshifted, tab/shift tab if shifted. 
3819             if event
.ShiftDown(): 
3820                 if keycode 
in (wx
.WXK_DOWN
, wx
.WXK_RIGHT
): 
3821                     # remove "shifting" and treat as (forward) tab: 
3822                     event
.m_shiftDown 
= False 
3823                 keep_processing 
= self
._OnChangeField
(event
) 
3825                 keep_processing 
= self
._OnArrow
(event
) 
3826         # else some other key; keep processing the key 
3828 ##        dbg('keep processing?', keep_processing, indent=0) 
3829         return keep_processing
 
3832     def _OnAutoSelect(self
, field
, match_index 
= None): 
3834         Function called if autoselect feature is enabled and entire control 
3837 ##        dbg('MaskedEditMixin::OnAutoSelect', field._index) 
3838         if match_index 
is not None: 
3839             field
._autoCompleteIndex 
= match_index
 
3842     def _autoComplete(self
, direction
, choices
, value
, compareNoCase
, current_index
): 
3844         This function gets called in response to Auto-complete events. 
3845         It attempts to find a match to the specified value against the 
3846         list of choices; if exact match, the index of then next 
3847         appropriate value in the list, based on the given direction. 
3848         If not an exact match, it will return the index of the 1st value from 
3849         the choice list for which the partial value can be extended to match. 
3850         If no match found, it will return None. 
3851         The function returns a 2-tuple, with the 2nd element being a boolean 
3852         that indicates if partial match was necessary. 
3854 ##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) 
3856 ##            dbg('nothing to match against', indent=0) 
3857             return (None, False) 
3859         partial_match 
= False 
3862             value 
= value
.lower() 
3864         last_index 
= len(choices
) - 1 
3865         if value 
in choices
: 
3866 ##            dbg('"%s" in', choices) 
3867             if current_index 
is not None and choices
[current_index
] == value
: 
3868                 index 
= current_index
 
3870                 index 
= choices
.index(value
) 
3872 ##            dbg('matched "%s" (%d)' % (choices[index], index)) 
3874 ##                dbg('going to previous') 
3875                 if index 
== 0: index 
= len(choices
) - 1 
3878                 if index 
== len(choices
) - 1: index 
= 0 
3880 ##            dbg('change value to "%s" (%d)' % (choices[index], index)) 
3883             partial_match 
= True 
3884             value 
= value
.strip() 
3885 ##            dbg('no match; try to auto-complete:') 
3887 ##            dbg('searching for "%s"' % value) 
3888             if current_index 
is None: 
3889                 indices 
= range(len(choices
)) 
3894                     indices 
= range(current_index 
+1, len(choices
)) + range(current_index
+1) 
3895 ##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) 
3897                     indices 
= range(current_index
-1, -1, -1) + range(len(choices
)-1, current_index
-1, -1) 
3898 ##                    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) 
3899 ####            dbg('indices:', indices) 
3900             for index 
in indices
: 
3901                 choice 
= choices
[index
] 
3902                 if choice
.find(value
, 0) == 0: 
3903 ##                    dbg('match found:', choice) 
3906                 else: dbg('choice: "%s" - no match' % choice
) 
3907             if match 
is not None: 
3908 ##                dbg('matched', match) 
3911 ##                dbg('no match found') 
3914         return (match
, partial_match
) 
3917     def _AdjustField(self
, pos
): 
3919         This function gets called by default whenever the cursor leaves a field. 
3920         The pos argument given is the char position before leaving that field. 
3921         By default, floating point, integer and date values are adjusted to be 
3922         legal in this function.  Derived classes may override this function 
3923         to modify the value of the control in a different way when changing fields. 
3925         NOTE: these change the value immediately, and restore the cursor to 
3926         the passed location, so that any subsequent code can then move it 
3927         based on the operation being performed. 
3929         newvalue 
= value 
= self
._GetValue
() 
3930         field 
= self
._FindField
(pos
) 
3931         start
, end
, slice = self
._FindFieldExtent
(getslice
=True) 
3932         newfield 
= field
._AdjustField
(slice) 
3933         newvalue 
= value
[:start
] + newfield 
+ value
[end
:] 
3935         if self
._isFloat 
and newvalue 
!= self
._template
: 
3936             newvalue 
= self
._adjustFloat
(newvalue
) 
3938         if self
._ctrl
_constraints
._isInt 
and value 
!= self
._template
: 
3939             newvalue 
= self
._adjustInt
(value
) 
3941         if self
._isDate 
and value 
!= self
._template
: 
3942             newvalue 
= self
._adjustDate
(value
, fixcentury
=True) 
3943             if self
._4digityear
: 
3944                 year2dig 
= self
._dateExtent 
- 2 
3945                 if pos 
== year2dig 
and value
[year2dig
] != newvalue
[year2dig
]: 
3948         if newvalue 
!= value
: 
3949             self
._SetValue
(newvalue
) 
3950             self
._SetInsertionPoint
(pos
) 
3953     def _adjustKey(self
, pos
, key
): 
3954         """ Apply control formatting to the key (e.g. convert to upper etc). """ 
3955         field 
= self
._FindField
(pos
) 
3956         if field
._forceupper 
and key 
in range(97,123): 
3957             key 
= ord( chr(key
).upper()) 
3959         if field
._forcelower 
and key 
in range(97,123): 
3960             key 
= ord( chr(key
).lower()) 
3965     def _adjustPos(self
, pos
, key
): 
3967         Checks the current insertion point position and adjusts it if 
3968         necessary to skip over non-editable characters. 
3970 ##        dbg('_adjustPos', pos, key, indent=1) 
3971         sel_start
, sel_to 
= self
._GetSelection
() 
3972         # If a numeric or decimal mask, and negatives allowed, reserve the 
3973         # first space for sign, and last one if using parens. 
3975             and ((pos 
== self
._signpos 
and key 
in (ord('-'), ord('+'), ord(' ')) ) 
3976                  or self
._useParens 
and pos 
== self
._masklength 
-1)): 
3977 ##            dbg('adjusted pos:', pos, indent=0) 
3980         if key 
not in self
._nav
: 
3981             field 
= self
._FindField
(pos
) 
3983 ##            dbg('field._insertRight?', field._insertRight) 
3984             if field
._insertRight
:              # if allow right-insert 
3985                 start
, end 
= field
._extent
 
3986                 slice = self
._GetValue
()[start
:end
].strip() 
3987                 field_len 
= end 
- start
 
3988                 if pos 
== end
:                      # if cursor at right edge of field 
3989                     # if not filled or supposed to stay in field, keep current position 
3990 ####                    dbg('pos==end') 
3991 ####                    dbg('len (slice):', len(slice)) 
3992 ####                    dbg('field_len?', field_len) 
3993 ####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) 
3994 ####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) 
3995                     if len(slice) == field_len 
and field
._moveOnFieldFull
: 
3996                         # move cursor to next field: 
3997                         pos 
= self
._findNextEntry
(pos
) 
3998                         self
._SetInsertionPoint
(pos
) 
4000                             self
._SetSelection
(pos
, sel_to
)     # restore selection 
4002                             self
._SetSelection
(pos
, pos
)        # remove selection 
4003                     else: # leave cursor alone 
4006                     # if at start of control, move to right edge 
4007                     if (sel_to 
== sel_start
 
4008                         and (self
._isTemplateChar
(pos
) or (pos 
== start 
and len(slice)+ 1 < field_len
)) 
4010                         pos 
= end                   
# move to right edge 
4011 ##                    elif sel_start <= start and sel_to == end: 
4012 ##                        # select to right edge of field - 1 (to replace char) 
4014 ##                        self._SetInsertionPoint(pos) 
4015 ##                        # restore selection 
4016 ##                        self._SetSelection(sel_start, pos) 
4018                     elif self
._signOk 
and sel_start 
== 0:   # if selected to beginning and signed, 
4019                         # adjust to past reserved sign position: 
4020                         pos 
= self
._fields
[0]._extent
[0] 
4021                         self
._SetInsertionPoint
(pos
) 
4023                         self
._SetSelection
(pos
, sel_to
) 
4025                         pass    # leave position/selection alone 
4027             # else make sure the user is not trying to type over a template character 
4028             # If they are, move them to the next valid entry position 
4029             elif self
._isTemplateChar
(pos
): 
4030                 if( not field
._moveOnFieldFull
 
4031                       and (not self
._signOk
 
4033                                and field
._index 
== 0 
4034                                and pos 
> 0) ) ):      # don't move to next field without explicit cursor movement 
4037                     # find next valid position 
4038                     pos 
= self
._findNextEntry
(pos
) 
4039                     self
._SetInsertionPoint
(pos
) 
4040                     if pos 
< sel_to
:    # restore selection 
4041                         self
._SetSelection
(pos
, sel_to
) 
4042 ##        dbg('adjusted pos:', pos, indent=0) 
4046     def _adjustFloat(self
, candidate
=None): 
4048         'Fixes' an floating point control. Collapses spaces, right-justifies, etc. 
4050 ##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) 
4051         lenInt
,lenFraction  
= [len(s
) for s 
in self
._mask
.split('.')]  ## Get integer, fraction lengths 
4053         if candidate 
is None: value 
= self
._GetValue
() 
4054         else: value 
= candidate
 
4055 ##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) 
4056         intStr
, fracStr 
= value
.split(self
._decimalChar
) 
4058         intStr 
= self
._fields
[0]._AdjustField
(intStr
) 
4059 ##        dbg('adjusted intStr: "%s"' % intStr) 
4060         lenInt 
= len(intStr
) 
4061         fracStr 
= fracStr 
+ ('0'*(lenFraction
-len(fracStr
)))  # add trailing spaces to decimal 
4063 ##        dbg('intStr "%(intStr)s"' % locals()) 
4064 ##        dbg('lenInt:', lenInt) 
4066         intStr 
= string
.rjust( intStr
[-lenInt
:], lenInt
) 
4067 ##        dbg('right-justifed intStr = "%(intStr)s"' % locals()) 
4068         newvalue 
= intStr 
+ self
._decimalChar 
+ fracStr
 
4071             if len(newvalue
) < self
._masklength
: 
4072                 newvalue 
= ' ' + newvalue
 
4073             signedvalue 
= self
._getSignedValue
(newvalue
)[0] 
4074             if signedvalue 
is not None: newvalue 
= signedvalue
 
4076         # Finally, align string with decimal position, left-padding with 
4078         newdecpos 
= newvalue
.find(self
._decimalChar
) 
4079         if newdecpos 
< self
._decimalpos
: 
4080             padlen 
= self
._decimalpos 
- newdecpos
 
4081             newvalue 
= string
.join([' ' * padlen
] + [newvalue
] ,'') 
4083         if self
._signOk 
and self
._useParens
: 
4084             if newvalue
.find('(') != -1: 
4085                 newvalue 
= newvalue
[:-1] + ')' 
4087                 newvalue 
= newvalue
[:-1] + ' ' 
4089 ##        dbg('newvalue = "%s"' % newvalue) 
4090         if candidate 
is None: 
4091             wx
.CallAfter(self
._SetValue
, newvalue
) 
4096     def _adjustInt(self
, candidate
=None): 
4097         """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" 
4098 ##        dbg("MaskedEditMixin::_adjustInt", candidate) 
4099         lenInt 
= self
._masklength
 
4100         if candidate 
is None: value 
= self
._GetValue
() 
4101         else: value 
= candidate
 
4103         intStr 
= self
._fields
[0]._AdjustField
(value
) 
4104         intStr 
= intStr
.strip() # drop extra spaces 
4105 ##        dbg('adjusted field: "%s"' % intStr) 
4107         if self
._isNeg 
and intStr
.find('-') == -1 and intStr
.find('(') == -1: 
4109                 intStr 
= '(' + intStr 
+ ')' 
4111                 intStr 
= '-' + intStr
 
4112         elif self
._isNeg 
and intStr
.find('-') != -1 and self
._useParens
: 
4113             intStr 
= intStr
.replace('-', '(') 
4115         if( self
._signOk 
and ((self
._useParens 
and intStr
.find('(') == -1) 
4116                                 or (not self
._useParens 
and intStr
.find('-') == -1))): 
4117             intStr 
= ' ' + intStr
 
4119                 intStr 
+= ' '   # space for right paren position 
4121         elif self
._signOk 
and self
._useParens 
and intStr
.find('(') != -1 and intStr
.find(')') == -1: 
4122             # ensure closing right paren: 
4125         if self
._fields
[0]._alignRight
:     ## Only if right-alignment is enabled 
4126             intStr 
= intStr
.rjust( lenInt 
) 
4128             intStr 
= intStr
.ljust( lenInt 
) 
4130         if candidate 
is None: 
4131             wx
.CallAfter(self
._SetValue
, intStr 
) 
4135     def _adjustDate(self
, candidate
=None, fixcentury
=False, force4digit_year
=False): 
4137         'Fixes' a date control, expanding the year if it can. 
4138         Applies various self-formatting options. 
4140 ##        dbg("MaskedEditMixin::_adjustDate", indent=1) 
4141         if candidate 
is None: text    
= self
._GetValue
() 
4142         else: text 
= candidate
 
4143 ##        dbg('text=', text) 
4144         if self
._datestyle 
== "YMD": 
4149 ##        dbg('getYear: "%s"' % getYear(text, self._datestyle)) 
4150         year    
= string
.replace( getYear( text
, self
._datestyle
),self
._fields
[year_field
]._fillChar
,"")  # drop extra fillChars 
4151         month   
= getMonth( text
, self
._datestyle
) 
4152         day     
= getDay( text
, self
._datestyle
) 
4153 ##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) 
4156         yearstart 
= self
._dateExtent 
- 4 
4160                  or (self
._GetInsertionPoint
() > yearstart
+1 and text
[yearstart
+2] == ' ') 
4161                  or (self
._GetInsertionPoint
() > yearstart
+2 and text
[yearstart
+3] == ' ') ) ): 
4162             ## user entered less than four digits and changing fields or past point where we could 
4163             ## enter another digit: 
4167 ##                dbg('bad year=', year) 
4168                 year 
= text
[yearstart
:self
._dateExtent
] 
4170         if len(year
) < 4 and yearVal
: 
4172                 # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the 
4174                 now 
= wx
.DateTime_Now() 
4175                 century 
= (now
.GetYear() /100) * 100        # "this century" 
4176                 twodig_year 
= now
.GetYear() - century       
# "this year" (2 digits) 
4177                 # if separation between today's 2-digit year and typed value > 50, 
4178                 #      assume last century, 
4179                 # else assume this century. 
4181                 # Eg: if 2003 and yearVal == 30, => 2030 
4182                 #     if 2055 and yearVal == 80, => 2080 
4183                 #     if 2010 and yearVal == 96, => 1996 
4185                 if abs(yearVal 
- twodig_year
) > 50: 
4186                     yearVal 
= (century 
- 100) + yearVal
 
4188                     yearVal 
= century 
+ yearVal
 
4189                 year 
= str( yearVal 
) 
4190             else:   # pad with 0's to make a 4-digit year 
4191                 year 
= "%04d" % yearVal
 
4192             if self
._4digityear 
or force4digit_year
: 
4193                 text 
= makeDate(year
, month
, day
, self
._datestyle
, text
) + text
[self
._dateExtent
:] 
4194 ##        dbg('newdate: "%s"' % text, indent=0) 
4198     def _goEnd(self
, getPosOnly
=False): 
4199         """ Moves the insertion point to the end of user-entry """ 
4200 ##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) 
4201         text 
= self
._GetValue
() 
4202 ####        dbg('text: "%s"' % text) 
4204         if len(text
.rstrip()): 
4205             for i 
in range( min( self
._masklength
-1, len(text
.rstrip())), -1, -1): 
4206 ####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) 
4207                 if self
._isMaskChar
(i
): 
4209 ####                    dbg("text[%d]: '%s'" % (i, char)) 
4215             pos 
= self
._goHome
(getPosOnly
=True) 
4217             pos 
= min(i
,self
._masklength
) 
4219         field 
= self
._FindField
(pos
) 
4220         start
, end 
= field
._extent
 
4221         if field
._insertRight 
and pos 
< end
: 
4223 ##        dbg('next pos:', pos) 
4228             self
._SetInsertionPoint
(pos
) 
4231     def _goHome(self
, getPosOnly
=False): 
4232         """ Moves the insertion point to the beginning of user-entry """ 
4233 ##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) 
4234         text 
= self
._GetValue
() 
4235         for i 
in range(self
._masklength
): 
4236             if self
._isMaskChar
(i
): 
4243             self
._SetInsertionPoint
(max(i
,0)) 
4247     def _getAllowedChars(self
, pos
): 
4248         """ Returns a string of all allowed user input characters for the provided 
4249             mask character plus control options 
4251         maskChar 
= self
.maskdict
[pos
] 
4252         okchars 
= self
.maskchardict
[maskChar
]    ## entry, get mask approved characters 
4253         field 
= self
._FindField
(pos
) 
4254         if okchars 
and field
._okSpaces
:          ## Allow spaces? 
4256         if okchars 
and field
._includeChars
:      ## any additional included characters? 
4257             okchars 
+= field
._includeChars
 
4258 ####        dbg('okchars[%d]:' % pos, okchars) 
4262     def _isMaskChar(self
, pos
): 
4263         """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) 
4265         if pos 
< self
._masklength
: 
4266             return self
.ismasked
[pos
] 
4271     def _isTemplateChar(self
,Pos
): 
4272         """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) 
4274         if Pos 
< self
._masklength
: 
4275             return not self
._isMaskChar
(Pos
) 
4280     def _isCharAllowed(self
, char
, pos
, checkRegex
=False, allowAutoSelect
=True, ignoreInsertRight
=False): 
4281         """ Returns True if character is allowed at the specific position, otherwise False.""" 
4282 ##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1) 
4283         field 
= self
._FindField
(pos
) 
4284         right_insert 
= False 
4286         if self
.controlInitialized
: 
4287             sel_start
, sel_to 
= self
._GetSelection
() 
4289             sel_start
, sel_to 
= pos
, pos
 
4291         if (field
._insertRight 
or self
._ctrl
_constraints
._insertRight
) and not ignoreInsertRight
: 
4292             start
, end 
= field
._extent
 
4293             field_len 
= end 
- start
 
4294             if self
.controlInitialized
: 
4295                 value 
= self
._GetValue
() 
4296                 fstr 
= value
[start
:end
].strip() 
4298                     while fstr 
and fstr
[0] == '0': 
4300                 input_len 
= len(fstr
) 
4301                 if self
._signOk 
and '-' in fstr 
or '(' in fstr
: 
4302                     input_len 
-= 1  # sign can move out of field, so don't consider it in length 
4304                 value 
= self
._template
 
4305                 input_len 
= 0   # can't get the current "value", so use 0 
4308             # if entire field is selected or position is at end and field is not full, 
4309             # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar: 
4310             if( (sel_start
, sel_to
) == field
._extent
 
4311                 or (pos 
== end 
and input_len 
< field_len
)): 
4313 ##                dbg('pos = end - 1 = ', pos, 'right_insert? 1') 
4315             elif( field
._allowInsert 
and sel_start 
== sel_to
 
4316                   and (sel_to 
== end 
or (sel_to 
< self
._masklength 
and value
[sel_start
] != field
._fillChar
)) 
4317                   and input_len 
< field_len 
): 
4318                 pos 
= sel_to 
- 1    # where character will go 
4319 ##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') 
4321             # else leave pos alone... 
4323 ##                dbg('pos stays ', pos, 'right_insert? 0') 
4326         if self
._isTemplateChar
( pos 
):  ## if a template character, return empty 
4327 ##            dbg('%d is a template character; returning False' % pos, indent=0) 
4330         if self
._isMaskChar
( pos 
): 
4331             okChars  
= self
._getAllowedChars
(pos
) 
4333             if self
._fields
[0]._groupdigits 
and (self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
)): 
4334                 okChars 
+= self
._fields
[0]._groupChar
 
4337                 if self
._isInt 
or (self
._isFloat 
and pos 
< self
._decimalpos
): 
4341                 elif self
._useParens 
and (self
._isInt 
or (self
._isFloat 
and pos 
> self
._decimalpos
)): 
4344 ####            dbg('%s in %s?' % (char, okChars), char in okChars) 
4345             approved 
= char 
in okChars
 
4347             if approved 
and checkRegex
: 
4348 ##                dbg("checking appropriate regex's") 
4349                 value 
= self
._eraseSelection
(self
._GetValue
()) 
4355                     newvalue
, ignore
, ignore
, ignore
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
, allowAutoSelect
=True) 
4357                     newvalue
, ignore 
= self
._insertKey
(char
, at
, sel_start
, sel_to
, value
) 
4358 ##                dbg('newvalue: "%s"' % newvalue) 
4360                 fields 
= [self
._FindField
(pos
)] + [self
._ctrl
_constraints
] 
4361                 for field 
in fields
:    # includes fields[-1] == "ctrl_constraints" 
4362                     if field
._regexMask 
and field
._filter
: 
4363 ##                        dbg('checking vs. regex') 
4364                         start
, end 
= field
._extent
 
4365                         slice = newvalue
[start
:end
] 
4366                         approved 
= (re
.match( field
._filter
, slice) is not None) 
4367 ##                        dbg('approved?', approved) 
4368                     if not approved
: break 
4372 ##            dbg('%d is a !???! character; returning False', indent=0) 
4376     def _applyFormatting(self
): 
4377         """ Apply formatting depending on the control's state. 
4378             Need to find a way to call this whenever the value changes, in case the control's 
4379             value has been changed or set programatically. 
4382 ##        dbg('MaskedEditMixin::_applyFormatting', indent=1) 
4384         # Handle negative numbers 
4386             text
, signpos
, right_signpos 
= self
._getSignedValue
() 
4387 ##            dbg('text: "%s", signpos:' % text, signpos) 
4388             if not text 
or text
[signpos
] not in ('-','('): 
4390 ##                dbg('no valid sign found; new sign:', self._isNeg) 
4391                 if text 
and signpos 
!= self
._signpos
: 
4392                     self
._signpos 
= signpos
 
4393             elif text 
and self
._valid 
and not self
._isNeg 
and text
[signpos
] in ('-', '('): 
4394 ##                dbg('setting _isNeg to True') 
4396 ##            dbg('self._isNeg:', self._isNeg) 
4398         if self
._signOk 
and self
._isNeg
: 
4399             fc 
= self
._signedForegroundColour
 
4401             fc 
= self
._foregroundColour
 
4403         if hasattr(fc
, '_name'): 
4407 ##        dbg('setting foreground to', c) 
4408         self
.SetForegroundColour(fc
) 
4413                 bc 
= self
._emptyBackgroundColour
 
4415                 bc 
= self
._validBackgroundColour
 
4418             bc 
= self
._invalidBackgroundColour
 
4419         if hasattr(bc
, '_name'): 
4423 ##        dbg('setting background to', c) 
4424         self
.SetBackgroundColour(bc
) 
4426 ##        dbg(indent=0, suspend=0) 
4429     def _getAbsValue(self
, candidate
=None): 
4430         """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). 
4432 ##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) 
4433         if candidate 
is None: text 
= self
._GetValue
() 
4434         else: text 
= candidate
 
4435         right_signpos 
= text
.find(')') 
4438             if self
._ctrl
_constraints
._alignRight 
and self
._fields
[0]._fillChar 
== ' ': 
4439                 signpos 
= text
.find('-') 
4441 ##                    dbg('no - found; searching for (') 
4442                     signpos 
= text
.find('(') 
4444 ##                    dbg('- found at', signpos) 
4448 ##                    dbg('signpos still -1') 
4449 ##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) 
4450                     if len(text
) < self
._masklength
: 
4452                     if len(text
) < self
._masklength
: 
4454                     if len(text
) > self
._masklength 
and text
[-1] in (')', ' '): 
4457 ##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) 
4458 ##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) 
4459                         signpos 
= len(text
) - (len(text
.lstrip()) + 1) 
4461                         if self
._useParens 
and not text
.strip(): 
4462                             signpos 
-= 1    # empty value; use penultimate space 
4463 ##                dbg('signpos:', signpos) 
4465                     text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4470                     text 
= self
._template
[0] + text
[1:] 
4474             if right_signpos 
!= -1: 
4476                     text 
= text
[:right_signpos
] + ' ' + text
[right_signpos
+1:] 
4477                 elif len(text
) > self
._masklength
: 
4478                     text 
= text
[:right_signpos
] + text
[right_signpos
+1:] 
4482             elif self
._useParens 
and self
._signOk
: 
4483                 # figure out where it ought to go: 
4484                 right_signpos 
= self
._masklength 
- 1     # initial guess 
4485                 if not self
._ctrl
_constraints
._alignRight
: 
4486 ##                    dbg('not right-aligned') 
4487                     if len(text
.strip()) == 0: 
4488                         right_signpos 
= signpos 
+ 1 
4489                     elif len(text
.strip()) < self
._masklength
: 
4490                         right_signpos 
= len(text
.rstrip()) 
4491 ##                dbg('right_signpos:', right_signpos) 
4493             groupchar 
= self
._fields
[0]._groupChar
 
4495                 value 
= long(text
.replace(groupchar
,'').replace('(','-').replace(')','').replace(' ', '')) 
4497 ##                dbg('invalid number', indent=0) 
4498                 return None, signpos
, right_signpos
 
4502                 groupchar 
= self
._fields
[0]._groupChar
 
4503                 value 
= float(text
.replace(groupchar
,'').replace(self
._decimalChar
, '.').replace('(', '-').replace(')','').replace(' ', '')) 
4504 ##                dbg('value:', value) 
4508             if value 
< 0 and value 
is not None: 
4509                 signpos 
= text
.find('-') 
4511                     signpos 
= text
.find('(') 
4513                 text 
= text
[:signpos
] + self
._template
[signpos
] + text
[signpos
+1:] 
4515                 # look forwards up to the decimal point for the 1st non-digit 
4516 ##                dbg('decimal pos:', self._decimalpos) 
4517 ##                dbg('text: "%s"' % text) 
4519                     signpos 
= self
._decimalpos 
- (len(text
[:self
._decimalpos
].lstrip()) + 1) 
4520                     # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET 
4521                     if len(text
) >= signpos
+1 and  text
[signpos
+1] in ('-','('): 
4525 ##                dbg('signpos:', signpos) 
4529                     right_signpos 
= self
._masklength 
- 1 
4530                     text 
= text
[:right_signpos
] + ' ' 
4531                     if text
[signpos
] == '(': 
4532                         text 
= text
[:signpos
] + ' ' + text
[signpos
+1:] 
4534                     right_signpos 
= text
.find(')') 
4535                     if right_signpos 
!= -1: 
4540 ##                dbg('invalid number') 
4543 ##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) 
4545         return text
, signpos
, right_signpos
 
4548     def _getSignedValue(self
, candidate
=None): 
4549         """ Return a signed value by adding a "-" prefix if the value 
4550             is set to negative, or a space if positive. 
4552 ##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) 
4553         if candidate 
is None: text 
= self
._GetValue
() 
4554         else: text 
= candidate
 
4557         abstext
, signpos
, right_signpos 
= self
._getAbsValue
(text
) 
4561                 return abstext
, signpos
, right_signpos
 
4563             if self
._isNeg 
or text
[signpos
] in ('-', '('): 
4570             if abstext
[signpos
] not in string
.digits
: 
4571                 text 
= abstext
[:signpos
] + sign 
+ abstext
[signpos
+1:] 
4573                 # this can happen if value passed is too big; sign assumed to be 
4574                 # in position 0, but if already filled with a digit, prepend sign... 
4575                 text 
= sign 
+ abstext
 
4576             if self
._useParens 
and text
.find('(') != -1: 
4577                 text 
= text
[:right_signpos
] + ')' + text
[right_signpos
+1:] 
4580 ##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) 
4582         return text
, signpos
, right_signpos
 
4585     def GetPlainValue(self
, candidate
=None): 
4586         """ Returns control's value stripped of the template text. 
4587             plainvalue = MaskedEditMixin.GetPlainValue() 
4589 ##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) 
4591         if candidate 
is None: text 
= self
._GetValue
() 
4592         else: text 
= candidate
 
4595 ##            dbg('returned ""', indent=0) 
4599             for idx 
in range( min(len(self
._template
), len(text
)) ): 
4600                 if self
._mask
[idx
] in maskchars
: 
4603             if self
._isFloat 
or self
._isInt
: 
4604 ##                dbg('plain so far: "%s"' % plain) 
4605                 plain 
= plain
.replace('(', '-').replace(')', ' ') 
4606 ##                dbg('plain after sign regularization: "%s"' % plain) 
4608                 if self
._signOk 
and self
._isNeg 
and plain
.count('-') == 0: 
4609                     # must be in reserved position; add to "plain value" 
4610                     plain 
= '-' + plain
.strip() 
4612                 if self
._fields
[0]._alignRight
: 
4613                     lpad 
= plain
.count(',') 
4614                     plain 
= ' ' * lpad 
+ plain
.replace(',','') 
4616                     plain 
= plain
.replace(',','') 
4617 ##                dbg('plain after pad and group:"%s"' % plain) 
4619 ##            dbg('returned "%s"' % plain.rstrip(), indent=0) 
4620             return plain
.rstrip() 
4623     def IsEmpty(self
, value
=None): 
4625         Returns True if control is equal to an empty value. 
4626         (Empty means all editable positions in the template == fillChar.) 
4628         if value 
is None: value 
= self
._GetValue
() 
4629         if value 
== self
._template 
and not self
._defaultValue
: 
4630 ####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") 
4631             return True     # (all mask chars == fillChar by defn) 
4632         elif value 
== self
._template
: 
4634             for pos 
in range(len(self
._template
)): 
4635 ####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) 
4636 ####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) 
4637                 if self
._isMaskChar
(pos
) and value
[pos
] not in (' ', self
._fillChar
[pos
]): 
4639 ####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) 
4642 ####            dbg("IsEmpty? 0 (value doesn't match template)") 
4646     def IsDefault(self
, value
=None): 
4648         Returns True if the value specified (or the value of the control if not specified) 
4649         is equal to the default value. 
4651         if value 
is None: value 
= self
._GetValue
() 
4652         return value 
== self
._template
 
4655     def IsValid(self
, value
=None): 
4656         """ Indicates whether the value specified (or the current value of the control 
4657         if not specified) is considered valid.""" 
4658 ####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) 
4659         if value 
is None: value 
= self
._GetValue
() 
4660         ret 
= self
._CheckValid
(value
) 
4665     def _eraseSelection(self
, value
=None, sel_start
=None, sel_to
=None): 
4666         """ Used to blank the selection when inserting a new character. """ 
4667 ##        dbg("MaskedEditMixin::_eraseSelection", indent=1) 
4668         if value 
is None: value 
= self
._GetValue
() 
4669         if sel_start 
is None or sel_to 
is None: 
4670             sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
4671 ##        dbg('value: "%s"' % value) 
4672 ##        dbg("current sel_start, sel_to:", sel_start, sel_to) 
4674         newvalue 
= list(value
) 
4675         for i 
in range(sel_start
, sel_to
): 
4676             if self
._signOk 
and newvalue
[i
] in ('-', '(', ')'): 
4677 ##                dbg('found sign (%s) at' % newvalue[i], i) 
4679                 # balance parentheses: 
4680                 if newvalue
[i
] == '(': 
4681                     right_signpos 
= value
.find(')') 
4682                     if right_signpos 
!= -1: 
4683                         newvalue
[right_signpos
] = ' ' 
4685                 elif newvalue
[i
] == ')': 
4686                     left_signpos 
= value
.find('(') 
4687                     if left_signpos 
!= -1: 
4688                         newvalue
[left_signpos
] = ' ' 
4692             elif self
._isMaskChar
(i
): 
4693                 field 
= self
._FindField
(i
) 
4697                     newvalue
[i
] = self
._template
[i
] 
4699         value 
= string
.join(newvalue
,"") 
4700 ##        dbg('new value: "%s"' % value) 
4705     def _insertKey(self
, char
, pos
, sel_start
, sel_to
, value
, allowAutoSelect
=False): 
4706         """ Handles replacement of the character at the current insertion point.""" 
4707 ##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) 
4709         text 
= self
._eraseSelection
(value
) 
4710         field 
= self
._FindField
(pos
) 
4711         start
, end 
= field
._extent
 
4715         if pos 
!= sel_start 
and sel_start 
== sel_to
: 
4716             # adjustpos must have moved the position; make selection match: 
4717             sel_start 
= sel_to 
= pos
 
4719 ##        dbg('field._insertRight?', field._insertRight) 
4720         if( field
._insertRight                                  
# field allows right insert 
4721             and ((sel_start
, sel_to
) == field
._extent           
# and whole field selected 
4722                  or (sel_start 
== sel_to                        
# or nothing selected 
4723                      and (sel_start 
== end                      
# and cursor at right edge 
4724                           or (field
._allowInsert                
# or field allows right-insert 
4725                               and sel_start 
< end               
# next to other char in field: 
4726                               and text
[sel_start
] != field
._fillChar
) ) ) ) ): 
4727 ##            dbg('insertRight') 
4728             fstr 
= text
[start
:end
] 
4729             erasable_chars 
= [field
._fillChar
, ' '] 
4732                 erasable_chars
.append('0') 
4735 ####            dbg("fstr[0]:'%s'" % fstr[0]) 
4736 ####            dbg('field_index:', field._index) 
4737 ####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) 
4738 ####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", 
4739 ##                 self._signOk and field._index == 0 and fstr[0] in ('-','(')) 
4740             if fstr
[0] in erasable_chars 
or (self
._signOk 
and field
._index 
== 0 and fstr
[0] in ('-','(')): 
4742 ####                dbg('value:      "%s"' % text) 
4743 ####                dbg('fstr:       "%s"' % fstr) 
4744 ####                dbg("erased:     '%s'" % erased) 
4745                 field_sel_start 
= sel_start 
- start
 
4746                 field_sel_to 
= sel_to 
- start
 
4747 ##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start]) 
4748 ##                dbg('right fstr: "%s"' % fstr[field_sel_to:end]) 
4749                 fstr 
= fstr
[1:field_sel_start
] + char 
+ fstr
[field_sel_to
:end
] 
4750             if field
._alignRight 
and sel_start 
!= sel_to
: 
4751                 field_len 
= end 
- start
 
4752 ##                pos += (field_len - len(fstr))    # move cursor right by deleted amount 
4754 ##                dbg('setting pos to:', pos) 
4756                     fstr 
= '0' * (field_len 
- len(fstr
)) + fstr
 
4758                     fstr 
= fstr
.rjust(field_len
)   # adjust the field accordingly 
4759 ##            dbg('field str: "%s"' % fstr) 
4761             newtext 
= text
[:start
] + fstr 
+ text
[end
:] 
4762             if erased 
in ('-', '(') and self
._signOk
: 
4763                 newtext 
= erased 
+ newtext
[1:] 
4764 ##            dbg('newtext: "%s"' % newtext) 
4766             if self
._signOk 
and field
._index 
== 0: 
4767                 start 
-= 1             # account for sign position 
4769 ####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull) 
4770 ####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) 
4771             if( field
._moveOnFieldFull 
and pos 
== end
 
4772                 and len(fstr
.lstrip()) == end
-start
):   # if field now full 
4773                 newpos 
= self
._findNextEntry
(end
)       #   go to next field 
4775                 newpos 
= pos                            
# else keep cursor at current position 
4778 ##            dbg('not newtext') 
4780 ##                dbg('newpos:', newpos) 
4782             if self
._signOk 
and self
._useParens
: 
4783                 old_right_signpos 
= text
.find(')') 
4785             if field
._allowInsert 
and not field
._insertRight 
and sel_to 
<= end 
and sel_start 
>= start
: 
4786                 # inserting within a left-insert-capable field 
4787                 field_len 
= end 
- start
 
4788                 before 
= text
[start
:sel_start
] 
4789                 after 
= text
[sel_to
:end
].strip() 
4790 ####                dbg("current field:'%s'" % text[start:end]) 
4791 ####                dbg("before:'%s'" % before, "after:'%s'" % after) 
4792                 new_len 
= len(before
) + len(after
) + 1 # (for inserted char) 
4793 ####                dbg('new_len:', new_len) 
4795                 if new_len 
< field_len
: 
4796                     retained 
= after 
+ self
._template
[end
-(field_len
-new_len
):end
] 
4797                 elif new_len 
> end
-start
: 
4798                     retained 
= after
[1:] 
4802                 left 
= text
[0:start
] + before
 
4803 ####                dbg("left:'%s'" % left, "retained:'%s'" % retained) 
4804                 right   
= retained 
+ text
[end
:] 
4807                 right   
= text
[pos
+1:] 
4809             newtext 
= left 
+ char 
+ right
 
4811             if self
._signOk 
and self
._useParens
: 
4812                 # Balance parentheses: 
4813                 left_signpos 
= newtext
.find('(') 
4815                 if left_signpos 
== -1:     # erased '('; remove ')' 
4816                     right_signpos 
= newtext
.find(')') 
4817                     if right_signpos 
!= -1: 
4818                         newtext 
= newtext
[:right_signpos
] + ' ' + newtext
[right_signpos
+1:] 
4820                 elif old_right_signpos 
!= -1: 
4821                     right_signpos 
= newtext
.find(')') 
4823                     if right_signpos 
== -1: # just replaced right-paren 
4824                         if newtext
[pos
] == ' ': # we just erased '); erase '(' 
4825                             newtext 
= newtext
[:left_signpos
] + ' ' + newtext
[left_signpos
+1:] 
4826                         else:   # replaced with digit; move ') over 
4827                             if self
._ctrl
_constraints
._alignRight 
or self
._isFloat
: 
4828                                 newtext 
= newtext
[:-1] + ')' 
4830                                 rstripped_text 
= newtext
.rstrip() 
4831                                 right_signpos 
= len(rstripped_text
) 
4832 ##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) 
4833                                 newtext 
= newtext
[:right_signpos
] + ')' + newtext
[right_signpos
+1:] 
4835             if( field
._insertRight                                  
# if insert-right field (but we didn't start at right edge) 
4836                 and field
._moveOnFieldFull                          
# and should move cursor when full 
4837                 and len(newtext
[start
:end
].strip()) == end
-start
):  # and field now full 
4838                 newpos 
= self
._findNextEntry
(end
)                   #   go to next field 
4839 ##                dbg('newpos = nextentry =', newpos) 
4841 ##                dbg('pos:', pos, 'newpos:', pos+1) 
4846             new_select_to 
= newpos     
# (default return values) 
4850             if field
._autoSelect
: 
4851                 match_index
, partial_match 
= self
._autoComplete
(1,  # (always forward) 
4852                                                                 field
._compareChoices
, 
4854                                                                 compareNoCase
=field
._compareNoCase
, 
4855                                                                 current_index 
= field
._autoCompleteIndex
-1) 
4856                 if match_index 
is not None and partial_match
: 
4857                     matched_str 
= newtext
[start
:end
] 
4858                     newtext 
= newtext
[:start
] + field
._choices
[match_index
] + newtext
[end
:] 
4861                     if field
._insertRight
: 
4862                         # adjust position to just after partial match in field 
4863                         newpos 
= end 
- (len(field
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
4865             elif self
._ctrl
_constraints
._autoSelect
: 
4866                 match_index
, partial_match 
= self
._autoComplete
( 
4867                                         1,  # (always forward) 
4868                                         self
._ctrl
_constraints
._compareChoices
, 
4870                                         self
._ctrl
_constraints
._compareNoCase
, 
4871                                         current_index 
= self
._ctrl
_constraints
._autoCompleteIndex 
- 1) 
4872                 if match_index 
is not None and partial_match
: 
4873                     matched_str 
= newtext
 
4874                     newtext 
= self
._ctrl
_constraints
._choices
[match_index
] 
4875                     new_select_to 
= self
._ctrl
_constraints
._extent
[1] 
4876                     match_field 
= self
._ctrl
_constraints
 
4877                     if self
._ctrl
_constraints
._insertRight
: 
4878                         # adjust position to just after partial match in control: 
4879                         newpos 
= self
._masklength 
- (len(self
._ctrl
_constraints
._choices
[match_index
].strip()) - len(matched_str
.strip())) 
4881 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) 
4883             return newtext
, newpos
, new_select_to
, match_field
, match_index
 
4885 ##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos) 
4887             return newtext
, newpos
 
4890     def _OnFocus(self
,event
): 
4892         This event handler is currently necessary to work around new default 
4893         behavior as of wxPython2.3.3; 
4894         The TAB key auto selects the entire contents of the wx.TextCtrl *after* 
4895         the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection 
4896         *here*, because it hasn't happened yet.  So to prevent this behavior, and 
4897         preserve the correct selection when the focus event is not due to tab, 
4898         we need to pull the following trick: 
4900 ##        dbg('MaskedEditMixin::_OnFocus') 
4901         wx
.CallAfter(self
._fixSelection
) 
4906     def _CheckValid(self
, candidate
=None): 
4908         This is the default validation checking routine; It verifies that the 
4909         current value of the control is a "valid value," and has the side 
4910         effect of coloring the control appropriately. 
4913 ##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) 
4914         oldValid 
= self
._valid
 
4915         if candidate 
is None: value 
= self
._GetValue
() 
4916         else: value 
= candidate
 
4917 ##        dbg('value: "%s"' % value) 
4919         valid 
= True    # assume True 
4921         if not self
.IsDefault(value
) and self
._isDate
:                    ## Date type validation 
4922             valid 
= self
._validateDate
(value
) 
4923 ##            dbg("valid date?", valid) 
4925         elif not self
.IsDefault(value
) and self
._isTime
: 
4926             valid 
= self
._validateTime
(value
) 
4927 ##            dbg("valid time?", valid) 
4929         elif not self
.IsDefault(value
) and (self
._isInt 
or self
._isFloat
):  ## Numeric type 
4930             valid 
= self
._validateNumeric
(value
) 
4931 ##            dbg("valid Number?", valid) 
4933         if valid
:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault() 
4934             ## valid so far; ensure also allowed by any list or regex provided: 
4935             valid 
= self
._validateGeneric
(value
) 
4936 ##            dbg("valid value?", valid) 
4938 ##        dbg('valid?', valid) 
4942             self
._applyFormatting
() 
4943             if self
._valid 
!= oldValid
: 
4944 ##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) 
4945 ##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) 
4947 ##        dbg(indent=0, suspend=0) 
4951     def _validateGeneric(self
, candidate
=None): 
4952         """ Validate the current value using the provided list or Regex filter (if any). 
4954         if candidate 
is None: 
4955             text 
= self
._GetValue
() 
4959         valid 
= True    # assume True 
4960         for i 
in [-1] + self
._field
_indices
:   # process global constraints first: 
4961             field 
= self
._fields
[i
] 
4962             start
, end 
= field
._extent
 
4963             slice = text
[start
:end
] 
4964             valid 
= field
.IsValid(slice) 
4971     def _validateNumeric(self
, candidate
=None): 
4972         """ Validate that the value is within the specified range (if specified.)""" 
4973         if candidate 
is None: value 
= self
._GetValue
() 
4974         else: value 
= candidate
 
4976             groupchar 
= self
._fields
[0]._groupChar
 
4978                 number 
= float(value
.replace(groupchar
, '').replace(self
._decimalChar
, '.').replace('(', '-').replace(')', '')) 
4980                 number 
= long( value
.replace(groupchar
, '').replace('(', '-').replace(')', '')) 
4982                     if self
._fields
[0]._alignRight
: 
4983                         require_digit_at 
= self
._fields
[0]._extent
[1]-1 
4985                         require_digit_at 
= self
._fields
[0]._extent
[0] 
4986 ##                    dbg('require_digit_at:', require_digit_at) 
4987 ##                    dbg("value[rda]: '%s'" % value[require_digit_at]) 
4988                     if value
[require_digit_at
] not in list(string
.digits
): 
4992 ##            dbg('number:', number) 
4993             if self
._ctrl
_constraints
._hasRange
: 
4994                 valid 
= self
._ctrl
_constraints
._rangeLow 
<= number 
<= self
._ctrl
_constraints
._rangeHigh
 
4997             groupcharpos 
= value
.rfind(groupchar
) 
4998             if groupcharpos 
!= -1:  # group char present 
4999 ##                dbg('groupchar found at', groupcharpos) 
5000                 if self
._isFloat 
and groupcharpos 
> self
._decimalpos
: 
5001                     # 1st one found on right-hand side is past decimal point 
5002 ##                    dbg('groupchar in fraction; illegal') 
5005                     integer 
= value
[:self
._decimalpos
].strip() 
5007                     integer 
= value
.strip() 
5008 ##                dbg("integer:'%s'" % integer) 
5009                 if integer
[0] in ('-', '('): 
5010                     integer 
= integer
[1:] 
5011                 if integer
[-1] == ')': 
5012                     integer 
= integer
[:-1] 
5014                 parts 
= integer
.split(groupchar
) 
5015 ##                dbg('parts:', parts) 
5016                 for i 
in range(len(parts
)): 
5017                     if i 
== 0 and abs(int(parts
[0])) > 999: 
5018 ##                        dbg('group 0 too long; illegal') 
5021                     elif i 
> 0 and (len(parts
[i
]) != 3 or ' ' in parts
[i
]): 
5022 ##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i])) 
5026 ##            dbg('value not a valid number') 
5031     def _validateDate(self
, candidate
=None): 
5032         """ Validate the current date value using the provided Regex filter. 
5033             Generally used for character types.BufferType 
5035 ##        dbg('MaskedEditMixin::_validateDate', indent=1) 
5036         if candidate 
is None: value 
= self
._GetValue
() 
5037         else: value 
= candidate
 
5038 ##        dbg('value = "%s"' % value) 
5039         text 
= self
._adjustDate
(value
, force4digit_year
=True)     ## Fix the date up before validating it 
5040 ##        dbg('text =', text) 
5041         valid 
= True   # assume True until proven otherwise 
5044             # replace fillChar in each field with space: 
5045             datestr 
= text
[0:self
._dateExtent
] 
5047                 field 
= self
._fields
[i
] 
5048                 start
, end 
= field
._extent
 
5049                 fstr 
= datestr
[start
:end
] 
5050                 fstr
.replace(field
._fillChar
, ' ') 
5051                 datestr 
= datestr
[:start
] + fstr 
+ datestr
[end
:] 
5053             year
, month
, day 
= getDateParts( datestr
, self
._datestyle
) 
5055 ##            dbg('self._dateExtent:', self._dateExtent) 
5056             if self
._dateExtent 
== 11: 
5057                 month 
= charmonths_dict
[month
.lower()] 
5061 ##            dbg('year, month, day:', year, month, day) 
5064 ##            dbg('cannot convert string to integer parts') 
5067 ##            dbg('cannot convert string to integer month') 
5071             # use wxDateTime to unambiguously try to parse the date: 
5072             # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, 
5073             # rather than 1-12, so handle accordingly: 
5079 ##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) 
5080                     dateHandler 
= wx
.DateTimeFromDMY(day
,month
,year
) 
5084 ##                    dbg('cannot convert string to valid date') 
5090                 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5091                 # so we eliminate them here: 
5092                 timeStr     
= text
[self
._dateExtent
+1:].strip()         ## time portion of the string 
5094 ##                    dbg('timeStr: "%s"' % timeStr) 
5096                         checkTime    
= dateHandler
.ParseTime(timeStr
) 
5097                         valid 
= checkTime 
== len(timeStr
) 
5101 ##                        dbg('cannot convert string to valid time') 
5103         if valid
: dbg('valid date') 
5108     def _validateTime(self
, candidate
=None): 
5109         """ Validate the current time value using the provided Regex filter. 
5110             Generally used for character types.BufferType 
5112 ##        dbg('MaskedEditMixin::_validateTime', indent=1) 
5113         # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, 
5114         # so we eliminate them here: 
5115         if candidate 
is None: value 
= self
._GetValue
().strip() 
5116         else: value 
= candidate
.strip() 
5117 ##        dbg('value = "%s"' % value) 
5118         valid 
= True   # assume True until proven otherwise 
5120         dateHandler 
= wx
.DateTime_Today() 
5122             checkTime    
= dateHandler
.ParseTime(value
) 
5123 ##            dbg('checkTime:', checkTime, 'len(value)', len(value)) 
5124             valid 
= checkTime 
== len(value
) 
5129 ##            dbg('cannot convert string to valid time') 
5131         if valid
: dbg('valid time') 
5136     def _OnKillFocus(self
,event
): 
5137         """ Handler for EVT_KILL_FOCUS event. 
5139 ##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) 
5140         if self
._mask 
and self
._IsEditable
(): 
5141             self
._AdjustField
(self
._GetInsertionPoint
()) 
5142             self
._CheckValid
()   ## Call valid handler 
5144         self
._LostFocus
()    ## Provided for subclass use 
5149     def _fixSelection(self
): 
5151         This gets called after the TAB traversal selection is made, if the 
5152         focus event was due to this, but before the EVT_LEFT_* events if 
5153         the focus shift was due to a mouse event. 
5155         The trouble is that, a priori, there's no explicit notification of 
5156         why the focus event we received.  However, the whole reason we need to 
5157         do this is because the default behavior on TAB traveral in a wx.TextCtrl is 
5158         now to select the entire contents of the window, something we don't want. 
5159         So we can *now* test the selection range, and if it's "the whole text" 
5160         we can assume the cause, change the insertion point to the start of 
5161         the control, and deselect. 
5163 ##        dbg('MaskedEditMixin::_fixSelection', indent=1) 
5164         if not self
._mask 
or not self
._IsEditable
(): 
5168         sel_start
, sel_to 
= self
._GetSelection
() 
5169 ##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) 
5171         if( sel_start 
== 0 and sel_to 
>= len( self
._mask 
)   #(can be greater in numeric controls because of reserved space) 
5172             and (not self
._ctrl
_constraints
._autoSelect 
or self
.IsEmpty() or self
.IsDefault() ) ): 
5173             # This isn't normally allowed, and so assume we got here by the new 
5174             # "tab traversal" behavior, so we need to reset the selection 
5175             # and insertion point: 
5176 ##            dbg('entire text selected; resetting selection to start of control') 
5178             field 
= self
._FindField
(self
._GetInsertionPoint
()) 
5179             edit_start
, edit_end 
= field
._extent
 
5180             if field
._selectOnFieldEntry
: 
5181                 self
._SetInsertionPoint
(edit_start
) 
5182                 self
._SetSelection
(edit_start
, edit_end
) 
5184             elif field
._insertRight
: 
5185                 self
._SetInsertionPoint
(edit_end
) 
5186                 self
._SetSelection
(edit_end
, edit_end
) 
5188         elif (self
._isFloat 
or self
._isInt
): 
5190             text
, signpos
, right_signpos 
= self
._getAbsValue
() 
5191             if text 
is None or text 
== self
._template
: 
5192                 integer 
= self
._fields
[0] 
5193                 edit_start
, edit_end 
= integer
._extent
 
5195                 if integer
._selectOnFieldEntry
: 
5196 ##                    dbg('select on field entry:') 
5197                     self
._SetInsertionPoint
(edit_start
) 
5198                     self
._SetSelection
(edit_start
, edit_end
) 
5200                 elif integer
._insertRight
: 
5201 ##                    dbg('moving insertion point to end') 
5202                     self
._SetInsertionPoint
(edit_end
) 
5203                     self
._SetSelection
(edit_end
, edit_end
) 
5205 ##                    dbg('numeric ctrl is empty; start at beginning after sign') 
5206                     self
._SetInsertionPoint
(signpos
+1)   ## Move past minus sign space if signed 
5207                     self
._SetSelection
(signpos
+1, signpos
+1) 
5209         elif sel_start 
> self
._goEnd
(getPosOnly
=True): 
5210 ##            dbg('cursor beyond the end of the user input; go to end of it') 
5213 ##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) 
5218     def _Keypress(self
,key
): 
5219         """ Method provided to override OnChar routine. Return False to force 
5220             a skip of the 'normal' OnChar process. Called before class OnChar. 
5225     def _LostFocus(self
): 
5226         """ Method provided for subclasses. _LostFocus() is called after 
5227             the class processes its EVT_KILL_FOCUS event code. 
5232     def _OnDoubleClick(self
, event
): 
5233         """ selects field under cursor on dclick.""" 
5234         pos 
= self
._GetInsertionPoint
() 
5235         field 
= self
._FindField
(pos
) 
5236         start
, end 
= field
._extent
 
5237         self
._SetInsertionPoint
(start
) 
5238         self
._SetSelection
(start
, end
) 
5242         """ Method provided for subclasses. Called by internal EVT_TEXT 
5243             handler. Return False to override the class handler, True otherwise. 
5250         Used to override the default Cut() method in base controls, instead 
5251         copying the selection to the clipboard and then blanking the selection, 
5252         leaving only the mask in the selected area behind. 
5253         Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the 
5254         derived control because the mixin functions can't override a method of 
5257 ##        dbg("MaskedEditMixin::_Cut", indent=1) 
5258         value 
= self
._GetValue
() 
5259 ##        dbg('current value: "%s"' % value) 
5260         sel_start
, sel_to 
= self
._GetSelection
()                   ## check for a range of selected text 
5261 ##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) 
5262         do 
= wx
.TextDataObject() 
5263         do
.SetText(value
[sel_start
:sel_to
].strip()) 
5264         wx
.TheClipboard
.Open() 
5265         wx
.TheClipboard
.SetData(do
) 
5266         wx
.TheClipboard
.Close() 
5268         if sel_to 
- sel_start 
!= 0: 
5273 # WS Note: overriding Copy is no longer necessary given that you 
5274 # can no longer select beyond the last non-empty char in the control. 
5276 ##    def _Copy( self ): 
5278 ##        Override the wx.TextCtrl's .Copy function, with our own 
5279 ##        that does validation.  Need to strip trailing spaces. 
5281 ##        sel_start, sel_to = self._GetSelection() 
5282 ##        select_len = sel_to - sel_start 
5283 ##        textval = wx.TextCtrl._GetValue(self) 
5285 ##        do = wx.TextDataObject() 
5286 ##        do.SetText(textval[sel_start:sel_to].strip()) 
5287 ##        wx.TheClipboard.Open() 
5288 ##        wx.TheClipboard.SetData(do) 
5289 ##        wx.TheClipboard.Close() 
5292     def _getClipboardContents( self 
): 
5293         """ Subroutine for getting the current contents of the clipboard. 
5295         do 
= wx
.TextDataObject() 
5296         wx
.TheClipboard
.Open() 
5297         success 
= wx
.TheClipboard
.GetData(do
) 
5298         wx
.TheClipboard
.Close() 
5303             # Remove leading and trailing spaces before evaluating contents 
5304             return do
.GetText().strip() 
5307     def _validatePaste(self
, paste_text
, sel_start
, sel_to
, raise_on_invalid
=False): 
5309         Used by paste routine and field choice validation to see 
5310         if a given slice of paste text is legal for the area in question: 
5311         returns validity, replacement text, and extent of paste in 
5315 ##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) 
5316         select_length 
= sel_to 
- sel_start
 
5317         maxlength 
= select_length
 
5318 ##        dbg('sel_to - sel_start:', maxlength) 
5320             maxlength 
= self
._masklength 
- sel_start
 
5324 ##        dbg('maxlength:', maxlength) 
5325         length_considered 
= len(paste_text
) 
5326         if length_considered 
> maxlength
: 
5327 ##            dbg('paste text will not fit into the %s:' % item, indent=0) 
5328             if raise_on_invalid
: 
5329 ##                dbg(indent=0, suspend=0) 
5330                 if item 
== 'control': 
5331                     raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5333                     raise ValueError('"%s" will not fit into the selection' % paste_text
) 
5335 ##                dbg(indent=0, suspend=0) 
5336                 return False, None, None 
5338         text 
= self
._template
 
5339 ##        dbg('length_considered:', length_considered) 
5342         replacement_text 
= "" 
5343         replace_to 
= sel_start
 
5345         while valid_paste 
and i 
< length_considered 
and replace_to 
< self
._masklength
: 
5346             if paste_text
[i
:] == self
._template
[replace_to
:length_considered
]: 
5347                 # remainder of paste matches template; skip char-by-char analysis 
5348 ##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) 
5349                 replacement_text 
+= paste_text
[i
:] 
5350                 replace_to 
= i 
= length_considered
 
5353             char 
= paste_text
[i
] 
5354             field 
= self
._FindField
(replace_to
) 
5355             if not field
._compareNoCase
: 
5356                 if field
._forceupper
:   char 
= char
.upper() 
5357                 elif field
._forcelower
: char 
= char
.lower() 
5359 ##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) 
5360 ##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) 
5361             if not self
._isTemplateChar
(replace_to
) and self
._isCharAllowed
( char
, replace_to
, allowAutoSelect
=False, ignoreInsertRight
=True): 
5362                 replacement_text 
+= char
 
5363 ##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) 
5364 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5367             elif( char 
== self
._template
[replace_to
] 
5368                   or (self
._signOk 
and 
5369                           ( (i 
== 0 and (char 
== '-' or (self
._useParens 
and char 
== '('))) 
5370                             or (i 
== self
._masklength 
- 1 and self
._useParens 
and char 
== ')') ) ) ): 
5371                 replacement_text 
+= char
 
5372 ##                dbg("'%(char)s' == template(%(replace_to)d)" % locals()) 
5373 ##                dbg("replacement_text:", '"'+replacement_text+'"') 
5377                 next_entry 
= self
._findNextEntry
(replace_to
, adjustInsert
=False) 
5378                 if next_entry 
== replace_to
: 
5381                     replacement_text 
+= self
._template
[replace_to
:next_entry
] 
5382 ##                    dbg("skipping template; next_entry =", next_entry) 
5383 ##                    dbg("replacement_text:", '"'+replacement_text+'"') 
5384                     replace_to 
= next_entry  
# so next_entry will be considered on next loop 
5386         if not valid_paste 
and raise_on_invalid
: 
5387 ##            dbg('raising exception', indent=0, suspend=0) 
5388             raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text
, self
.name
)) 
5390         elif i 
< len(paste_text
): 
5392             if raise_on_invalid
: 
5393 ##                dbg('raising exception', indent=0, suspend=0) 
5394                 raise ValueError('"%s" will not fit into the control "%s"' % (paste_text
, self
.name
)) 
5396 ##        dbg('valid_paste?', valid_paste) 
5398 ##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) 
5400 ##        dbg(indent=0, suspend=0) 
5401         return valid_paste
, replacement_text
, replace_to
 
5404     def _Paste( self
, value
=None, raise_on_invalid
=False, just_return_value
=False ): 
5406         Used to override the base control's .Paste() function, 
5407         with our own that does validation. 
5408         Note: _Paste must be called from a Paste() override in the 
5409         derived control because the mixin functions can't override a 
5410         method of a sibling class. 
5412 ##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) 
5414             paste_text 
= self
._getClipboardContents
() 
5418         if paste_text 
is not None: 
5419 ##            dbg('paste text: "%s"' % paste_text) 
5420             # (conversion will raise ValueError if paste isn't legal) 
5421             sel_start
, sel_to 
= self
._GetSelection
() 
5422 ##            dbg('selection:', (sel_start, sel_to)) 
5424             # special case: handle allowInsert fields properly 
5425             field 
= self
._FindField
(sel_start
) 
5426             edit_start
, edit_end 
= field
._extent
 
5428             if field
._allowInsert 
and sel_to 
<= edit_end 
and sel_start 
+ len(paste_text
) < edit_end
: 
5429                 new_pos 
= sel_start 
+ len(paste_text
)   # store for subsequent positioning 
5430                 paste_text 
= paste_text 
+ self
._GetValue
()[sel_to
:edit_end
].rstrip() 
5431 ##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) 
5432                 sel_to 
= sel_start 
+ len(paste_text
) 
5434             # Another special case: paste won't fit, but it's a right-insert field where entire 
5435             # non-empty value is selected, and there's room if the selection is expanded leftward: 
5436             if( len(paste_text
) > sel_to 
- sel_start
 
5437                 and field
._insertRight
 
5438                 and sel_start 
> edit_start
 
5439                 and sel_to 
>= edit_end
 
5440                 and not self
._GetValue
()[edit_start
:sel_start
].strip() ): 
5441                 # text won't fit within selection, but left of selection is empty; 
5442                 # check to see if we can expand selection to accomodate the value: 
5443                 empty_space 
= sel_start 
- edit_start
 
5444                 amount_needed 
= len(paste_text
) - (sel_to 
- sel_start
) 
5445                 if amount_needed 
<= empty_space
: 
5446                     sel_start 
-= amount_needed
 
5447 ##                    dbg('expanded selection to:', (sel_start, sel_to)) 
5450             # another special case: deal with signed values properly: 
5452                 signedvalue
, signpos
, right_signpos 
= self
._getSignedValue
() 
5453                 paste_signpos 
= paste_text
.find('-') 
5454                 if paste_signpos 
== -1: 
5455                     paste_signpos 
= paste_text
.find('(') 
5457                 # if paste text will result in signed value: 
5458 ####                dbg('paste_signpos != -1?', paste_signpos != -1) 
5459 ####                dbg('sel_start:', sel_start, 'signpos:', signpos) 
5460 ####                dbg('field._insertRight?', field._insertRight) 
5461 ####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) 
5462                 if paste_signpos 
!= -1 and (sel_start 
<= signpos
 
5463                                             or (field
._insertRight 
and sel_start 
- len(paste_text
) <= signpos
)): 
5467                 # remove "sign" from paste text, so we can auto-adjust for sign type after paste: 
5468                 paste_text 
= paste_text
.replace('-', ' ').replace('(',' ').replace(')','') 
5469 ##                dbg('unsigned paste text: "%s"' % paste_text) 
5473             # another special case: deal with insert-right fields when selection is empty and 
5474             # cursor is at end of field: 
5475 ####            dbg('field._insertRight?', field._insertRight) 
5476 ####            dbg('sel_start == edit_end?', sel_start == edit_end) 
5477 ####            dbg('sel_start', sel_start, 'sel_to', sel_to) 
5478             if field
._insertRight 
and sel_start 
== edit_end 
and sel_start 
== sel_to
: 
5479                 sel_start 
-= len(paste_text
) 
5482 ##                dbg('adjusted selection:', (sel_start, sel_to)) 
5485                 valid_paste
, replacement_text
, replace_to 
= self
._validatePaste
(paste_text
, sel_start
, sel_to
, raise_on_invalid
) 
5487 ##                dbg('exception thrown', indent=0) 
5491 ##                dbg('paste text not legal for the selection or portion of the control following the cursor;') 
5492                 if not wx
.Validator_IsSilent(): 
5497             text 
= self
._eraseSelection
() 
5499             new_text 
= text
[:sel_start
] + replacement_text 
+ text
[replace_to
:] 
5501                 new_text 
= string
.ljust(new_text
,self
._masklength
) 
5503                 new_text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=new_text
) 
5506                         new_text 
= new_text
[:signpos
] + '(' + new_text
[signpos
+1:right_signpos
] + ')' + new_text
[right_signpos
+1:] 
5508                         new_text 
= new_text
[:signpos
] + '-' + new_text
[signpos
+1:] 
5512 ##            dbg("new_text:", '"'+new_text+'"') 
5514             if not just_return_value
: 
5515                 if new_text 
!= self
._GetValue
(): 
5516                     self
.modified 
= True 
5520                     wx
.CallAfter(self
._SetValue
, new_text
) 
5522                         new_pos 
= sel_start 
+ len(replacement_text
) 
5523                     wx
.CallAfter(self
._SetInsertionPoint
, new_pos
) 
5527         elif just_return_value
: 
5529             return self
._GetValue
() 
5533         """ Provides an Undo() method in base controls. """ 
5534 ##        dbg("MaskedEditMixin::_Undo", indent=1) 
5535         value 
= self
._GetValue
() 
5536         prev 
= self
._prevValue
 
5537 ##        dbg('current value:  "%s"' % value) 
5538 ##        dbg('previous value: "%s"' % prev) 
5540 ##            dbg('no previous value', indent=0) 
5544             # Determine what to select: (relies on fixed-length strings) 
5545             # (This is a lot harder than it would first appear, because 
5546             # of mask chars that stay fixed, and so break up the "diff"...) 
5548             # Determine where they start to differ: 
5550             length 
= len(value
)     # (both are same length in masked control) 
5552             while( value
[:i
] == prev
[:i
] ): 
5557             # handle signed values carefully, so undo from signed to unsigned or vice-versa 
5560                 text
, signpos
, right_signpos 
= self
._getSignedValue
(candidate
=prev
) 
5562                     if prev
[signpos
] == '(' and prev
[right_signpos
] == ')': 
5566                     # eliminate source of "far-end" undo difference if using balanced parens: 
5567                     value 
= value
.replace(')', ' ') 
5568                     prev 
= prev
.replace(')', ' ') 
5569                 elif prev
[signpos
] == '-': 
5574             # Determine where they stop differing in "undo" result: 
5575             sm 
= difflib
.SequenceMatcher(None, a
=value
, b
=prev
) 
5576             i
, j
, k 
= sm
.find_longest_match(sel_start
, length
, sel_start
, length
) 
5577 ##            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] ) 
5579             if k 
== 0:                              # no match found; select to end 
5582                 code_5tuples 
= sm
.get_opcodes() 
5583                 for op
, i1
, i2
, j1
, j2 
in code_5tuples
: 
5584 ##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) 
5588                 # look backward through operations needed to produce "previous" value; 
5589                 # first change wins: 
5590                 for next_op 
in range(len(code_5tuples
)-1, -1, -1): 
5591                     op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5592 ##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) 
5593                     if op 
== 'insert' and prev
[j1
:j2
] != self
._template
[j1
:j2
]: 
5594 ##                        dbg('insert found: selection =>', (j1, j2)) 
5599                     elif op 
== 'delete' and value
[i1
:i2
] != self
._template
[i1
:i2
]: 
5600                         field 
= self
._FindField
(i2
) 
5601                         edit_start
, edit_end 
= field
._extent
 
5602                         if field
._insertRight 
and i2 
== edit_end
: 
5608 ##                        dbg('delete found: selection =>', (sel_start, sel_to)) 
5611                     elif op 
== 'replace': 
5612 ##                        dbg('replace found: selection =>', (j1, j2)) 
5620                     # now go forwards, looking for earlier changes: 
5621                     for next_op 
in range(len(code_5tuples
)): 
5622                         op
, i1
, i2
, j1
, j2 
= code_5tuples
[next_op
] 
5623                         field 
= self
._FindField
(i1
) 
5626                         elif op 
== 'replace': 
5627 ##                            dbg('setting sel_start to', i1) 
5630                         elif op 
== 'insert' and not value
[i1
:i2
]: 
5631 ##                            dbg('forward %s found' % op) 
5632                             if prev
[j1
:j2
].strip(): 
5633 ##                                dbg('item to insert non-empty; setting sel_start to', j1) 
5636                             elif not field
._insertRight
: 
5637 ##                                dbg('setting sel_start to inserted space:', j1) 
5640                         elif op 
== 'delete' and field
._insertRight 
and not value
[i1
:i2
].lstrip(): 
5643                             # we've got what we need 
5648 ##                    dbg('no insert,delete or replace found (!)') 
5649                     # do "left-insert"-centric processing of difference based on l.c.s.: 
5650                     if i 
== j 
and j 
!= sel_start
:         # match starts after start of selection 
5651                         sel_to 
= sel_start 
+ (j
-sel_start
)  # select to start of match 
5653                         sel_to 
= j                          
# (change ends at j) 
5656             # There are several situations where the calculated difference is 
5657             # not what we want to select.  If changing sign, or just adding 
5658             # group characters, we really don't want to highlight the characters 
5659             # changed, but instead leave the cursor where it is. 
5660             # Also, there a situations in which the difference can be ambiguous; 
5663             # current value:    11234 
5664             # previous value:   1111234 
5666             # Where did the cursor actually lie and which 1s were selected on the delete 
5669             # Also, difflib can "get it wrong;" Consider: 
5671             # current value:    "       128.66" 
5672             # previous value:   "       121.86" 
5674             # difflib produces the following opcodes, which are sub-optimal: 
5675             #    equal value[0:9] (       12) prev[0:9] (       12) 
5676             #   insert value[9:9] () prev[9:11] (1.) 
5677             #    equal value[9:10] (8) prev[11:12] (8) 
5678             #   delete value[10:11] (.) prev[12:12] () 
5679             #    equal value[11:12] (6) prev[12:13] (6) 
5680             #   delete value[12:13] (6) prev[13:13] () 
5682             # This should have been: 
5683             #    equal value[0:9] (       12) prev[0:9] (       12) 
5684             #  replace value[9:11] (8.6) prev[9:11] (1.8) 
5685             #    equal value[12:13] (6) prev[12:13] (6) 
5687             # But it didn't figure this out! 
5689             # To get all this right, we use the previous selection recorded to help us... 
5691             if (sel_start
, sel_to
) != self
._prevSelection
: 
5692 ##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) 
5694                 prev_sel_start
, prev_sel_to 
= self
._prevSelection
 
5695                 field 
= self
._FindField
(sel_start
) 
5697                 if self
._signOk 
and (self
._prevValue
[sel_start
] in ('-', '(', ')') 
5698                                      or self
._curValue
[sel_start
] in ('-', '(', ')')): 
5699                     # change of sign; leave cursor alone... 
5700                     sel_start
, sel_to 
= self
._prevSelection
 
5702                 elif field
._groupdigits 
and (self
._curValue
[sel_start
:sel_to
] == field
._groupChar
 
5703                                              or self
._prevValue
[sel_start
:sel_to
] == field
._groupChar
): 
5704                     # do not highlight grouping changes 
5705                     sel_start
, sel_to 
= self
._prevSelection
 
5708                     calc_select_len 
= sel_to 
- sel_start
 
5709                     prev_select_len 
= prev_sel_to 
- prev_sel_start
 
5711 ##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) 
5712 ##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) 
5714                     if prev_select_len 
>= calc_select_len
: 
5715                         # old selection was bigger; trust it: 
5716                         sel_start
, sel_to 
= self
._prevSelection
 
5718                     elif( sel_to 
> prev_sel_to                  
# calculated select past last selection 
5719                           and prev_sel_to 
< len(self
._template
) # and prev_sel_to not at end of control 
5720                           and sel_to 
== len(self
._template
) ):  # and calculated selection goes to end of control 
5722                         i
, j
, k 
= sm
.find_longest_match(prev_sel_to
, length
, prev_sel_to
, length
) 
5723 ##                        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] ) 
5725                             # difflib must not have optimized opcodes properly; 
5729                         # look for possible ambiguous diff: 
5731                         # if last change resulted in no selection, test from resulting cursor position: 
5732                         if prev_sel_start 
== prev_sel_to
: 
5733                             calc_select_len 
= sel_to 
- sel_start
 
5734                             field 
= self
._FindField
(prev_sel_start
) 
5736                             # determine which way to search from last cursor position for ambiguous change: 
5737                             if field
._insertRight
: 
5738                                 test_sel_start 
= prev_sel_start
 
5739                                 test_sel_to 
= prev_sel_start 
+ calc_select_len
 
5741                                 test_sel_start 
= prev_sel_start 
- calc_select_len
 
5742                                 test_sel_to 
= prev_sel_start
 
5744                             test_sel_start
, test_sel_to 
= prev_sel_start
, prev_sel_to
 
5746 ##                        dbg('test selection:', (test_sel_start, test_sel_to)) 
5747 ##                        dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to]) 
5748 ##                        dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to]) 
5750                         # if calculated selection spans characters, and same characters 
5751                         # "before" the previous insertion point are present there as well, 
5752                         # select the ones related to the last known selection instead. 
5753                         if( sel_start 
!= sel_to
 
5754                             and test_sel_to 
< len(self
._template
) 
5755                             and self
._prevValue
[test_sel_start
:test_sel_to
] == self
._prevValue
[sel_start
:sel_to
] ): 
5757                             sel_start
, sel_to 
= test_sel_start
, test_sel_to
 
5759 ##            dbg('sel_start, sel_to:', sel_start, sel_to) 
5760 ##            dbg('previous value: "%s"' % self._prevValue) 
5761             self
._SetValue
(self
._prevValue
) 
5762             self
._SetInsertionPoint
(sel_start
) 
5763             self
._SetSelection
(sel_start
, sel_to
) 
5765 ##            dbg('no difference between previous value') 
5770     def _OnClear(self
, event
): 
5771         """ Provides an action for context menu delete operation """ 
5775     def _OnContextMenu(self
, event
): 
5776 ##        dbg('MaskedEditMixin::OnContextMenu()', indent=1) 
5778         menu
.Append(wx
.ID_UNDO
, "Undo", "") 
5779         menu
.AppendSeparator() 
5780         menu
.Append(wx
.ID_CUT
, "Cut", "") 
5781         menu
.Append(wx
.ID_COPY
, "Copy", "") 
5782         menu
.Append(wx
.ID_PASTE
, "Paste", "") 
5783         menu
.Append(wx
.ID_CLEAR
, "Delete", "") 
5784         menu
.AppendSeparator() 
5785         menu
.Append(wx
.ID_SELECTALL
, "Select All", "") 
5787         wx
.EVT_MENU(menu
, wx
.ID_UNDO
, self
._OnCtrl
_Z
) 
5788         wx
.EVT_MENU(menu
, wx
.ID_CUT
, self
._OnCtrl
_X
) 
5789         wx
.EVT_MENU(menu
, wx
.ID_COPY
, self
._OnCtrl
_C
) 
5790         wx
.EVT_MENU(menu
, wx
.ID_PASTE
, self
._OnCtrl
_V
) 
5791         wx
.EVT_MENU(menu
, wx
.ID_CLEAR
, self
._OnClear
) 
5792         wx
.EVT_MENU(menu
, wx
.ID_SELECTALL
, self
._OnCtrl
_A
) 
5794         # ## WSS: The base control apparently handles 
5795         # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE 
5796         # and wxID_CLEAR menu items even if the menu is one 
5797         # we created.  However, it doesn't do undo properly, 
5798         # so we're keeping track of previous values ourselves. 
5799         # Therefore, we have to override the default update for 
5800         # that item on the menu: 
5801         wx
.EVT_UPDATE_UI(self
, wx
.ID_UNDO
, self
._UndoUpdateUI
) 
5802         self
._contextMenu 
= menu
 
5804         self
.PopupMenu(menu
, event
.GetPosition()) 
5806         self
._contextMenu 
= None 
5809     def _UndoUpdateUI(self
, event
): 
5810         if self
._prevValue 
is None or self
._prevValue 
== self
._curValue
: 
5811             self
._contextMenu
.Enable(wx
.ID_UNDO
, False) 
5813             self
._contextMenu
.Enable(wx
.ID_UNDO
, True) 
5816     def _OnCtrlParametersChanged(self
): 
5818         Overridable function to allow derived classes to take action as a 
5819         result of parameter changes prior to possibly changing the value 
5824  ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
5825 # ## TRICKY BIT: to avoid a ton of boiler-plate, and to 
5826 # ## automate the getter/setter generation for each valid 
5827 # ## control parameter so we never forget to add the 
5828 # ## functions when adding parameters, this loop 
5829 # ## programmatically adds them to the class: 
5830 # ## (This makes it easier for Designers like Boa to 
5831 # ## deal with masked controls.) 
5833 # ## To further complicate matters, this is done with an 
5834 # ## extra level of inheritance, so that "general" classes like 
5835 # ## MaskedTextCtrl can have all possible attributes, 
5836 # ## while derived classes, like TimeCtrl and MaskedNumCtrl 
5837 # ## can prevent exposure of those optional attributes of their base 
5838 # ## class that do not make sense for their derivation.  Therefore, 
5840 # ##    BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) 
5842 # ##    MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). 
5844 # ## This allows us to then derive: 
5845 # ##    MaskedNumCtrl( BaseMaskedTextCtrl ) 
5847 # ## and not have to expose all the same accessor functions for the 
5848 # ## derived control when they don't all make sense for it. 
5850 class MaskedEditAccessorsMixin
: 
5852     # Define the default set of attributes exposed by the most generic masked controls: 
5853     exposed_basectrl_params 
= MaskedEditMixin
.valid_ctrl_params
.keys() + Field
.valid_params
.keys() 
5854     exposed_basectrl_params
.remove('index') 
5855     exposed_basectrl_params
.remove('extent') 
5856     exposed_basectrl_params
.remove('foregroundColour')   # (base class already has this) 
5858     for param 
in exposed_basectrl_params
: 
5859         propname 
= param
[0].upper() + param
[1:] 
5860         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
5861         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
5863         if param.find('Colour
') != -1: 
5864             # add non-british spellings, for backward-compatibility 
5865             propname.replace('Colour
', 'Color
') 
5867             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
5868             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
5873 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
5874 ## these are helper subroutines: 
5876 def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): 
5877     """ addseparators = add separator character every three numerals if True 
5879     fmt0 = fmtstring.split('.') 
5882     val  = origvalue.split('.')[0].strip() 
5883     ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) 
5886     return (ret,len(fmt1)) 
5889 def isDateType( fmtstring ): 
5890     """ Checks the mask and returns True if it fits an allowed 
5891         date or datetime format. 
5893     dateMasks = ("^##/##/####", 
5905     reString  = "|".join(dateMasks) 
5906     filter = re.compile( reString) 
5907     if re.match(filter,fmtstring): return True 
5910 def isTimeType( fmtstring ): 
5911     """ Checks the mask and returns True if it fits an allowed 
5914     reTimeMask = "^##:##(:##)?( (AM|PM))?" 
5915     filter = re.compile( reTimeMask ) 
5916     if re.match(filter,fmtstring): return True 
5920 def isFloatingPoint( fmtstring): 
5921     filter = re.compile("[ ]?[#]+\.[#]+\n") 
5922     if re.match(filter,fmtstring+"\n"): return True 
5926 def isInteger( fmtstring ): 
5927     filter = re.compile("[#]+\n") 
5928     if re.match(filter,fmtstring+"\n"): return True 
5932 def getDateParts( dateStr, dateFmt ): 
5933     if len(dateStr) > 11: clip = dateStr[0:11] 
5934     else:                 clip = dateStr 
5935     if clip[-2] not in string.digits: 
5936         clip = clip[:-1]    # (got part of time; drop it) 
5938     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
5939     slices  = clip.split(dateSep) 
5940     if dateFmt == "MDY": 
5941         y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts 
5942     elif dateFmt == "DMY": 
5943         y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts 
5944     elif dateFmt == "YMD": 
5945         y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts 
5947         y,m,d = None, None, None 
5954 def getDateSepChar(dateStr): 
5955     clip   = dateStr[0:10] 
5956     dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') 
5960 def makeDate( year, month, day, dateFmt, dateStr): 
5961     sep    = getDateSepChar( dateStr) 
5962     if dateFmt == "MDY": 
5963         return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts 
5964     elif dateFmt == "DMY": 
5965         return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts 
5966     elif dateFmt == "YMD": 
5967         return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts 
5972 def getYear(dateStr,dateFmt): 
5973     parts = getDateParts( dateStr, dateFmt) 
5976 def getMonth(dateStr,dateFmt): 
5977     parts = getDateParts( dateStr, dateFmt) 
5980 def getDay(dateStr,dateFmt): 
5981     parts = getDateParts( dateStr, dateFmt) 
5984 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
5985 class test(wx.PySimpleApp): 
5987             from wx.lib.rcsizer import RowColSizer 
5988             self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) 
5989             self.panel = wx.Panel( self.frame, -1) 
5990             self.sizer = RowColSizer() 
5995             id, id1 = wx.NewId(), wx.NewId() 
5996             self.command1  = wx.Button( self.panel, id, "&Close" ) 
5997             self.command2  = wx.Button( self.panel, id1, "&AutoFormats" ) 
5998             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
5999             self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) 
6000             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) 
6001 ##            self.panel.SetDefaultItem(self.command1 ) 
6002             self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) 
6004             self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) 
6005             self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) 
6006             self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) 
6007             self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) 
6008             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) 
6009             self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) 
6012             label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. 
6013 Note that all controls have been auto-sized by including F in the format code. 
6014 Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" 
6015             label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." 
6017             self.label1 = wx.StaticText( self.panel, -1, label) 
6018             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6019             self.label3 = wx.StaticText( self.panel, -1, "Mask Value") 
6020             self.label4 = wx.StaticText( self.panel, -1, "Format") 
6021             self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") 
6022             self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl") 
6023             self.label7 = wx.StaticText( self.panel, -1, label2) 
6024             self.label7.SetForegroundColour("Blue") 
6025             self.label1.SetForegroundColour("Blue") 
6026             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6027             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6028             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6029             self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6030             self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6032             self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) 
6033             self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) 
6034             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6035             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6036             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6037             self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) 
6038             self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) 
6040             # The following list is of the controls for the demo. Feel free to play around with 
6043             #description        mask                    excl format     regexp                              range,list,initial 
6044            ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''), 
6045            ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''), 
6046            ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''), 
6047            ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''), 
6048            ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''), 
6049            ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'), 
6050            ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''), 
6051            ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'), 
6052            ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''), 
6053            ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '), 
6054            ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '), 
6055            ("Month selector",   "XXX",                  "", 'F',        "",                                 (), 
6056                 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), 
6057            ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (), 
6058                 ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") 
6061             for control in controls: 
6062                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6063                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6064                 self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) 
6065                 self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) 
6067                 if control in controls[:]:#-2]: 
6068                     newControl  = MaskedTextCtrl( self.panel, -1, "", 
6070                                                     excludeChars = control[2], 
6071                                                     formatcodes  = control[3], 
6073                                                     validRegex   = control[4], 
6074                                                     validRange   = control[5], 
6075                                                     choices      = control[6], 
6076                                                     defaultValue = control[7], 
6078                     if control[6]: newControl.SetCtrlParameters(choiceRequired = True) 
6080                     newControl = MaskedComboBox(  self.panel, -1, "", 
6081                                                     choices = control[7], 
6082                                                     choiceRequired  = True, 
6084                                                     formatcodes  = control[3], 
6085                                                     excludeChars = control[2], 
6087                                                     validRegex   = control[4], 
6088                                                     validRange   = control[5], 
6090                 self.editList.append( newControl ) 
6092                 self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) 
6095             self.sizer.AddGrowableCol(4) 
6097             self.panel.SetSizer(self.sizer) 
6098             self.panel.SetAutoLayout(1) 
6105         def onClick(self, event): 
6108         def onClickPage(self, event): 
6109             self.page2 = test2(self.frame,-1,"") 
6110             self.page2.Show(True) 
6112         def _onCheck1(self,event): 
6113             """ Set required value on/off """ 
6114             value = event.IsChecked() 
6116                 for control in self.editList: 
6117                     control.SetCtrlParameters(emptyInvalid=True) 
6120                 for control in self.editList: 
6121                     control.SetCtrlParameters(emptyInvalid=False) 
6123             self.panel.Refresh() 
6125         def _onCheck2(self,event): 
6126             """ Highlight empty values""" 
6127             value = event.IsChecked() 
6129                 for control in self.editList: 
6130                     control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') 
6133                 for control in self.editList: 
6134                     control.SetCtrlParameters( emptyBackgroundColour = 'White') 
6136             self.panel.Refresh() 
6139 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6141 class test2(wx.Frame): 
6142         def __init__(self, parent, id, caption): 
6143             wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) 
6144             from wx.lib.rcsizer import RowColSizer 
6145             self.panel = wx.Panel( self, -1) 
6146             self.sizer = RowColSizer() 
6152 All these controls have been created by passing a single parameter, the AutoFormat code. 
6153 The class contains an internal dictionary of types and formats (autoformats). 
6154 To see a great example of validations in action, try entering a bad email address, then tab out.""" 
6156             self.label1 = wx.StaticText( self.panel, -1, label) 
6157             self.label2 = wx.StaticText( self.panel, -1, "Description") 
6158             self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") 
6159             self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control") 
6160             self.label1.SetForegroundColour("Blue") 
6161             self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6162             self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6163             self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) 
6165             self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) 
6166             self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) 
6167             self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) 
6168             self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) 
6170             id, id1 = wx.NewId(), wx.NewId() 
6171             self.command1  = wx.Button( self.panel, id, "&Close") 
6172             self.command2  = wx.Button( self.panel, id1, "&Print Formats") 
6173             self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) 
6174             self.panel.SetDefaultItem(self.command1) 
6175             self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) 
6177             # The following list is of the controls for the demo. Feel free to play around with 
6180            ("Phone No","USPHONEFULLEXT"), 
6181            ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), 
6182            ("US Date MMDDYYYY","USDATEMMDDYYYY/"), 
6183            ("Time (with seconds)","TIMEHHMMSS"), 
6184            ("Military Time\n(without seconds)","24HRTIMEHHMM"), 
6185            ("Social Sec#","USSOCIALSEC"), 
6186            ("Credit Card","CREDITCARD"), 
6187            ("Expiration MM/YY","EXPDATEMMYY"), 
6188            ("Percentage","PERCENT"), 
6189            ("Person's Age","AGE"), 
6190            ("US Zip Code","USZIP"), 
6191            ("US Zip+4","USZIPPLUS4"), 
6192            ("Email Address","EMAIL"), 
6193            ("IP Address", "(derived control IpAddrCtrl)") 
6196             for control in controls: 
6197                 self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) 
6198                 self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) 
6199                 if control in controls[:-1]: 
6200                     self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", 
6201                                                       autoformat  = control[1], 
6203                                 row=rowcount,col=2,flag=wx.ALL,border=5) 
6205                     self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), 
6206                                     row=rowcount,col=2,flag=wx.ALL,border=5) 
6209             self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) 
6210             self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) 
6211             self.sizer.AddGrowableCol(3) 
6213             self.panel.SetSizer(self.sizer) 
6214             self.panel.SetAutoLayout(1) 
6216         def onClick(self, event): 
6219         def onClickPrint(self, event): 
6220             for format in masktags.keys(): 
6221                 sep = "+------------------------+" 
6222                 print "%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) 
6224 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
6226 if __name__ == "__main__": 
6232 ## =================================== 
6234 ## 1. WS: For some reason I don't understand, the control is generating two (2) 
6235 ##      EVT_TEXT events for every one (1) .SetValue() of the underlying control. 
6236 ##      I've been unsuccessful in determining why or in my efforts to make just one 
6237 ##      occur.  So, I've added a hack to save the last seen value from the 
6238 ##      control in the EVT_TEXT handler, and if *different*, call event.Skip() 
6239 ##      to propagate it down the event chain, and let the application see it. 
6241 ## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the 
6242 ##      behavior of the underlying control that I can't fix.  The problems are: 
6243 ##      a) The background coloring doesn't work in the text field of the control; 
6244 ##         instead, there's a only border around it that assumes the correct color. 
6245 ##      b) The control will not pass WXK_TAB to the event handler, no matter what 
6246 ##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to 
6247 ##         indicate that we want these events.  As a result, MaskedComboBox 
6248 ##         doesn't do the nice field-tabbing that MaskedTextCtrl does. 
6249 ##      c) Auto-complete had to be reimplemented for the control because programmatic 
6250 ##         setting of the value of the text field does not set up the auto complete 
6251 ##         the way that the control processing keystrokes does.  (But I think I've 
6252 ##         implemented a fairly decent approximation.)  Because of this the control 
6253 ##         also won't auto-complete on dropdown, and there's no event I can catch 
6254 ##         to work around this problem. 
6255 ##      d) There is no method provided for getting the selection; the hack I've 
6256 ##         implemented has its flaws, not the least of which is that due to the 
6257 ##         strategy that I'm using, the paste buffer is always replaced by the 
6258 ##         contents of the control's selection when in focus, on each keystroke; 
6259 ##         this makes it impossible to paste anything into a MaskedComboBox 
6260 ##         at the moment... :-( 
6261 ##      e) The other deficient behavior, likely induced by the workaround for (d), 
6262 ##         is that you can can't shift-left to select more than one character 
6266 ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their 
6267 ##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in 
6268 ##      wxWindows, shift-tab won't take you backwards through the fields of 
6269 ##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will 
6270 ##      work like shift-tab and tab ought to. 
6274 ## =============================## 
6275 ##  1. Add Popup list for auto-completable fields that simulates combobox on individual 
6276 ##     fields.  Example: City validates against list of cities, or zip vs zip code list. 
6277 ##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" 
6279 ##  3. Fix shift-left selection for MaskedComboBox. 
6280 ##  5. Transform notion of "decimal control" to be less "entire control"-centric, 
6281 ##     so that monetary symbols can be included and still have the appropriate 
6282 ##     semantics.  (Big job, as currently written, but would make control even 
6283 ##     more useful for business applications.) 
6287 ## ==================== 
6289 ##  1. Reorganized masked controls into separate package, renamed things accordingly 
6290 ##  2. Split actual controls out of this file into their own files. 
6292 ##  (Reported) bugs fixed: 
6293 ##   1. Crash ensues if you attempt to change the mask of a read-only 
6294 ##      MaskedComboBox after initial construction. 
6295 ##   2. Changed strategy of defining Get/Set property functions so that 
6296 ##      these are now generated dynamically at runtime, rather than as 
6297 ##      part of the class definition.  (This makes it possible to have 
6298 ##      more general base classes that have many more options for configuration 
6299 ##      without requiring that derivations support the same options.) 
6300 ##   3. Fixed IsModified for _Paste() and _OnErase(). 
6303 ##   1. Fixed "attribute function inheritance," since base control is more 
6304 ##      generic than subsequent derivations, not all property functions of a 
6305 ##      generic control should be exposed in those derivations.  New strategy 
6306 ##      uses base control classes (eg. BaseMaskedTextCtrl) that should be 
6307 ##      used to derive new class types, and mixed with their own mixins to 
6308 ##      only expose those attributes from the generic masked controls that 
6309 ##      make sense for the derivation.  (This makes Boa happier.) 
6310 ##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less 
6314 ##  (Reported) bugs fixed: 
6315 ##   1. Right-click menu allowed "cut" operation that destroyed mask 
6316 ##      (was implemented by base control) 
6317 ##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all 
6318 ##      got converted to lower case. 
6319 ##   3. MaskedComboBox selection didn't deal with spaces in values 
6320 ##      properly when autocompleting, and didn't have a concept of "next" 
6321 ##      match for handling choice list duplicates. 
6322 ##   4. Size of MaskedComboBox was always default. 
6323 ##   5. Email address regexp allowed some "non-standard" things, and wasn't 
6325 ##   6. Couldn't easily reset MaskedComboBox contents programmatically. 
6326 ##   7. Couldn't set emptyInvalid during construction. 
6327 ##   8. Under some versions of wxPython, readonly comboboxes can apparently 
6328 ##      return a GetInsertionPoint() result (655535), causing masked control 
6330 ##   9. Specifying an empty mask caused the controls to traceback. 
6331 ##  10. Can't specify float ranges for validRange. 
6332 ##  11. '.' from within a the static portion of a restricted IP address 
6333 ##      destroyed the mask from that point rightward; tab when cursor is 
6334 ##      before 1st field takes cursor past that field. 
6337 ##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) 
6338 ##  13. Added auto-select option on char input for masked controls with 
6340 ##  14. Added '>' formatcode, allowing insert within a given or each field 
6341 ##      as appropriate, rather than requiring "overwrite".  This makes single 
6342 ##      field controls that just have validation rules (eg. EMAIL) much more 
6343 ##      friendly.  The same flag controls left shift when deleting vs just 
6344 ##      blanking the value, and for right-insert fields, allows right-insert 
6345 ##      at any non-blank (non-sign) position in the field. 
6346 ##  15. Added option to use to indicate negative values for numeric controls. 
6347 ##  16. Improved OnFocus handling of numeric controls. 
6348 ##  17. Enhanced Home/End processing to allow operation on a field level, 
6350 ##  18. Added individual Get/Set functions for control parameters, for 
6351 ##      simplified integration with Boa Constructor. 
6352 ##  19. Standardized "Colour" parameter names to match wxPython, with 
6353 ##      non-british spellings still supported for backward-compatibility. 
6354 ##  20. Added '&' mask specification character for punctuation only (no letters 
6356 ##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide 
6357 ##      unified interface to the masked edit subclasses. 
6361 ##   1. Made it possible to configure grouping, decimal and shift-decimal characters, 
6362 ##      to make controls more usable internationally. 
6363 ##   2. Added code to smart "adjust" value strings presented to .SetValue() 
6364 ##      for right-aligned numeric format controls if they are shorter than 
6365 ##      than the control width,  prepending the missing portion, prepending control 
6366 ##      template left substring for the missing characters, so that setting 
6367 ##      numeric values is easier. 
6368 ##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved 
6369 ##      for b-c), as this makes more sense. 
6372 ##   1. Fixed .SetValue() to replace the current value, rather than the current 
6373 ##      selection. Also changed it to generate ValueError if presented with 
6374 ##      either a value which doesn't follow the format or won't fit.  Also made 
6375 ##      set value adjust numeric and date controls as if user entered the value. 
6376 ##      Expanded doc explaining how SetValue() works. 
6377 ##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to 
6378 ##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. 
6379 ##   3. Made all date autoformats automatically pick implied "datestyle". 
6380 ##   4. Added IsModified override, since base wx.TextCtrl never reports modified if 
6381 ##      .SetValue used to change the value, which is what the masked edit controls 
6383 ##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when 
6384 ##      using tab to "leave field" and auto-adjust. 
6385 ##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, 
6386 ##      and bug in ._Paste() that didn't account for signs in signed masks either. 
6387 ##   7. Fixed issues with _adjustPos for right-insert fields causing improper 
6388 ##      selection/replacement of values 
6389 ##   8. Fixed _OnHome handler to properly handle extending current selection to 
6390 ##      beginning of control. 
6391 ##   9. Exposed all (valid) autoformats to demo, binding descriptions to 
6393 ##  10. Fixed a couple of bugs in email regexp. 
6394 ##  11. Made maskchardict an instance var, to make mask chars to be more 
6395 ##      amenable to international use. 
6396 ##  12. Clarified meaning of '-' formatcode in doc. 
6397 ##  13. Fixed a couple of coding bugs being flagged by Python2.1. 
6398 ##  14. Fixed several issues with sign positioning, erasure and validity 
6399 ##      checking for "numeric" masked controls. 
6400 ##  15. Added validation to IpAddrCtrl.SetValue(). 
6403 ##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) 
6404 ##      vs. literal font facename, and use wxTELETYPE as the font family 
6406 ##   2. Switched to use of dbg module vs. locally defined version. 
6407 ##   3. Revamped entire control structure to use Field classes to hold constraint 
6408 ##      and formatting data, to make code more hierarchical, allow for more 
6409 ##      sophisticated masked edit construction. 
6410 ##   4. Better strategy for managing options, and better validation on keywords. 
6411 ##   5. Added 'V' format code, which requires that in order for a character 
6412 ##      to be accepted, it must result in a string that passes the validRegex. 
6413 ##   6. Added 'S' format code which means "select entire field when navigating 
6415 ##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) 
6416 ##   8. Added '<' format code to allow fields to require explicit cursor movement 
6418 ##   9. Added validFunc option to other validation mechanisms, that allows derived 
6419 ##      classes to add dynamic validation constraints to the control. 
6420 ##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also 
6421 ##      fixed failure to obey case conversion codes when pasting. 
6422 ##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... 
6423 ##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' 
6424 ##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, 
6425 ##      individual field validation to prevent field values > 255, and require explicit 
6426 ##      tab/. to change fields. 
6427 ##  14. Added handler for left double-click to select field under cursor. 
6428 ##  15. Fixed handling for "Read-only" styles. 
6429 ##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor 
6430 ##      attribute, for more consistent and controllable coloring. 
6431 ##  17. Added retainFieldValidation parameter, allowing top-level constraints 
6432 ##      such as "validRequired" to be set independently of field-level equivalent. 
6433 ##      (needed in TimeCtrl for bounds constraints.) 
6434 ##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed 
6435 ##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, 
6437 ##  19. Fixed maskchar setting for upper/lowercase, to work in all locales. 
6441 ##   1. Decimal point behavior restored for decimal and integer type controls: 
6442 ##      decimal point now trucates the portion > 0. 
6443 ##   2. Return key now works like the tab character and moves to the next field, 
6444 ##      provided no default button is set for the form panel on which the control 
6446 ##   3. Support added in _FindField() for subclasses controls (like timecontrol) 
6447 ##      to determine where the current insertion point is within the mask (i.e. 
6448 ##      which sub-'field'). See method documentation for more info and examples. 
6449 ##   4. Added Field class and support for all constraints to be field-specific 
6450 ##      in addition to being globally settable for the control. 
6451 ##      Choices for each field are validated for length and pastability into 
6452 ##      the field in question, raising ValueError if not appropriate for the control. 
6453 ##      Also added selective additional validation based on individual field constraints. 
6454 ##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all 
6455 ##      auto-complete fields with choice lists, supplying the 1st entry in 
6456 ##      the choice list if the field is empty, and cycling through the list in 
6457 ##      the appropriate direction if already a match.  WXK_DOWN will also auto- 
6458 ##      complete if the field is partially completed and a match can be made. 
6459 ##      SHIFT-WXK_UP/DOWN will also take you to the next field after any 
6460 ##      auto-completion performed. 
6461 ##   5. Added autoCompleteKeycodes=[] parameters for allowing further 
6462 ##      customization of the control.  Any keycode supplied as a member 
6463 ##      of the _autoCompleteKeycodes list will be treated like WXK_NEXT.  If 
6464 ##      requireFieldChoice is set, then a valid value from each non-empty 
6465 ##      choice list will be required for the value of the control to validate. 
6466 ##   6. Fixed "auto-sizing" to be relative to the font actually used, rather 
6467 ##      than making assumptions about character width. 
6468 ##   7. Fixed GetMaskParameter(), which was non-functional in previous version. 
6469 ##   8. Fixed exceptions raised to provide info on which control had the error. 
6470 ##   9. Fixed bug in choice management of MaskedComboBox. 
6471 ##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of 
6472 ##     the form '# #'.  Modified control code for IpAddrCtrl so that '.' 
6473 ##     in the middle of a field clips the rest of that field, similar to 
6474 ##     decimal and integer controls. 
6478 ##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. 
6479 ##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). 
6480 ##   3. New support for selecting text within the control.(thanks Will Sadkin!) 
6481 ##      Shift-End and Shift-Home now select text as you would expect 
6482 ##      Control-Shift-End selects to the end of the mask string, even if value not entered. 
6483 ##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control. 
6484 ##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- 
6485 ##      for some reason I couldn't find the original email but thanks!!!) 
6486 ##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome, 
6487 ##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. 
6488 ##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). 
6489 ##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) 
6490 ##   7. New mechanism for replacing default behavior for any given key, using 
6491 ##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available 
6492 ##      for easier subclassing of the control. 
6493 ##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs 
6494 ##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut" 
6495 ##      semantics, erasing the selection, rather than erasing the entire control. 
6496 ##   9. Added option for an "default value" (ie. the template) for use when a single fillChar 
6497 ##      is not desired in every position.  Added IsDefault() function to mean "does the value 
6498 ##      equal the template?" and modified .IsEmpty() to mean "do all of the editable 
6499 ##      positions in the template == the fillChar?" 
6500 ##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, 
6502 ##  11. MaskedComboBox now adds the capability to validate from list of valid values. 
6503 ##      Example: City validates against list of cities, or zip vs zip code list. 
6504 ##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being 
6505 ##      passed to the next handler in the event chain, causing updates to the 
6506 ##      control to be invisible to the parent code. 
6507 ##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within 
6508 ##      the control, that auto-reformats as you move between cells. 
6509 ##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. 
6510 ##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} 
6511 ##  16. Fixed major bugs in date validation, due to the fact that 
6512 ##      wxDateTime.ParseDate is too liberal, and will accept any form that 
6513 ##      makes any kind of sense, regardless of the datestyle you specified 
6514 ##      for the control.  Unfortunately, the strategy used to fix it only 
6515 ##      works for versions of wxPython post 2.3.3.1, as a C++ assert box 
6516 ##      seems to show up on an invalid date otherwise, instead of a catchable 
6518 ##  17. Enhanced date adjustment to automatically adjust heuristic based on 
6519 ##      current year, making last century/this century determination on 
6520 ##      2-digit year based on distance between today's year and value; 
6521 ##      if > 50 year separation, assume last century (and don't assume last 
6522 ##      century is 20th.) 
6523 ##  18. Added autoformats and support for including HHMMSS as well as HHMM for 
6524 ##      date times, and added similar time, and militaray time autoformats. 
6525 ##  19. Enhanced tabbing logic so that tab takes you to the next field if the 
6526 ##      control is a multi-field control. 
6527 ##  20. Added stub method called whenever the control "changes fields", that 
6528 ##      can be overridden by subclasses (eg. IpAddrCtrl.) 
6529 ##  21. Changed a lot of code to be more functionally-oriented so side-effects 
6530 ##      aren't as problematic when maintaining code and/or adding features. 
6531 ##      Eg: IsValid() now does not have side-effects; it merely reflects the 
6532 ##      validity of the value of the control; to determine validity AND recolor 
6533 ##      the control, _CheckValid() should be used with a value argument of None. 
6534 ##      Similarly, made most reformatting function take an optional candidate value 
6535 ##      rather than just using the current value of the control, and only 
6536 ##      have them change the value of the control if a candidate is not specified. 
6537 ##      In this way, you can do validation *before* changing the control. 
6538 ##  22. Changed validRequired to mean "disallow chars that result in invalid 
6539 ##      value."  (Old meaning now represented by emptyInvalid.)  (This was 
6540 ##      possible once I'd made the changes in (19) above.) 
6541 ##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they 
6542 ##      can be set/modified/retrieved after construction.  Removed individual 
6543 ##      parameter setting functions, in favor of this mechanism, so that 
6544 ##      all adjustment of the control based on changing parameter values can 
6545 ##      be handled in one place with unified mechanism. 
6546 ##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability 
6547 ##      to type "grouping char" (ie. ',') and validate as appropriate. 
6548 ##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. 
6549 ##  26. Fixed assumption about "decimal or integer" masks so that they're only 
6550 ##      made iff there's no validRegex associated with the field.  (This 
6551 ##      is so things like zipcodes which look like integers can have more 
6552 ##      restrictive validation (ie. must be 5 digits.) 
6553 ##  27. Added a ton more doc strings to explain use and derivation requirements 
6554 ##      and did regularization of the naming conventions. 
6555 ##  28. Fixed a range bug in _adjustKey preventing z from being handled properly. 
6556 ##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to 
6557 ##      reformat the value and move the next field as appropriate. (shift-'.', 
6558 ##      ie. '>' moves to the previous field. 
6561 ##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending 
6563 ##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the 
6564 ##      year position, the control will expand the value to four digits, using numerals below 
6565 ##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). 
6566 ##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} 
6567 ##   3. revalid parameter renamed validRegex to conform to standard for all validation 
6568 ##      parameters (see 2 new ones below). 
6569 ##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types. 
6570 ##      Allows the developer to specify a valid low/high range of values. 
6571 ##   5. New optional init parameter = validList. Used for character types. Allows developer 
6572 ##      to send a list of values to the control to be used for specific validation. 
6573 ##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams. 
6574 ##   6. Date type fields now use wxDateTime's parser to validate the date and time. 
6575 ##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing 
6576 ##      me toward this solution! 
6577 ##   7. Date fields now automatically expand 2-digit years when it can. For example, 
6578 ##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year 
6579 ##      date is entered it will be expanded in any case when the user tabs out of the 
6581 ##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, 
6582 ##      SetSignedForeColor allow accessto override default class coloring behavior. 
6583 ##   9. Documentation updated and improved. 
6584 ##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. 
6585 ##      Two new options (checkboxes) - test highlight empty and disallow empty. 
6586 ##  11. Home and End now work more intuitively, moving to the first and last user-entry 
6587 ##      value, respectively. 
6588 ##  12. New class function: SetRequired(bool). Sets the control's entry required flag 
6589 ##      (i.e. disallow empty values if True). 
6592 ##   1. get_plainValue method renamed to GetPlainValue following the wxWindows 
6593 ##      StudlyCaps(tm) standard (thanks Paul Moore).  ;) 
6594 ##   2. New format code 'F' causes the control to auto-fit (auto-size) itself 
6595 ##      based on the length of the mask template. 
6596 ##   3. Class now supports "autoformat" codes. These can be passed to the class 
6597 ##      on instantiation using the parameter autoformat="code". If the code is in 
6598 ##      the dictionary, it will self set the mask, formatting, and validation string. 
6599 ##      I have included a number of samples, but I am hoping that someone out there 
6600 ##      can help me to define a whole bunch more. 
6601 ##   4. I have added a second page to the demo (as well as a second demo class, test2) 
6602 ##      to showcase how autoformats work. The way they self-format and self-size is, 
6603 ##      I must say, pretty cool. 
6604 ##   5. Comments added and some internal cosmetic revisions re: matching the code 
6605 ##      standards for class submission. 
6606 ##   6. Regex validation is now done in real time - field turns yellow immediately 
6607 ##      and stays yellow until the entered value is valid 
6608 ##   7. Cursor now skips over template characters in a more intuitive way (before the 
6610 ##   8. Change, Keypress and LostFocus methods added for convenience of subclasses. 
6611 ##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, 
6612 ##      and EVT_KILL_FOCUS, respectively. 
6613 ##   9. Decimal and numeric handlers have been rewritten and now work more intuitively. 
6616 ##   1. New .IsEmpty() method returns True if the control's value is equal to the 
6617 ##      blank template string 
6618 ##   2. Control now supports a new init parameter: revalid. Pass a regular expression 
6619 ##      that the value will have to match when the control loses focus. If invalid, 
6620 ##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next). 
6621 ##   3. Demo now shows revalid functionality. Try entering a partial value, such as a 
6622 ##      partial social security number. 
6623 ##   4. New .IsValid() value returns True if the control is empty, or if the value matches 
6624 ##      the revalid expression. If not, .IsValid() returns False. 
6625 ##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never 
6626 ##      presses the decimal point. 
6627 ##   6. Cursor now goes to the beginning of the field if the user clicks in an 
6628 ##      "empty" field intead of leaving the insertion point in the middle of the 
6630 ##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. 
6631 ##   8. New formatcodes init parameter replaces other init params and adds functions. 
6632 ##      String passed to control on init controls: 
6636 ##        R Show negative #s in red 
6638 ##        - Signed numerals 
6639 ##        0 Numeric fields get leading zeros 
6640 ##   9. Ctrl-X in any field clears the current value. 
6641 ##   10. Code refactored and made more modular (esp in OnChar method). Should be more 
6642 ##       easy to read and understand. 
6643 ##   11. Demo enhanced. 
6644 ##   12. Now has _doc_. 
6647 ##   1. GetPlainValue() now returns the value without the template characters; 
6648 ##      so, for example, a social security number (123-33-1212) would return as 
6649 ##      123331212; also removes white spaces from numeric/decimal values, so 
6650 ##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value. 
6651 ##   2. Press '.' in an integer style masked control and truncate any trailing digits. 
6652 ##   3. Code moderately refactored. Internal names improved for clarity. Additional 
6653 ##      internal documentation. 
6654 ##   4. Home and End keys now supported to move cursor to beginning or end of field. 
6655 ##   5. Un-signed integers and decimals now supported. 
6656 ##   6. Cosmetic improvements to the demo. 
6657 ##   7. Class renamed to MaskedTextCtrl. 
6658 ##   8. Can now specify include characters that will override the basic 
6659 ##      controls: for example, includeChars = "@." for email addresses 
6660 ##   9. Added mask character 'C' -> allow any upper or lowercase character 
6661 ##   10. .SetSignColor(str:color) sets the foreground color for negative values 
6662 ##       in signed controls (defaults to red) 
6663 ##   11. Overview documentation written. 
6666 ##   1. Tab now works properly when pressed in last position 
6667 ##   2. Decimal types now work (e.g. #####.##) 
6668 ##   3. Signed decimal or numeric values supported (i.e. negative numbers) 
6669 ##   4. Negative decimal or numeric values now can show in red. 
6670 ##   5. Can now specify an "exclude list" with the excludeChars parameter. 
6671 ##      See date/time formatted example - you can only enter A or P in the 
6672 ##      character mask space (i.e. AM/PM). 
6673 ##   6. Backspace now works properly, including clearing data from a selected 
6674 ##      region but leaving template characters intact. Also delete key. 
6675 ##   7. Left/right arrows now work properly. 
6676 ##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3