1#----------------------------------------------------------------------------
2# Name:         maskededit.py
3# Authors:      Will Sadkin, Jeff Childers
4# Email:        wsadkin@parlancecorp.com, jchilders_98@yahoo.com
5# Created:      02/11/2003
6# Copyright:    (c) 2003 by Jeff Childers, Will Sadkin, 2003
7# Portions:     (c) 2002 by Will Sadkin, 2002-2007
8#
9# License:      wxWidgets license
10# Tags:         phoenix-port, py3-port
11#----------------------------------------------------------------------------
12# NOTE:
13#   MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
14#   Jason Hihn, and borrows liberally from Will Sadkin's original masked edit
15#   control for time entry, TimeCtrl (which is now rewritten using this
16#   control!).
17#
18#   MaskedEdit controls do not normally use validators, because they do
19#   careful manipulation of the cursor in the text window on each keystroke,
20#   and validation is cursor-position specific, so the control intercepts the
21#   key codes before the validator would fire.  However, validators can be
22#   provided to do data transfer to the controls.
23#
24#----------------------------------------------------------------------------
25#
26# This file now contains the bulk of the logic behind all masked controls,
27# the MaskedEditMixin class, the Field class, and the autoformat codes.
28#
29#----------------------------------------------------------------------------
30#
31# 03/30/2004 - Will Sadkin (wsadkin@parlancecorp.com)
32#
33# o Split out TextCtrl, ComboBox and IpAddrCtrl into their own files,
34# o Reorganized code into masked package
35#
36# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
37#
38# o Updated for wx namespace. No guarantees. This is one huge file.
39#
40# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
41#
42# o Missed wx.DateTime stuff earlier.
43#
44# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
45#
46# o MaskedEditMixin -> MaskedEditMixin
47# o wxMaskedTextCtrl -> maskedTextCtrl
48# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent
49# o wxMaskedComboBox -> MaskedComboBox
50# o wxIpAddrCtrl -> IpAddrCtrl
51# o wxTimeCtrl -> TimeCtrl
52#
53# Tags:        phoenix-port, unittest, documented, py3-port
54#
55
56__doc__ = r"""\
57contains MaskedEditMixin class that drives all the other masked controls.
58
59====================
60Masked Edit Overview
61====================
62
63masked.TextCtrl:
64    is a sublassed text control that can carefully control the user's input
65    based on a mask string you provide.
66
67    General usage example::
68
69        control = masked.TextCtrl( win, -1, '', mask = '(###) ###-####')
70
71    The example above will create a text control that allows only numbers to be
72    entered and then only in the positions indicated in the mask by the # sign.
73
74masked.ComboBox:
75    is a similar subclass of wxComboBox that allows the same sort of masking,
76    but also can do auto-complete of values, and can require the value typed
77    to be in the list of choices to be colored appropriately.
78
79masked.Ctrl:
80    is actually a factory function for several types of masked edit controls:
81
82    =================   ==================================================
83    masked.TextCtrl     standard masked edit text box
84    masked.ComboBox     adds combobox capabilities
85    masked.IpAddrCtrl   adds special semantics for IP address entry
86    masked.TimeCtrl     special subclass handling lots of types as values
87    masked.NumCtrl      special subclass handling numeric values
88    =================   ==================================================
89
90    It works by looking for a *controlType* parameter in the keyword
91    arguments of the control, to determine what kind of instance to return.
92    If not specified as a keyword argument, the default control type returned
93    will be masked.TextCtrl.
94
95    Each of the above classes has its own set of arguments, but masked.Ctrl
96    provides a single "unified" interface for masked controls.
97
98What follows is a description of how to configure the generic masked.TextCtrl
99and masked.ComboBox;  masked.NumCtrl and masked.TimeCtrl have their own demo
100pages and interface descriptions.
101
102=========================
103
104Initialization Parameters
105-------------------------
106mask
107    Allowed mask characters and function:
108
109    =========  ==========================================================
110    Character   Function
111    =========  ==========================================================
112        #       Allow numeric only (0-9)
113        N       Allow letters and numbers (0-9)
114        A       Allow uppercase letters only
115        a       Allow lowercase letters only
116        C       Allow any letter, upper or lower
117        X       Allow string.letters, string.punctuation, string.digits
118        &       Allow string.punctuation only (doesn't include all unicode symbols)
119        \*      Allow any visible character
120        |       explicit field boundary (takes no space in the control; allows mix
121                of adjacent mask characters to be treated as separate fields,
122                eg: '&|###' means "field 0 = '&', field 1 = '###'", but there's
123                no fixed characters in between.
124    =========  ==========================================================
125
126
127    These controls define these sets of characters using string.letters,
128    string.uppercase, etc.  These sets are affected by the system locale
129    setting, so in order to have the masked controls accept characters
130    that are specific to your users' language, your application should
131    set the locale.
132    For example, to allow international characters to be used in the
133    above masks, you can place the following in your code as part of
134    your application's initialization code::
135
136      import locale
137      locale.setlocale(locale.LC_ALL, '')
138
139    The controls now also support (by popular demand) all "visible" characters,
140    by use of the * mask character, including unicode characters above
141    the standard ANSI keycode range.
142    Note:  As string.punctuation doesn't typically include all unicode
143    symbols, you will have to use includechars to get some of these into
144    otherwise restricted positions in your control, such as those specified
145    with &.
146
147  Using these mask characters, a variety of template masks can be built. See
148  the demo for some other common examples include date+time, social security
149  number, etc.  If any of these characters are needed as template rather
150  than mask characters, they can be escaped with \, ie. \N means "literal N".
151  (use \\ for literal backslash, as in: r'CCC\\NNN'.)
152
153
154  .. note::
155
156      Masks containing only # characters and one optional decimal point
157      character are handled specially, as "numeric" controls.  Such
158      controls have special handling for typing the '-' key, handling
159      the "decimal point" character as truncating the integer portion,
160      optionally allowing grouping characters and so forth.
161      There are several parameters and format codes that only make sense
162      when combined with such masks, eg. groupChar, decimalChar, and so
163      forth (see below).  These allow you to construct reasonable
164      numeric entry controls.
165
166
167  .. note::
168
169      Changing the mask for a control deletes any previous field classes
170      (and any associated validation or formatting constraints) for them.
171
172
173useFixedWidthFont
174  By default, masked edit controls use a fixed width font, so that
175  the mask characters are fixed within the control, regardless of
176  subsequent modifications to the value.  Set to False if having
177  the control font be the same as other controls is required. (This is
178  a control-level parameter.)
179
180  .. versionchanged::  2.9.5
181     The default is changed to False for numctrl only
182
183defaultEncoding
184  (Applies to unicode systems only) By default, the default unicode encoding
185  used is latin1, or iso-8859-1.  If necessary, you can set this control-level
186  parameter to govern the codec used to decode your keyboard inputs.
187  (This is a control-level parameter.)
188
189formatcodes
190  These other properties can be passed to the class when instantiating it:
191    Formatcodes are specified as a string of single character formatting
192    codes that modify  behavior of the control::
193
194            _  Allow spaces
195            !  Force upper
196            ^  Force lower
197            R  Right-align field(s)
198            r  Right-insert in field(s) (implies R)
199            <  Stay in field until explicit navigation out of it
200
201            >  Allow insert/delete within partially filled fields (as
202               opposed to the default "overwrite" mode for fixed-width
203               masked edit controls.)  This allows single-field controls
204               or each field within a multi-field control to optionally
205               behave more like standard text controls.
206               (See EMAIL or phone number autoformat examples.)
207
208               *Note: This also governs whether backspace/delete operations
209               shift contents of field to right of cursor, or just blank the
210               erased section.
211
212               Also, when combined with 'r', this indicates that the field
213               or control allows right insert anywhere within the current
214               non-empty value in the field.  (Otherwise right-insert behavior
215               is only performed to when the entire right-insertable field is
216               selected or the cursor is at the right edge of the field.*
217
218
219            ,  Allow grouping character in integer fields of numeric controls
220               and auto-group/regroup digits (if the result fits) when leaving
221               such a field.  (If specified, .SetValue() will attempt to
222               auto-group as well.)
223               ',' is also the default grouping character.  To change the
224               grouping character and/or decimal character, use the groupChar
225               and decimalChar parameters, respectively.
226               Note: typing the "decimal point" character in such fields will
227               clip the value to that left of the cursor for integer
228               fields of controls with "integer" or "floating point" masks.
229               If the ',' format code is specified, this will also cause the
230               resulting digits to be regrouped properly, using the current
231               grouping character.
232            -  Prepend and reserve leading space for sign to mask and allow
233               signed values (negative #s shown in red by default.) Can be
234               used with argument useParensForNegatives (see below.)
235            0  integer fields get leading zeros
236            D  Date[/time] field
237            T  Time field
238            F  Auto-Fit: the control calulates its size from
239               the length of the template mask
240            V  validate entered chars against validRegex before allowing them
241               to be entered vs. being allowed by basic mask and then having
242               the resulting value just colored as invalid.
243               (See USSTATE autoformat demo for how this can be used.)
244            S  select entire field when navigating to new field
245
246fillChar
247
248defaultValue
249  These controls have two options for the initial state of the control.
250  If a blank control with just the non-editable characters showing
251  is desired, simply leave the constructor variable fillChar as its
252  default (' ').  If you want some other character there, simply
253  change the fillChar to that value.  Note: changing the control's fillChar
254  will implicitly reset all of the fields' fillChars to this value.
255
256  If you need different default characters in each mask position,
257  you can specify a defaultValue parameter in the constructor, or
258  set them for each field individually.
259  This value must satisfy the non-editable characters of the mask,
260  but need not conform to the replaceable characters.
261
262groupChar
263
264decimalChar
265  These parameters govern what character is used to group numbers
266  and is used to indicate the decimal point for numeric format controls.
267  The default groupChar is ',', the default decimalChar is '.'
268  By changing these, you can customize the presentation of numbers
269  for your location.
270
271  Eg::
272
273        formatcodes = ',', groupChar='\''                  allows  12'345.34
274        formatcodes = ',', groupChar='.', decimalChar=','  allows  12.345,34
275
276  (These are control-level parameters.)
277
278shiftDecimalChar
279  The default "shiftDecimalChar" (used for "backwards-tabbing" until
280  shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for
281  other keyboards, you may want to customize this, eg '?' for shift ',' on
282  AZERTY keyboards, ':' or ';' for other European keyboards, etc.
283  (This is a control-level parameter.)
284
285useParensForNegatives=False
286  This option can be used with signed numeric format controls to
287  indicate signs via () rather than '-'.
288  (This is a control-level parameter.)
289
290autoSelect=False
291  This option can be used to have a field or the control try to
292  auto-complete on each keystroke if choices have been specified.
293
294autoCompleteKeycodes=[]
295  By default, DownArrow, PageUp and PageDown will auto-complete a
296  partially entered field.  Shift-DownArrow, Shift-UpArrow, PageUp
297  and PageDown will also auto-complete, but if the field already
298  contains a matched value, these keys will cycle through the list
299  of choices forward or backward as appropriate.  Shift-Up and
300  Shift-Down also take you to the next/previous field after any
301  auto-complete action.
302
303  Additional auto-complete keys can be specified via this parameter.
304  Any keys so specified will act like PageDown.
305  (This is a control-level parameter.)
306
307
308
309Validating User Input
310=====================
311There are a variety of initialization parameters that are used to validate
312user input.  These parameters can apply to the control as a whole, and/or
313to individual fields:
314
315========================  ==================================================================
316excludeChars              A string of characters to exclude even if otherwise allowed
317includeChars              A string of characters to allow even if otherwise disallowed
318validRegex                Use a regular expression to validate the contents of the text box
319validRange                Pass a rangeas list (low,high) to limit numeric fields/values
320choices                   A list of strings that are allowed choices for the control.
321choiceRequired            value must be member of choices list
322compareNoCase             Perform case-insensitive matching when validating against list. *Note: for masked.ComboBox, this defaults to True.*
323emptyInvalid              Boolean indicating whether an empty value should be considered invalid
324validFunc                 A function to call of the form: bool = func(candidate_value) which will return True if the candidate_value satisfies some external criteria for the control in addition to the the other validation, or False if not.  (This validation is applied last in the chain of validations.)
325validRequired             Boolean indicating whether or not keys that are allowed by the mask, but result in an invalid value are allowed to be entered into the control.  Setting this to True implies that a valid default value is set for the control.
326retainFieldValidation     False by default; if True, this allows individual fields to retain their own validation constraints independently of any subsequent changes to the control's overall parameters. (This is a control-level parameter.)
327validator                 Validators are not normally needed for masked controls, because of the nature of the validation and control of input.  However, you can supply one to provide data transfer routines for the controls.
328raiseOnInvalidPaste       False by default; normally a bad paste simply is ignored with a bell; if True, this will cause a ValueError exception to be thrown, with the .value attribute of the exception containing the bad value.
329stopFieldChangeIfInvalid  False by default; tries to prevent navigation out of a field if its current value is invalid.  Can be used to create a hybrid of validation settings, allowing intermediate invalid values in a field without sacrificing ability to limit values as with validRequired. NOTE: It is possible to end up with an invalid value when using this option if focus is switched to some other control via mousing. To avoid this, consider deriving a class that defines _LostFocus() function that returns the control to a valid value when the focus shifts.  (AFAICT, The change in focus is unpreventable.)
330========================  ==================================================================
331
332
333Coloring Behavior
334=================
335
336The following parameters have been provided to allow you to change the default
337coloring behavior of the control.   These can be set at construction, or via
338the .SetCtrlParameters() function.  Pass a color as string e.g. 'Yellow':
339
340========================  =======================================================================
341emptyBackgroundColour     Control Background color when identified as empty. Default=White
342invalidBackgroundColour   Control Background color when identified as Not valid. Default=Yellow
343validBackgroundColour     Control Background color when identified as Valid. Default=white
344========================  =======================================================================
345
346
347The following parameters control the default foreground color coloring behavior of the
348control. Pass a color as string e.g. 'Yellow':
349
350========================  ======================================================================
351foregroundColour          Control foreground color when value is not negative.  Default=Black
352signedForegroundColour    Control foreground color when value is negative. Default=Red
353========================  ======================================================================
354
355
356Fields
357======
358
359Each part of the mask that allows user input is considered a field.  The fields
360are represented by their own class instances.  You can specify field-specific
361constraints by constructing or accessing the field instances for the control
362and then specifying those constraints via parameters.
363
364fields
365  This parameter allows you to specify Field instances containing
366  constraints for the individual fields of a control, eg: local
367  choice lists, validation rules, functions, regexps, etc.
368  It can be either an ordered list or a dictionary.  If a list,
369  the fields will be applied as fields 0, 1, 2, etc.
370  If a dictionary, it should be keyed by field index.
371  the values should be a instances of maskededit.Field.
372
373  Any field not represented by the list or dictionary will be
374  implicitly created by the control.
375
376  Eg::
377
378    fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ]
379
380  Or::
381
382    fields = {
383               1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']),
384               3: ( Field(choices=['01', '02', '03'], choiceRequired=True)
385             }
386
387  The following parameters are available for individual fields, with the
388  same semantics as for the whole control but applied to the field in question:
389
390    ==============  =============================================================================
391    fillChar        if set for a field, it will override the control's fillChar for that field
392    groupChar       if set for a field, it will override the control's default
393    defaultValue    sets field-specific default value; overrides any default from control
394    compareNoCase   overrides control's settings
395    emptyInvalid    determines whether field is required to be filled at all times
396    validRequired   if set, requires field to contain valid value
397    ==============  =============================================================================
398
399  If any of the above parameters are subsequently specified for the control as a
400  whole, that new value will be propagated to each field, unless the
401  retainFieldValidation control-level parameter is set.
402
403    ==============  ==============================
404    formatcodes      Augments control's settings
405    excludeChars         '       '        '
406    includeChars         '       '        '
407    validRegex           '       '        '
408    validRange           '       '        '
409    choices              '       '        '
410    choiceRequired       '       '        '
411    validFunc            '       '        '
412    ==============  ==============================
413
414
415
416Control Class Functions
417=======================
418
419.GetPlainValue(value=None)
420                    Returns the value specified (or the control's text value
421                    not specified) without the formatting text.
422                    In the example above, might return phone no='3522640075',
423                    whereas control.GetValue() would return '(352) 264-0075'
424.ClearValue()
425                    Returns the control's value to its default, and places the
426                    cursor at the beginning of the control.
427.SetValue()
428                    Does "smart replacement" of passed value into the control, as does
429                    the .Paste() method.  As with other text entry controls, the
430                    .SetValue() text replacement begins at left-edge of the control,
431                    with missing mask characters inserted as appropriate.
432                    .SetValue will also adjust integer, float or date mask entry values,
433                    adding commas, auto-completing years, etc. as appropriate.
434                    For "right-aligned" numeric controls, it will also now automatically
435                    right-adjust any value whose length is less than the width of the
436                    control before attempting to set the value.
437                    If a value does not follow the format of the control's mask, or will
438                    not fit into the control, a ValueError exception will be raised.
439
440                    Eg::
441
442                      mask = '(###) ###-####'
443                          .SetValue('1234567890')           => '(123) 456-7890'
444                          .SetValue('(123)4567890')         => '(123) 456-7890'
445                          .SetValue('(123)456-7890')        => '(123) 456-7890'
446                          .SetValue('123/4567-890')         => illegal paste; ValueError
447
448                      mask = '#{6}.#{2}', formatcodes = '_,-',
449                          .SetValue('111')                  => ' 111   .  '
450                          .SetValue(' %9.2f' % -111.12345 ) => '   -111.12'
451                          .SetValue(' %9.2f' % 1234.00 )    => '  1,234.00'
452                          .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError
453
454                      mask = '#{6}.#{2}', formatcodes = '_,-R'  # will right-adjust value for right-aligned control
455                          .SetValue('111')                  => padded value misalignment ValueError: "       111" will not fit
456                          .SetValue('%.2f' % 111 )          => '    111.00'
457                          .SetValue('%.2f' % -111.12345 )   => '   -111.12'
458
459
460.IsValid(value=None)
461                    Returns True if the value specified (or the value of the control
462                    if not specified) passes validation tests
463.IsEmpty(value=None)
464                    Returns True if the value specified (or the value of the control
465                    if not specified) is equal to an "empty value," ie. all
466                    editable characters == the fillChar for their respective fields.
467.IsDefault(value=None)
468                    Returns True if the value specified (or the value of the control
469                    if not specified) is equal to the initial value of the control.
470
471.Refresh()
472                    Recolors the control as appropriate to its current settings.
473
474.SetCtrlParameters(\*\*kwargs)
475                    This function allows you to set up and/or change the control parameters
476                    after construction; it takes a list of key/value pairs as arguments,
477                    where the keys can be any of the mask-specific parameters in the constructor.
478
479                    Eg::
480
481                        ctl = masked.TextCtrl( self, -1 )
482                        ctl.SetCtrlParameters( mask='###-####',
483                                               defaultValue='555-1212',
484                                               formatcodes='F')
485
486.GetCtrlParameter(parametername)
487                    This function allows you to retrieve the current value of a parameter
488                    from the control.
489
490  *Note:* Each of the control parameters can also be set using its
491      own Set and Get function.  These functions follow a regular form:
492      All of the parameter names start with lower case; for their
493      corresponding Set/Get function, the parameter name is capitalized.
494
495    Eg::
496
497          ctl.SetMask('###-####')
498          ctl.SetDefaultValue('555-1212')
499          ctl.GetChoiceRequired()
500          ctl.GetFormatcodes()
501
502  *Note:* After any change in parameters, the choices for the
503      control are reevaluated to ensure that they are still legal.  If you
504      have large choice lists, it is therefore more efficient to set parameters
505      before setting the choices available.
506
507.SetFieldParameters(field_index, \*\*kwargs)
508                    This function allows you to specify change individual field
509                    parameters after construction. (Indices are 0-based.)
510
511.GetFieldParameter(field_index, parametername)
512                    Allows the retrieval of field parameters after construction
513
514
515The control detects certain common constructions. In order to use the signed feature
516(negative numbers and coloring), the mask has to be all numbers with optionally one
517decimal point. Without a decimal (e.g. '######', the control will treat it as an integer
518value. With a decimal (e.g. '###.##'), the control will act as a floating point control
519(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
520integer control truncates the value.  However, for a true numeric control,
521masked.NumCtrl provides all this, and true numeric input/output support as well.
522
523
524Check your controls by calling each control's .IsValid() function and the
525.IsEmpty() function to determine which controls have been a) filled in and
526b) filled in properly.
527
528
529Regular expression validations can be used flexibly and creatively.
530Take a look at the demo; the zip-code validation succeeds as long as the
531first five numerals are entered. the last four are optional, but if
532any are entered, there must be 4 to be valid.
533
534masked.Ctrl Configuration
535=========================
536masked.Ctrl works by looking for a special *controlType*
537parameter in the variable arguments of the control, to determine
538what kind of instance to return.
539controlType can be one of::
540
541    controlTypes.TEXT
542    controlTypes.COMBO
543    controlTypes.IPADDR
544    controlTypes.TIME
545    controlTypes.NUMBER
546
547These constants are also available individually, ie, you can
548use either of the following::
549
550    from wx.lib.masked import MaskedCtrl, controlTypes
551    from wx.lib.masked import MaskedCtrl, COMBO, TEXT, NUMBER, IPADDR
552
553If not specified as a keyword argument, the default controlType is
554controlTypes.TEXT.
555
556"""
557
558"""
559+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
560DEVELOPER COMMENTS:
561
562Naming Conventions
563------------------
564  All methods of the Mixin that are not meant to be exposed to the external
565  interface are prefaced with '_'.  Those functions that are primarily
566  intended to be internal subroutines subsequently start with a lower-case
567  letter; those that are primarily intended to be used and/or overridden
568  by derived subclasses start with a capital letter.
569
570  The following methods must be used and/or defined when deriving a control
571  from MaskedEditMixin.  NOTE: if deriving from a *masked edit* control
572  (eg. class IpAddrCtrl(masked.TextCtrl) ), then this is NOT necessary,
573  as it's already been done for you in the base class.
574
575        ._SetInitialValue()
576                        This function must be called after the associated base
577                        control has been initialized in the subclass __init__
578                        function.  It sets the initial value of the control,
579                        either to the value specified if non-empty, the
580                        default value if specified, or the "template" for
581                        the empty control as necessary.  It will also set/reset
582                        the font if necessary and apply formatting to the
583                        control at this time.
584
585        ._GetSelection()
586                        REQUIRED
587                        Each class derived from MaskedEditMixin must define
588                        the function for getting the start and end of the
589                        current text selection.  The reason for this is
590                        that not all controls have the same function name for
591                        doing this; eg. wx.TextCtrl uses .GetSelection(),
592                        whereas we had to write a .GetMark() function for
593                        wxComboBox, because .GetSelection() for the control
594                        gets the currently selected list item from the combo
595                        box, and the control doesn't (yet) natively provide
596                        a means of determining the text selection.
597        ._SetSelection()
598                        REQUIRED
599                        Similarly to _GetSelection, each class derived from
600                        MaskedEditMixin must define the function for setting
601                        the start and end of the current text selection.
602                        (eg. .SetSelection() for masked.TextCtrl, and .SetMark() for
603                        masked.ComboBox.
604
605        ._GetInsertionPoint()
606        ._SetInsertionPoint()
607                        REQUIRED
608                        For consistency, and because the mixin shouldn't rely
609                        on fixed names for any manipulations it does of any of
610                        the base controls, we require each class derived from
611                        MaskedEditMixin to define these functions as well.
612
613        ._GetValue()
614        ._SetValue()    REQUIRED
615                        Each class derived from MaskedEditMixin must define
616                        the functions used to get and set the raw value of the
617                        control.
618                        This is necessary so that recursion doesn't take place
619                        when setting the value, and so that the mixin can
620                        call the appropriate function after doing all its
621                        validation and manipulation without knowing what kind
622                        of base control it was mixed in with.  To handle undo
623                        functionality, the ._SetValue() must record the current
624                        selection prior to setting the value.
625
626        .Cut()
627        .Paste()
628        .Undo()
629        .SetValue()     REQUIRED
630                        Each class derived from MaskedEditMixin must redefine
631                        these functions to call the _Cut(), _Paste(), _Undo()
632                        and _SetValue() methods, respectively for the control,
633                        so as to prevent programmatic corruption of the control's
634                        value.  This must be done in each derivation, as the
635                        mixin cannot itself override a member of a sibling class.
636
637        ._Refresh()     REQUIRED
638                        Each class derived from MaskedEditMixin must define
639                        the function used to refresh the base control.
640
641        .Refresh()      REQUIRED
642                        Each class derived from MaskedEditMixin must redefine
643                        this function so that it checks the validity of the
644                        control (via self._CheckValid) and then refreshes
645                        control using the base class method.
646
647        ._IsEditable()  REQUIRED
648                        Each class derived from MaskedEditMixin must define
649                        the function used to determine if the base control is
650                        editable or not.  (For masked.ComboBox, this has to
651                        be done with code, rather than specifying the proper
652                        function in the base control, as there isn't one...)
653        ._CalcSize()    REQUIRED
654                        Each class derived from MaskedEditMixin must define
655                        the function used to determine how wide the control
656                        should be given the mask.  (The mixin function
657                        ._calcSize() provides a baseline estimate.)
658
659
660Event Handling
661--------------
662  Event handlers are "chained", and MaskedEditMixin usually
663  swallows most of the events it sees, thereby preventing any other
664  handlers from firing in the chain.  It is therefore required that
665  each class derivation using the mixin to have an option to hook up
666  the event handlers itself or forego this operation and let a
667  subclass of the masked control do so.  For this reason, each
668  subclass should probably include the following code::
669
670    if setupEventHandling:
671        ## Setup event handlers
672        EVT_SET_FOCUS( self, self._OnFocus )        ## defeat automatic full selection
673        EVT_KILL_FOCUS( self, self._OnKillFocus )   ## run internal validator
674        EVT_LEFT_DCLICK(self, self._OnDoubleClick)  ## select field under cursor on dclick
675        EVT_RIGHT_UP(self, self._OnContextMenu )    ## bring up an appropriate context menu
676        EVT_KEY_DOWN( self, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab.
677        EVT_CHAR( self, self._OnChar )              ## handle each keypress
678        EVT_TEXT( self, self.GetId(), self._OnTextChange )  ## color control appropriately & keep
679                                                            ## track of previous value for undo
680
681  where setupEventHandling is an argument to its constructor.
682
683  These 5 handlers must be "wired up" for the masked edit
684  controls to provide default behavior.  (The setupEventHandling
685  is an argument to masked.TextCtrl and masked.ComboBox, so
686  that controls derived from *them* may replace one of these
687  handlers if they so choose.)
688
689  If your derived control wants to preprocess events before
690  taking action, it should then set up the event handling itself,
691  so it can be first in the event handler chain.
692
693
694  The following routines are available to facilitate changing
695  the default behavior of masked edit controls:
696
697        ._SetKeycodeHandler(keycode, func)
698        ._SetKeyHandler(char, func)
699                        Use to replace default handling for any given keycode.
700                        func should take the key event as argument and return
701                        False if no further action is required to handle the
702                        key. Eg:
703                            self._SetKeycodeHandler(WXK_UP, self.IncrementValue)
704                            self._SetKeyHandler('-', self._OnChangeSign)
705
706        (Setting a func of None removes any keyhandler for the given key.)
707
708        "Navigation" keys are assumed to change the cursor position, and
709        therefore don't cause automatic motion of the cursor as insertable
710        characters do.
711
712        ._AddNavKeycode(keycode, handler=None)
713        ._AddNavKey(char, handler=None)
714                        Allows controls to specify other keys (and optional handlers)
715                        to be treated as navigational characters. (eg. '.' in IpAddrCtrl)
716
717        ._GetNavKeycodes()  Returns the current list of navigational keycodes.
718
719        ._SetNavKeycodes(key_func_tuples)
720                        Allows replacement of the current list of keycode
721                        processed as navigation keys, and bind associated
722                        optional keyhandlers. argument is a list of key/handler
723                        tuples.  Passing a value of None for the handler in a
724                        given tuple indicates that default processing for the key
725                        is desired.
726
727        ._FindField(pos) Returns the Field object associated with this position
728                        in the control.
729
730        ._FindFieldExtent(pos, getslice=False, value=None)
731                        Returns edit_start, edit_end of the field corresponding
732                        to the specified position within the control, and
733                        optionally also returns the current contents of that field.
734                        If value is specified, it will retrieve the slice the corresponding
735                        slice from that value, rather than the current value of the
736                        control.
737
738        ._AdjustField(pos)
739                        This is, the function that gets called for a given position
740                        whenever the cursor is adjusted to leave a given field.
741                        By default, it adjusts the year in date fields if mask is a date,
742                        It can be overridden by a derived class to
743                        adjust the value of the control at that time.
744                        (eg. IpAddrCtrl reformats the address in this way.)
745
746        ._Change()      Called by internal EVT_TEXT handler. Return False to force
747                        skip of the normal class change event.
748        ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force
749                        skip of the normal class keypress event.
750        ._LostFocus()   Called by internal EVT_KILL_FOCUS handler
751
752        ._OnKeyDown(event)
753                        This is the default EVT_KEY_DOWN routine; it just checks for
754                        "navigation keys", and if event.ControlDown(), it fires the
755                        mixin's _OnChar() routine, as such events are not always seen
756                        by the "cooked" EVT_CHAR routine.
757
758        ._OnChar(event) This is the main EVT_CHAR handler for the
759                        MaskedEditMixin.
760
761    The following routines are used to handle standard actions
762    for control keys:
763        _OnArrow(event)         used for arrow navigation events
764        _OnCtrl_A(event)        'select all'
765        _OnCtrl_C(event)        'copy' (uses base control function, as copy is non-destructive)
766        _OnCtrl_S(event)        'save' (does nothing)
767        _OnCtrl_V(event)        'paste' - calls _Paste() method, to do smart paste
768        _OnCtrl_X(event)        'cut'   - calls _Cut() method, to "erase" selection
769        _OnCtrl_Z(event)        'undo'  - resets value to previous value (if any)
770
771        _OnChangeField(event)   primarily used for tab events, but can be
772                                used for other keys (eg. '.' in IpAddrCtrl)
773
774        _OnErase(event)         used for backspace and delete
775        _OnHome(event)
776        _OnEnd(event)
777
778    The following routine provides a hook back to any class derivations, so that
779    they can react to parameter changes before any value is set/reset as a result of
780    those changes.  (eg. masked.ComboBox needs to detect when the choices list is
781    modified, either implicitly or explicitly, so it can reset the base control
782    to have the appropriate choice list *before* the initial value is reset to match.)
783
784        _OnCtrlParametersChanged()
785
786Accessor Functions
787------------------
788    For convenience, each class derived from MaskedEditMixin should
789    define an accessors mixin, so that it exposes only those parameters
790    that make sense for the derivation.  This is done with an intermediate
791    level of inheritance, ie:
792
793    class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ):
794
795    class TextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
796    class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
797    class NumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ):
798    class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
799    class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ):
800
801    etc.
802
803    Each accessors mixin defines Get/Set functions for the base class parameters
804    that are appropriate for that derivation.
805    This allows the base classes to be "more generic," exposing the widest
806    set of options, while not requiring derived classes to be so general.
807"""
808
809import  copy
810import  difflib
811import  re
812import  string
813import  sys
814
815import  wx
816import  six
817
818# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
819# be a good place to implement the 2.3 logger class
820from wx.tools.dbg import Logger
821
822##dbg = Logger()
823##dbg(enable=1)
824
825## ---------- ---------- ---------- ---------- ---------- ---------- ----------
826
827## Constants for identifying control keys and classes of keys:
828
829WXK_CTRL_A = (ord('A')+1) - ord('A')   ## These keys are not already defined in wx
830WXK_CTRL_C = (ord('C')+1) - ord('A')
831WXK_CTRL_S = (ord('S')+1) - ord('A')
832WXK_CTRL_V = (ord('V')+1) - ord('A')
833WXK_CTRL_X = (ord('X')+1) - ord('A')
834WXK_CTRL_Z = (ord('Z')+1) - ord('A')
835
836nav = (
837    wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB,
838    wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN,
839    wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN,
840    wx.WXK_NUMPAD_HOME, wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_PAGEDOWN
841    )
842
843control = (
844    wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_INSERT,
845    wx.WXK_NUMPAD_DELETE, wx.WXK_NUMPAD_INSERT,
846    WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V,
847    WXK_CTRL_X, WXK_CTRL_Z
848    )
849
850# Because unicode can go over the ansi character range, we need to explicitly test
851# for all non-visible keystrokes, rather than just assuming a particular range for
852# visible characters:
853wx_control_keycodes = list(range(32)) + list(nav) + list(control) + [
854    wx.WXK_START, wx.WXK_LBUTTON, wx.WXK_RBUTTON, wx.WXK_CANCEL, wx.WXK_MBUTTON,
855    wx.WXK_CLEAR, wx.WXK_SHIFT, wx.WXK_CONTROL, wx.WXK_MENU, wx.WXK_PAUSE,
856    wx.WXK_CAPITAL, wx.WXK_SELECT, wx.WXK_PRINT, wx.WXK_EXECUTE, wx.WXK_SNAPSHOT,
857    wx.WXK_HELP, wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3,
858    wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, wx.WXK_NUMPAD8,
859    wx.WXK_NUMPAD9, wx.WXK_MULTIPLY, wx.WXK_ADD, wx.WXK_SEPARATOR, wx.WXK_SUBTRACT,
860    wx.WXK_DECIMAL, wx.WXK_DIVIDE, wx.WXK_F1, wx.WXK_F2, wx.WXK_F3, wx.WXK_F4,
861    wx.WXK_F5, wx.WXK_F6, wx.WXK_F7, wx.WXK_F8, wx.WXK_F9, wx.WXK_F10, wx.WXK_F11,
862    wx.WXK_F12, wx.WXK_F13, wx.WXK_F14, wx.WXK_F15, wx.WXK_F16, wx.WXK_F17,
863    wx.WXK_F18, wx.WXK_F19, wx.WXK_F20, wx.WXK_F21, wx.WXK_F22, wx.WXK_F23,
864    wx.WXK_F24, wx.WXK_NUMLOCK, wx.WXK_SCROLL, wx.WXK_PAGEUP, wx.WXK_PAGEDOWN,
865    wx.WXK_NUMPAD_SPACE, wx.WXK_NUMPAD_TAB, wx.WXK_NUMPAD_ENTER, wx.WXK_NUMPAD_F1,
866    wx.WXK_NUMPAD_F2, wx.WXK_NUMPAD_F3, wx.WXK_NUMPAD_F4, wx.WXK_NUMPAD_HOME,
867    wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN,
868    wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_PAGEDOWN, wx.WXK_NUMPAD_PAGEDOWN,
869    wx.WXK_NUMPAD_END, wx.WXK_NUMPAD_BEGIN, wx.WXK_NUMPAD_INSERT, wx.WXK_NUMPAD_DELETE,
870    wx.WXK_NUMPAD_EQUAL, wx.WXK_NUMPAD_MULTIPLY, wx.WXK_NUMPAD_ADD, wx.WXK_NUMPAD_SEPARATOR,
871    wx.WXK_NUMPAD_SUBTRACT, wx.WXK_NUMPAD_DECIMAL, wx.WXK_NUMPAD_DIVIDE, wx.WXK_WINDOWS_LEFT,
872    wx.WXK_WINDOWS_RIGHT, wx.WXK_WINDOWS_MENU, wx.WXK_COMMAND,
873    # Hardware-specific buttons
874    wx.WXK_SPECIAL1, wx.WXK_SPECIAL2, wx.WXK_SPECIAL3, wx.WXK_SPECIAL4, wx.WXK_SPECIAL5,
875    wx.WXK_SPECIAL6, wx.WXK_SPECIAL7, wx.WXK_SPECIAL8, wx.WXK_SPECIAL9, wx.WXK_SPECIAL10,
876    wx.WXK_SPECIAL11, wx.WXK_SPECIAL12, wx.WXK_SPECIAL13, wx.WXK_SPECIAL14, wx.WXK_SPECIAL15,
877    wx.WXK_SPECIAL16, wx.WXK_SPECIAL17, wx.WXK_SPECIAL18, wx.WXK_SPECIAL19, wx.WXK_SPECIAL20
878    ]
879
880
881## ---------- ---------- ---------- ---------- ---------- ---------- ----------
882
883## Constants for masking. This is where mask characters
884## are defined.
885##  maskchars used to identify valid mask characters from all others
886##   # - allow numeric 0-9 only
887##   A - allow uppercase only. Combine with forceupper to force lowercase to upper
888##   a - allow lowercase only. Combine with forcelower to force upper to lowercase
889##   C - allow any letter, upper or lower
890##   X - allow string.letters, string.punctuation, string.digits
891##   & - allow string.punctuation only (doesn't include all unicode symbols)
892##   * - allow any visible character
893
894## Note: locale settings affect what "uppercase", lowercase, etc comprise.
895## Note: '|' is not a maskchar, in that it is a mask processing directive, and so
896## does not appear here.
897##
898maskchars = ("#","A","a","X","C","N",'*','&')
899ansichars = ""
900for i in range(32, 256):
901    ansichars += chr(i)
902
903months = '(01|02|03|04|05|06|07|08|09|10|11|12)'
904charmonths = '(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)'
905charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
906                   'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
907
908days   = '(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)'
909hours  = '(0\d| \d|1[012])'
910milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)'
911minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
91216|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\
91336|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\
91456|57|58|59)"""
915seconds = minutes
916am_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'
917
918states = "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(',')
919
920state_names = ['Alabama','Alaska','Arizona','Arkansas',
921               'California','Colorado','Connecticut',
922               'Delaware','District of Columbia',
923               'Florida','Georgia','Hawaii',
924               'Idaho','Illinois','Indiana','Iowa',
925               'Kansas','Kentucky','Louisiana',
926               'Maine','Maryland','Massachusetts','Michigan',
927               'Minnesota','Mississippi','Missouri','Montana',
928               'Nebraska','Nevada','New Hampshire','New Jersey',
929               'New Mexico','New York','North Carolina','North Dakokta',
930               'Ohio','Oklahoma','Oregon',
931               'Pennsylvania','Puerto Rico','Rhode Island',
932               'South Carolina','South Dakota',
933               'Tennessee','Texas','Utah',
934               'Vermont','Virginia',
935               'Washington','West Virginia',
936               'Wisconsin','Wyoming']
937
938## ---------- ---------- ---------- ---------- ---------- ---------- ----------
939
940## The following dictionary defines the current set of autoformats:
941
942masktags = {
943       "USPHONEFULLEXT": {
944           'mask': "(###) ###-#### x:###",
945           'formatcodes': 'F^->',
946           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
947           'description': "Phone Number w/opt. ext"
948           },
949       "USPHONETIGHTEXT": {
950           'mask': "###-###-#### x:###",
951           'formatcodes': 'F^->',
952           'validRegex': "^\d{3}-\d{3}-\d{4}",
953           'description': "Phone Number\n (w/hyphens and opt. ext)"
954           },
955       "USPHONEFULL": {
956           'mask': "(###) ###-####",
957           'formatcodes': 'F^->',
958           'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
959           'description': "Phone Number only"
960           },
961       "USPHONETIGHT": {
962           'mask': "###-###-####",
963           'formatcodes': 'F^->',
964           'validRegex': "^\d{3}-\d{3}-\d{4}",
965           'description': "Phone Number\n(w/hyphens)"
966           },
967       "USSTATE": {
968           'mask': "AA",
969           'formatcodes': 'F!V',
970           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % '|'.join(states),
971           'choices': states,
972           'choiceRequired': True,
973           'description': "US State Code"
974           },
975       "USSTATENAME": {
976           'mask': "ACCCCCCCCCCCCCCCCCCC",
977           'formatcodes': 'F_',
978           'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % '|'.join(state_names),
979           'choices': state_names,
980           'choiceRequired': True,
981           'description': "US State Name"
982           },
983
984       "USDATETIMEMMDDYYYY/HHMMSS": {
985           'mask': "##/##/#### ##:##:## AM",
986           'excludeChars': am_pm_exclude,
987           'formatcodes': 'DF!',
988           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
989           'description': "US Date + Time"
990           },
991       "USDATETIMEMMDDYYYY-HHMMSS": {
992           'mask': "##-##-#### ##:##:## AM",
993           'excludeChars': am_pm_exclude,
994           'formatcodes': 'DF!',
995           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
996           'description': "US Date + Time\n(w/hypens)"
997           },
998       "USDATE24HRTIMEMMDDYYYY/HHMMSS": {
999           'mask': "##/##/#### ##:##:##",
1000           'formatcodes': 'DF',
1001           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
1002           'description': "US Date + 24Hr (Military) Time"
1003           },
1004       "USDATE24HRTIMEMMDDYYYY-HHMMSS": {
1005           'mask': "##-##-#### ##:##:##",
1006           'formatcodes': 'DF',
1007           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
1008           'description': "US Date + 24Hr Time\n(w/hypens)"
1009           },
1010       "USDATETIMEMMDDYYYY/HHMM": {
1011           'mask': "##/##/#### ##:## AM",
1012           'excludeChars': am_pm_exclude,
1013           'formatcodes': 'DF!',
1014           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
1015           'description': "US Date + Time\n(without seconds)"
1016           },
1017       "USDATE24HRTIMEMMDDYYYY/HHMM": {
1018           'mask': "##/##/#### ##:##",
1019           'formatcodes': 'DF',
1020           'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,
1021           'description': "US Date + 24Hr Time\n(without seconds)"
1022           },
1023       "USDATETIMEMMDDYYYY-HHMM": {
1024           'mask': "##-##-#### ##:## AM",
1025           'excludeChars': am_pm_exclude,
1026           'formatcodes': 'DF!',
1027           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
1028           'description': "US Date + Time\n(w/hypens and w/o secs)"
1029           },
1030       "USDATE24HRTIMEMMDDYYYY-HHMM": {
1031           'mask': "##-##-#### ##:##",
1032           'formatcodes': 'DF',
1033           'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes,
1034           'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)"
1035           },
1036       "USDATEMMDDYYYY/": {
1037           'mask': "##/##/####",
1038           'formatcodes': 'DF',
1039           'validRegex': '^' + months + '/' + days + '/' + '\d{4}',
1040           'description': "US Date\n(MMDDYYYY)"
1041           },
1042       "USDATEMMDDYY/": {
1043           'mask': "##/##/##",
1044           'formatcodes': 'DF',
1045           'validRegex': '^' + months + '/' + days + '/\d\d',
1046           'description': "US Date\n(MMDDYY)"
1047           },
1048       "USDATEMMDDYYYY-": {
1049           'mask': "##-##-####",
1050           'formatcodes': 'DF',
1051           'validRegex': '^' + months + '-' + days + '-' +'\d{4}',
1052           'description': "MM-DD-YYYY"
1053           },
1054
1055       "EUDATEYYYYMMDD/": {
1056           'mask': "####/##/##",
1057           'formatcodes': 'DF',
1058           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days,
1059           'description': "YYYY/MM/DD"
1060           },
1061       "EUDATEYYYYMMDD.": {
1062           'mask': "####.##.##",
1063           'formatcodes': 'DF',
1064           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days,
1065           'description': "YYYY.MM.DD"
1066           },
1067       "EUDATEDDMMYYYY/": {
1068           'mask': "##/##/####",
1069           'formatcodes': 'DF',
1070           'validRegex': '^' + days + '/' + months + '/' + '\d{4}',
1071           'description': "DD/MM/YYYY"
1072           },
1073       "EUDATEDDMMYYYY.": {
1074           'mask': "##.##.####",
1075           'formatcodes': 'DF',
1076           'validRegex': '^' + days + '.' + months + '.' + '\d{4}',
1077           'description': "DD.MM.YYYY"
1078           },
1079       "EUDATEDDMMMYYYY.": {
1080           'mask': "##.CCC.####",
1081           'formatcodes': 'DF',
1082           'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}',
1083           'description': "DD.Month.YYYY"
1084           },
1085       "EUDATEDDMMMYYYY/": {
1086           'mask': "##/CCC/####",
1087           'formatcodes': 'DF',
1088           'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}',
1089           'description': "DD/Month/YYYY"
1090           },
1091
1092       "EUDATETIMEYYYYMMDD/HHMMSS": {
1093           'mask': "####/##/## ##:##:## AM",
1094           'excludeChars': am_pm_exclude,
1095           'formatcodes': 'DF!',
1096           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
1097           'description': "YYYY/MM/DD HH:MM:SS"
1098           },
1099       "EUDATETIMEYYYYMMDD.HHMMSS": {
1100           'mask': "####.##.## ##:##:## AM",
1101           'excludeChars': am_pm_exclude,
1102           'formatcodes': 'DF!',
1103           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
1104           'description': "YYYY.MM.DD HH:MM:SS"
1105           },
1106       "EUDATETIMEDDMMYYYY/HHMMSS": {
1107           'mask': "##/##/#### ##:##:## AM",
1108           'excludeChars': am_pm_exclude,
1109           'formatcodes': 'DF!',
1110           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
1111           'description': "DD/MM/YYYY HH:MM:SS"
1112           },
1113       "EUDATETIMEDDMMYYYY.HHMMSS": {
1114           'mask': "##.##.#### ##:##:## AM",
1115           'excludeChars': am_pm_exclude,
1116           'formatcodes': 'DF!',
1117           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
1118           'description': "DD.MM.YYYY HH:MM:SS"
1119           },
1120
1121       "EUDATETIMEYYYYMMDD/HHMM": {
1122           'mask': "####/##/## ##:## AM",
1123           'excludeChars': am_pm_exclude,
1124           'formatcodes': 'DF!',
1125           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
1126           'description': "YYYY/MM/DD HH:MM"
1127           },
1128       "EUDATETIMEYYYYMMDD.HHMM": {
1129           'mask': "####.##.## ##:## AM",
1130           'excludeChars': am_pm_exclude,
1131           'formatcodes': 'DF!',
1132           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
1133           'description': "YYYY.MM.DD HH:MM"
1134           },
1135       "EUDATETIMEDDMMYYYY/HHMM": {
1136           'mask': "##/##/#### ##:## AM",
1137           'excludeChars': am_pm_exclude,
1138           'formatcodes': 'DF!',
1139           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
1140           'description': "DD/MM/YYYY HH:MM"
1141           },
1142       "EUDATETIMEDDMMYYYY.HHMM": {
1143           'mask': "##.##.#### ##:## AM",
1144           'excludeChars': am_pm_exclude,
1145           'formatcodes': 'DF!',
1146           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
1147           'description': "DD.MM.YYYY HH:MM"
1148           },
1149
1150       "EUDATE24HRTIMEYYYYMMDD/HHMMSS": {
1151           'mask': "####/##/## ##:##:##",
1152           'formatcodes': 'DF',
1153           'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
1154           'description': "YYYY/MM/DD 24Hr Time"
1155           },
1156       "EUDATE24HRTIMEYYYYMMDD.HHMMSS": {
1157           'mask': "####.##.## ##:##:##",
1158           'formatcodes': 'DF',
1159           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
1160           'description': "YYYY.MM.DD 24Hr Time"
1161           },
1162       "EUDATE24HRTIMEDDMMYYYY/HHMMSS": {
1163           'mask': "##/##/#### ##:##:##",
1164           'formatcodes': 'DF',
1165           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
1166           'description': "DD/MM/YYYY 24Hr Time"
1167           },
1168       "EUDATE24HRTIMEDDMMYYYY.HHMMSS": {
1169           'mask': "##.##.#### ##:##:##",
1170           'formatcodes': 'DF',
1171           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
1172           'description': "DD.MM.YYYY 24Hr Time"
1173           },
1174       "EUDATE24HRTIMEYYYYMMDD/HHMM": {
1175           'mask': "####/##/## ##:##",
1176           'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes,
1177           'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)"
1178           },
1179       "EUDATE24HRTIMEYYYYMMDD.HHMM": {
1180           'mask': "####.##.## ##:##",
1181           'formatcodes': 'DF',
1182           'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes,
1183           'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)"
1184           },
1185       "EUDATE24HRTIMEDDMMYYYY/HHMM": {
1186           'mask': "##/##/#### ##:##",
1187           'formatcodes': 'DF',
1188           'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes,
1189           'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)"
1190           },
1191       "EUDATE24HRTIMEDDMMYYYY.HHMM": {
1192           'mask': "##.##.#### ##:##",
1193           'formatcodes': 'DF',
1194           'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes,
1195           'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)"
1196           },
1197
1198       "TIMEHHMMSS": {
1199           'mask': "##:##:## AM",
1200           'excludeChars': am_pm_exclude,
1201           'formatcodes': 'TF!',
1202           'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
1203           'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)"
1204           },
1205       "TIMEHHMM": {
1206           'mask': "##:## AM",
1207           'excludeChars': am_pm_exclude,
1208           'formatcodes': 'TF!',
1209           'validRegex': '^' + hours + ':' + minutes + ' (A|P)M',
1210           'description': "HH:MM (A|P)M\n(see TimeCtrl)"
1211           },
1212       "24HRTIMEHHMMSS": {
1213           'mask': "##:##:##",
1214           'formatcodes': 'TF',
1215           'validRegex': '^' + milhours + ':' + minutes + ':' + seconds,
1216           'description': "24Hr HH:MM:SS\n(see TimeCtrl)"
1217           },
1218       "24HRTIMEHHMM": {
1219           'mask': "##:##",
1220           'formatcodes': 'TF',
1221           'validRegex': '^' + milhours + ':' + minutes,
1222           'description': "24Hr HH:MM\n(see TimeCtrl)"
1223           },
1224       "USSOCIALSEC": {
1225           'mask': "###-##-####",
1226           'formatcodes': 'F',
1227           'validRegex': "\d{3}-\d{2}-\d{4}",
1228           'description': "Social Sec#"
1229           },
1230       "CREDITCARD": {
1231           'mask': "####-####-####-####",
1232           'formatcodes': 'F',
1233           'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}",
1234           'description': "Credit Card"
1235           },
1236       "EXPDATEMMYY": {
1237           'mask': "##/##",
1238           'formatcodes': "F",
1239           'validRegex': "^" + months + "/\d\d",
1240           'description': "Expiration MM/YY"
1241           },
1242       "USZIP": {
1243           'mask': "#####",
1244           'formatcodes': 'F',
1245           'validRegex': "^\d{5}",
1246           'description': "US 5-digit zip code"
1247           },
1248       "USZIPPLUS4": {
1249           'mask': "#####-####",
1250           'formatcodes': 'F',
1251           'validRegex': "\d{5}-(\s{4}|\d{4})",
1252           'description': "US zip+4 code"
1253           },
1254       "PERCENT": {
1255           'mask': "0.##",
1256           'formatcodes': 'F',
1257           'validRegex': "^0.\d\d",
1258           'description': "Percentage"
1259           },
1260       "AGE": {
1261           'mask': "###",
1262           'formatcodes': "F",
1263           'validRegex': "^[1-9]{1}  |[1-9][0-9] |1[0|1|2][0-9]",
1264           'description': "Age"
1265           },
1266       "EMAIL": {
1267           'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
1268           'excludeChars': " \\/*&%$#!+='\"",
1269           'formatcodes': "F>",
1270           '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}\]) *$",
1271           'description': "Email address"
1272           },
1273       "IPADDR": {
1274           'mask': "###.###.###.###",
1275           'formatcodes': 'F_Sr',
1276           '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}",
1277           'description': "IP Address\n(see IpAddrCtrl)"
1278           }
1279       }
1280
1281# build demo-friendly dictionary of descriptions of autoformats
1282autoformats = []
1283for key, value in masktags.items():
1284    autoformats.append((key, value['description']))
1285autoformats.sort()
1286
1287## ---------- ---------- ---------- ---------- ---------- ---------- ----------
1288
1289class Field:
1290    """
1291    This class manages the individual fields in a masked edit control.
1292    Each field has a zero-based index, indicating its position in the
1293    control, an extent, an associated mask, and a plethora of optional
1294    parameters.  Fields can be instantiated and then associated with
1295    parent masked controls, in order to provide field-specific configuration.
1296    Alternatively, fields will be implicitly created by the parent control
1297    if not provided at construction, at which point, the fields can then
1298    manipulated by the controls .SetFieldParameters() method.
1299    """
1300    valid_params = {
1301              'index': None,                    ## which field of mask; set by parent control.
1302              'mask': "",                       ## mask chars for this field
1303              'extent': (),                     ## (edit start, edit_end) of field; set by parent control.
1304              'formatcodes':  "",               ## codes indicating formatting options for the control
1305              'fillChar':     ' ',              ## used as initial value for each mask position if initial value is not given
1306              'groupChar':    ',',              ## used with numeric fields; indicates what char groups 3-tuple digits
1307              'decimalChar':  '.',              ## used with numeric fields; indicates what char separates integer from fraction
1308              'shiftDecimalChar': '>',          ## used with numeric fields, indicates what is above the decimal point char on keyboard
1309              'useParensForNegatives': False,   ## used with numeric fields, indicates that () should be used vs. - to show negative numbers.
1310              'defaultValue': "",               ## use if you want different positional defaults vs. all the same fillChar
1311              'excludeChars': "",               ## optional string of chars to exclude even if main mask type does
1312              'includeChars': "",               ## optional string of chars to allow even if main mask type doesn't
1313              'validRegex':   "",               ## optional regular expression to use to validate the control
1314              'validRange':   (),               ## Optional hi-low range for numerics
1315              'choices':    [],                 ## Optional list for character expressions
1316              'choiceRequired': False,          ## If choices supplied this specifies if valid value must be in the list
1317              'compareNoCase': False,           ## Optional flag to indicate whether or not to use case-insensitive list search
1318              'autoSelect': False,              ## Set to True to try auto-completion on each keystroke:
1319              'validFunc': None,                ## Optional function for defining additional, possibly dynamic validation constraints on contrl
1320              'validRequired': False,           ## Set to True to disallow input that results in an invalid value
1321              'emptyInvalid':  False,           ## Set to True to make EMPTY = INVALID
1322              'description': "",                ## primarily for autoformats, but could be useful elsewhere
1323              'raiseOnInvalidPaste': False,     ## if True, paste into field will cause ValueError
1324              'stopFieldChangeIfInvalid': False,## if True, disallow field navigation out of invalid field
1325              }
1326
1327    # This list contains all parameters that when set at the control level should
1328    # propagate down to each field:
1329    propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
1330                          'compareNoCase', 'emptyInvalid', 'validRequired', 'raiseOnInvalidPaste',
1331                          'stopFieldChangeIfInvalid')
1332
1333    def __init__(self, **kwargs):
1334        """
1335        This is the "constructor" for setting up parameters for fields.
1336        a field_index of -1 is used to indicate "the entire control."
1337        """
1338####        dbg('Field::Field', indent=1)
1339        # Validate legitimate set of parameters:
1340        for key in kwargs.keys():
1341            if key not in Field.valid_params.keys():
1342####                dbg(indent=0)
1343                ae = AttributeError('invalid parameter "%s"' % (key))
1344                ae.attribute = key
1345                raise ae
1346
1347        # Set defaults for each parameter for this instance, and fully
1348        # populate initial parameter list for configuration:
1349        for key, value in Field.valid_params.items():
1350            setattr(self, '_' + key, copy.copy(value))
1351            if key not in kwargs:
1352                kwargs[key] = copy.copy(value)
1353
1354        self._autoCompleteIndex = -1
1355        self._SetParameters(**kwargs)
1356        self._ValidateParameters(**kwargs)
1357
1358####        dbg(indent=0)
1359
1360
1361    def _SetParameters(self, **kwargs):
1362        """
1363        This function can be used to set individual or multiple parameters for
1364        a masked edit field parameter after construction.
1365        """
1366##        dbg(suspend=1)
1367##        dbg('maskededit.Field::_SetParameters', indent=1)
1368        # Validate keyword arguments:
1369        for key in kwargs.keys():
1370            if key not in Field.valid_params.keys():
1371##                dbg(indent=0, suspend=0)
1372                ae = AttributeError('invalid keyword argument "%s"' % key)
1373                ae.attribute = key
1374                raise ae
1375
1376##        if self._index is not None: dbg('field index:', self._index)
1377##        dbg('parameters:', indent=1)
1378        for key, value in kwargs.items():
1379##            dbg('%s:' % key, value)
1380            pass
1381##        dbg(indent=0)
1382
1383
1384        old_fillChar = self._fillChar   # store so we can change choice lists accordingly if it changes
1385
1386        # First, Assign all parameters specified:
1387        for key in Field.valid_params.keys():
1388            if key in kwargs:
1389                setattr(self, '_' + key, kwargs[key] )
1390
1391        if 'formatcodes' in kwargs:   # (set/changed)
1392            self._forceupper  = '!' in self._formatcodes
1393            self._forcelower  = '^' in self._formatcodes
1394            self._groupdigits = ',' in self._formatcodes
1395            self._okSpaces    = '_' in self._formatcodes
1396            self._padZero     = '0' in self._formatcodes
1397            self._autofit     = 'F' in self._formatcodes
1398            self._insertRight = 'r' in self._formatcodes
1399            self._allowInsert = '>' in self._formatcodes
1400            self._alignRight  = 'R' in self._formatcodes or 'r' in self._formatcodes
1401            self._moveOnFieldFull = not '<' in self._formatcodes
1402            self._selectOnFieldEntry = 'S' in self._formatcodes
1403
1404            if 'groupChar' in kwargs:
1405                self._groupChar = kwargs['groupChar']
1406            if 'decimalChar' in kwargs:
1407                self._decimalChar = kwargs['decimalChar']
1408            if 'shiftDecimalChar' in kwargs:
1409                self._shiftDecimalChar = kwargs['shiftDecimalChar']
1410
1411        if 'formatcodes' in kwargs or 'validRegex' in kwargs:
1412            self._regexMask   = 'V' in self._formatcodes and self._validRegex
1413
1414        if 'fillChar' in kwargs:
1415            self._old_fillChar = old_fillChar
1416####            dbg("self._old_fillChar: '%s'" % self._old_fillChar)
1417
1418        if 'mask' in kwargs or 'validRegex' in kwargs:  # (set/changed)
1419            self._isInt = _isInteger(self._mask)
1420##            dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask)
1421
1422##        dbg(indent=0, suspend=0)
1423
1424
1425    def _ValidateParameters(self, **kwargs):
1426        """
1427        This function can be used to validate individual or multiple parameters for
1428        a masked edit field parameter after construction.
1429        """
1430##        dbg(suspend=1)
1431##        dbg('maskededit.Field::_ValidateParameters', indent=1)
1432##        if self._index is not None: dbg('field index:', self._index)
1433####        dbg('parameters:', indent=1)
1434##        for key, value in kwargs.items():
1435####            dbg('%s:' % key, value)
1436####        dbg(indent=0)
1437####        dbg("self._old_fillChar: '%s'" % self._old_fillChar)
1438
1439        # Verify proper numeric format params:
1440        if self._groupdigits and self._groupChar == self._decimalChar:
1441##            dbg(indent=0, suspend=0)
1442            ae = AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar))
1443            ae.attribute = self._groupChar
1444            raise ae
1445
1446
1447        # Now go do validation, semantic and inter-dependency parameter processing:
1448        if 'choices' in kwargs or 'compareNoCase' in kwargs or 'choiceRequired' in kwargs: # (set/changed)
1449
1450            self._compareChoices = [choice.strip() for choice in self._choices]
1451
1452            if self._compareNoCase and self._choices:
1453                self._compareChoices = [item.lower() for item in self._compareChoices]
1454
1455            if 'choices' in kwargs:
1456                self._autoCompleteIndex = -1
1457
1458
1459        if 'validRegex' in kwargs:    # (set/changed)
1460            if self._validRegex:
1461                try:
1462                    if self._compareNoCase:
1463                        self._filter = re.compile(self._validRegex, re.IGNORECASE)
1464                    else:
1465                        self._filter = re.compile(self._validRegex)
1466                except:
1467##                    dbg(indent=0, suspend=0)
1468                    raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
1469            else:
1470                self._filter = None
1471
1472        if 'validRange' in kwargs:    # (set/changed)
1473            self._hasRange  = False
1474            self._rangeHigh = 0
1475            self._rangeLow  = 0
1476            if self._validRange:
1477                if not isinstance(self._validRange, tuple) or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]:
1478##                    dbg(indent=0, suspend=0)
1479                    raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b'
1480                                    % (str(self._index), repr(self._validRange)) )
1481
1482                self._hasRange  = True
1483                self._rangeLow  = self._validRange[0]
1484                self._rangeHigh = self._validRange[1]
1485
1486        if 'choices' in kwargs or (len(self._choices) and len(self._choices[0]) != len(self._mask)):       # (set/changed)
1487            self._hasList   = False
1488            if self._choices and not isinstance(self._choices, (tuple, list)):
1489##                dbg(indent=0, suspend=0)
1490                raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
1491            elif len( self._choices) > 0:
1492                for choice in self._choices:
1493                    if not isinstance(choice, six.string_types):
1494##                        dbg(indent=0, suspend=0)
1495                        raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
1496
1497                length = len(self._mask)
1498##                dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight)
1499                if len(self._choices) and length:
1500                    if len(self._choices[0]) > length:
1501                        # changed mask without respecifying choices; readjust the width as appropriate:
1502                        self._choices = [choice.strip() for choice in self._choices]
1503                    if self._alignRight:
1504                        self._choices = [choice.rjust( length ) for choice in self._choices]
1505                    else:
1506                        self._choices = [choice.ljust( length ) for choice in self._choices]
1507##                    dbg('aligned choices:', self._choices)
1508
1509                if hasattr(self, '_template'):
1510                    # Verify each choice specified is valid:
1511                    for choice in self._choices:
1512                        if self.IsEmpty(choice) and not self._validRequired:
1513                            # allow empty values even if invalid, (just colored differently)
1514                            continue
1515                        if not self.IsValid(choice):
1516##                            dbg(indent=0, suspend=0)
1517                            ve = ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
1518                            ve.value = choice
1519                            raise ve
1520                self._hasList = True
1521
1522####        dbg("'fillChar' in kwargs?", 'fillChar' in kwargs, "len(self._choices) > 0?", len(self._choices) > 0)
1523####        dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar)
1524        if 'fillChar' in kwargs and len(self._choices) > 0:
1525            if kwargs['fillChar'] != ' ':
1526                self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices]
1527            else:
1528                self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices]
1529##            dbg('updated choices:', self._choices)
1530
1531
1532        if 'autoSelect' in kwargs and kwargs['autoSelect']:
1533            if not self._hasList:
1534##                dbg('no list to auto complete; ignoring "autoSelect=True"')
1535                self._autoSelect = False
1536
1537        # reset field validity assumption:
1538        self._valid = True
1539##        dbg(indent=0, suspend=0)
1540
1541
1542    def _GetParameter(self, paramname):
1543        """
1544        Routine for retrieving the value of any given parameter
1545        """
1546        if paramname in Field.valid_params:
1547            return getattr(self, '_' + paramname)
1548        else:
1549            TypeError('Field._GetParameter: invalid parameter "%s"' % key)
1550
1551
1552    def IsEmpty(self, slice):
1553        """
1554        Indicates whether the specified slice is considered empty for the
1555        field.
1556        """
1557##        dbg('Field::IsEmpty("%s")' % slice, indent=1)
1558        if not hasattr(self, '_template'):
1559##            dbg(indent=0)
1560            raise AttributeError('_template')
1561
1562##        dbg('self._template: "%s"' % self._template)
1563##        dbg('self._defaultValue: "%s"' % str(self._defaultValue))
1564        if slice == self._template and not self._defaultValue:
1565##            dbg(indent=0)
1566            return True
1567
1568        elif slice == self._template:
1569            empty = True
1570            for pos in range(len(self._template)):
1571####                dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos])
1572                if slice[pos] not in (' ', self._fillChar):
1573                    empty = False
1574                    break
1575##            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0)
1576            return empty
1577        else:
1578##            dbg("IsEmpty? 0 (slice doesn't match template)", indent=0)
1579            return False
1580
1581
1582    def IsValid(self, slice):
1583        """
1584        Indicates whether the specified slice is considered a valid value for the
1585        field.
1586        """
1587##        dbg(suspend=1)
1588##        dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1)
1589        valid = True    # assume true to start
1590
1591        if self.IsEmpty(slice):
1592##            dbg(indent=0, suspend=0)
1593            if self._emptyInvalid:
1594                return False
1595            else:
1596                return True
1597
1598        elif self._hasList and self._choiceRequired:
1599##            dbg("(member of list required)")
1600            # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices):
1601            if self._fillChar != ' ':
1602                slice = slice.replace(self._fillChar, ' ')
1603##                dbg('updated slice:"%s"' % slice)
1604            compareStr = slice.strip()
1605
1606            if self._compareNoCase:
1607                compareStr = compareStr.lower()
1608            valid = compareStr in self._compareChoices
1609
1610        elif self._hasRange and not self.IsEmpty(slice):
1611##            dbg('validating against range')
1612            try:
1613                # allow float as well as int ranges (int comparisons for free.)
1614                valid = self._rangeLow <= float(slice) <= self._rangeHigh
1615            except:
1616                valid = False
1617
1618        elif self._validRegex and self._filter:
1619##            dbg('validating against regex')
1620            valid = (re.match( self._filter, slice) is not None)
1621
1622        if valid and self._validFunc:
1623##            dbg('validating against supplied function')
1624            valid = self._validFunc(slice)
1625##        dbg('valid?', valid, indent=0, suspend=0)
1626        return valid
1627
1628
1629    def _AdjustField(self, slice):
1630        """ 'Fixes' an integer field. Right or left-justifies, as required."""
1631##        dbg('Field::_AdjustField("%s")' % slice, indent=1)
1632        length = len(self._mask)
1633####        dbg('length(self._mask):', length)
1634####        dbg('self._useParensForNegatives?', self._useParensForNegatives)
1635        if self._isInt:
1636            if self._useParensForNegatives:
1637                signpos = slice.find('(')
1638                right_signpos = slice.find(')')
1639                intStr = slice.replace('(', '').replace(')', '')    # drop sign, if any
1640            else:
1641                signpos = slice.find('-')
1642                intStr = slice.replace( '-', '' )                   # drop sign, if any
1643                right_signpos = -1
1644
1645            intStr = intStr.replace(' ', '')                        # drop extra spaces
1646            intStr = intStr.replace(self._fillChar,"")       # drop extra fillchars
1647            intStr = intStr.replace("-","")                  # drop sign, if any
1648            intStr = intStr.replace(self._groupChar, "")    # lose commas/dots
1649####            dbg('intStr:"%s"' % intStr)
1650            start, end = self._extent
1651            field_len = end - start
1652            if not self._padZero and len(intStr) != field_len and intStr.strip():
1653                intStr = str(int(intStr))
1654####            dbg('raw int str: "%s"' % intStr)
1655####            dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes)
1656            if self._groupdigits:
1657                new = ''
1658                cnt = 1
1659                for i in range(len(intStr)-1, -1, -1):
1660                    new = intStr[i] + new
1661                    if (cnt) % 3 == 0:
1662                        new = self._groupChar + new
1663                    cnt += 1
1664                if new and new[0] == self._groupChar:
1665                    new = new[1:]
1666                if len(new) <= length:
1667                    # expanded string will still fit and leave room for sign:
1668                    intStr = new
1669                # else... leave it without the commas...
1670
1671##            dbg('padzero?', self._padZero)
1672##            dbg('len(intStr):', len(intStr), 'field length:', length)
1673            if self._padZero and len(intStr) < length:
1674                intStr = '0' * (length - len(intStr)) + intStr
1675                if signpos != -1:   # we had a sign before; restore it
1676                    if self._useParensForNegatives:
1677                        intStr = '(' + intStr[1:]
1678                        if right_signpos != -1:
1679                            intStr += ')'
1680                    else:
1681                        intStr = '-' + intStr[1:]
1682            elif signpos != -1 and slice[0:signpos].strip() == '':    # - was before digits
1683                if self._useParensForNegatives:
1684                    intStr = '(' + intStr
1685                    if right_signpos != -1:
1686                        intStr += ')'
1687                else:
1688                    intStr = '-' + intStr
1689            elif right_signpos != -1:
1690                # must have had ')' but '(' was before field; re-add ')'
1691                intStr += ')'
1692            slice = intStr
1693
1694        slice = slice.strip() # drop extra spaces
1695
1696        if self._alignRight:     ## Only if right-alignment is enabled
1697            slice = slice.rjust( length )
1698        else:
1699            slice = slice.ljust( length )
1700        if self._fillChar != ' ':
1701            slice = slice.replace(' ', self._fillChar)
1702##        dbg('adjusted slice: "%s"' % slice, indent=0)
1703        return slice
1704
1705
1706## ---------- ---------- ---------- ---------- ---------- ---------- ----------
1707
1708class MaskedEditMixin:
1709    """
1710    This class allows us to abstract the masked edit functionality that could
1711    be associated with any text entry control. (eg. wx.TextCtrl, wx.ComboBox, etc.)
1712    It forms the basis for all of the lib.masked controls.
1713    """
1714    valid_ctrl_params = {
1715              'mask': 'XXXXXXXXXXXXX',          ## mask string for formatting this control
1716              'autoformat':   "",               ## optional auto-format code to set format from masktags dictionary
1717              'fields': {},                     ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask
1718              'datestyle':    'MDY',            ## optional date style for date-type values. Can trigger autocomplete year
1719              'autoCompleteKeycodes': [],       ## Optional list of additional keycodes which will invoke field-auto-complete
1720              'useFixedWidthFont': True,        ## Use fixed-width font instead of default for base control
1721              'defaultEncoding': 'latin1',      ## optional argument to indicate unicode codec to use (unicode ctrls only)
1722              'retainFieldValidation': False,   ## Set this to true if setting control-level parameters independently,
1723                                                ## from field validation constraints
1724              'emptyBackgroundColour': "White",
1725              'validBackgroundColour': "White",
1726              'invalidBackgroundColour': "Yellow",
1727              'foregroundColour': "Black",
1728              'signedForegroundColour': "Red",
1729              'demo': False}
1730
1731
1732    def __init__(self, name = 'MaskedEdit', **kwargs):
1733        """
1734        This is the "constructor" for setting up the mixin variable parameters for the composite class.
1735        """
1736
1737        self.name = name
1738
1739        # set up flag for doing optional things to base control if possible
1740        if not hasattr(self, 'controlInitialized'):
1741            self.controlInitialized = False
1742
1743        # Set internal state var for keeping track of whether or not a character
1744        # action results in a modification of the control, since .SetValue()
1745        # doesn't modify the base control's internal state:
1746        self.modified = False
1747        self._previous_mask = None
1748
1749        # Validate legitimate set of parameters:
1750        for key in kwargs.keys():
1751            if key.replace('Color', 'Colour') not in list(MaskedEditMixin.valid_ctrl_params.keys()) + list(Field.valid_params.keys()):
1752                raise TypeError('%s: invalid parameter "%s"' % (name, key))
1753
1754        ## Set up dictionary that can be used by subclasses to override or add to default
1755        ## behavior for individual characters.  Derived subclasses needing to change
1756        ## default behavior for keys can either redefine the default functions for the
1757        ## common keys or add functions for specific keys to this list.  Each function
1758        ## added should take the key event as argument, and return False if the key
1759        ## requires no further processing.
1760        ##
1761        ## Initially populated with navigation and function control keys:
1762        self._keyhandlers = {
1763            # default navigation keys and handlers:
1764            wx.WXK_BACK:            self._OnErase,
1765            wx.WXK_LEFT:            self._OnArrow,
1766            wx.WXK_NUMPAD_LEFT:     self._OnArrow,
1767            wx.WXK_RIGHT:           self._OnArrow,
1768            wx.WXK_NUMPAD_RIGHT:    self._OnArrow,
1769            wx.WXK_UP:              self._OnAutoCompleteField,
1770            wx.WXK_NUMPAD_UP:       self._OnAutoCompleteField,
1771            wx.WXK_DOWN:            self._OnAutoCompleteField,
1772            wx.WXK_NUMPAD_DOWN:     self._OnAutoCompleteField,
1773            wx.WXK_TAB:             self._OnChangeField,
1774            wx.WXK_HOME:            self._OnHome,
1775            wx.WXK_NUMPAD_HOME:     self._OnHome,
1776            wx.WXK_END:             self._OnEnd,
1777            wx.WXK_NUMPAD_END:      self._OnEnd,
1778            wx.WXK_RETURN:          self._OnReturn,
1779            wx.WXK_NUMPAD_ENTER:    self._OnReturn,
1780            wx.WXK_PAGEUP:           self._OnAutoCompleteField,
1781            wx.WXK_NUMPAD_PAGEUP:    self._OnAutoCompleteField,
1782            wx.WXK_PAGEDOWN:            self._OnAutoCompleteField,
1783            wx.WXK_NUMPAD_PAGEDOWN:     self._OnAutoCompleteField,
1784
1785            # default function control keys and handlers:
1786            wx.WXK_DELETE:          self._OnDelete,
1787            wx.WXK_NUMPAD_DELETE:   self._OnDelete,
1788            wx.WXK_INSERT:          self._OnInsert,
1789            wx.WXK_NUMPAD_INSERT:   self._OnInsert,
1790
1791            WXK_CTRL_A:             self._OnCtrl_A,
1792            WXK_CTRL_C:             self._OnCtrl_C,
1793            WXK_CTRL_S:             self._OnCtrl_S,
1794            WXK_CTRL_V:             self._OnCtrl_V,
1795            WXK_CTRL_X:             self._OnCtrl_X,
1796            WXK_CTRL_Z:             self._OnCtrl_Z,
1797            }
1798
1799        ## bind standard navigational and control keycodes to this instance,
1800        ## so that they can be augmented and/or changed in derived classes:
1801        self._nav = list(nav)
1802        self._control = list(control)
1803
1804        ## Dynamically evaluate and store string constants for mask chars
1805        ## so that locale settings can be made after this module is imported
1806        ## and the controls created after that is done can allow the
1807        ## appropriate characters:
1808        self.maskchardict  = {
1809            '#': string.digits,
1810            'A': string.ascii_uppercase,
1811            'a': string.ascii_lowercase,
1812            'X': string.ascii_letters + string.punctuation + string.digits,
1813            'C': string.ascii_letters,
1814            'N': string.ascii_letters + string.digits,
1815            '&': string.punctuation,
1816            '*': ansichars  # to give it a value, but now allows any non-wxcontrol character
1817        }
1818
1819        ## self._ignoreChange is used by MaskedComboBox, because
1820        ## of the hack necessary to determine the selection; it causes
1821        ## EVT_TEXT messages from the combobox to be ignored if set.
1822        self._ignoreChange = False
1823
1824        # These are used to keep track of previous value, for undo functionality:
1825        self._curValue  = None
1826        self._prevValue = None
1827
1828        self._valid     = True
1829
1830        # Set defaults for each parameter for this instance, and fully
1831        # populate initial parameter list for configuration:
1832        for key, value in MaskedEditMixin.valid_ctrl_params.items():
1833            setattr(self, '_' + key, copy.copy(value))
1834            if key not in kwargs:
1835####                dbg('%s: "%s"' % (key, repr(value)))
1836                kwargs[key] = copy.copy(value)
1837
1838        # Create a "field" that holds global parameters for control constraints
1839        self._ctrl_constraints = self._fields[-1] = Field(index=-1)
1840        self.SetCtrlParameters(**kwargs)
1841
1842
1843
1844    def SetCtrlParameters(self, **kwargs):
1845        """
1846        This public function can be used to set individual or multiple masked edit
1847        parameters after construction.  (See maskededit module overview for the list
1848        of valid parameters.)
1849        """
1850##        dbg(suspend=1)
1851##        dbg('MaskedEditMixin::SetCtrlParameters', indent=1)
1852####        dbg('kwargs:', indent=1)
1853##        for key, value in kwargs.items():
1854####            dbg(key, '=', value)
1855####        dbg(indent=0)
1856
1857        # Validate keyword arguments:
1858        constraint_kwargs = {}
1859        ctrl_kwargs = {}
1860        for key, value in kwargs.items():
1861            key = key.replace('Color', 'Colour')    # for b-c, and standard wxPython spelling
1862            if key not in list(MaskedEditMixin.valid_ctrl_params.keys()) + list(Field.valid_params.keys()):
1863##                dbg(indent=0, suspend=0)
1864                ae = AttributeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name))
1865                ae.attribute = key
1866                raise ae
1867            elif key in Field.valid_params.keys():
1868                constraint_kwargs[key] = value
1869            else:
1870                ctrl_kwargs[key] = value
1871
1872        mask = None
1873        reset_args = {}
1874
1875        if 'autoformat' in ctrl_kwargs:
1876            autoformat = ctrl_kwargs['autoformat']
1877        else:
1878            autoformat = None
1879
1880        # handle "parochial name" backward compatibility:
1881        if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys():
1882            autoformat = autoformat.replace('MILTIME', '24HRTIME')
1883
1884        if autoformat != self._autoformat and autoformat in masktags.keys():
1885##            dbg('autoformat:', autoformat)
1886            self._autoformat                  = autoformat
1887            mask                              = masktags[self._autoformat]['mask']
1888            # gather rest of any autoformat parameters:
1889            for param, value in masktags[self._autoformat].items():
1890                if param == 'mask': continue    # (must be present; already accounted for)
1891                constraint_kwargs[param] = value
1892
1893        elif autoformat and not autoformat in masktags.keys():
1894            ae = AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
1895            ae.attribute = autoformat
1896            raise ae
1897        else:
1898##            dbg('autoformat not selected')
1899            if 'mask' in kwargs:
1900                mask = kwargs['mask']
1901##                dbg('mask:', mask)
1902
1903        ## Assign style flags
1904        if mask is None:
1905##            dbg('preserving previous mask')
1906            mask = self._previous_mask   # preserve previous mask
1907        else:
1908##            dbg('mask (re)set')
1909            reset_args['reset_mask'] = mask
1910            constraint_kwargs['mask'] = mask
1911
1912            # wipe out previous fields; preserve new control-level constraints
1913            self._fields = {-1: self._ctrl_constraints}
1914
1915
1916        if 'fields' in ctrl_kwargs:
1917            # do field parameter type validation, and conversion to internal dictionary
1918            # as appropriate:
1919            fields = ctrl_kwargs['fields']
1920            if isinstance(fields, (list, tuple)):
1921                for i in range(len(fields)):
1922                    field = fields[i]
1923                    if not isinstance(field, Field):
1924##                        dbg(indent=0, suspend=0)
1925                        raise TypeError('invalid type for field parameter: %s' % repr(field))
1926                    self._fields[i] = field
1927
1928            elif isinstance(fields, dict):
1929                for index, field in fields.items():
1930                    if not isinstance(field, Field):
1931##                        dbg(indent=0, suspend=0)
1932                        raise TypeError('invalid type for field parameter: %s' % repr(field))
1933                    self._fields[index] = field
1934            else:
1935##                dbg(indent=0, suspend=0)
1936                raise TypeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
1937
1938        # Assign constraint parameters for entire control:
1939####        dbg('control constraints:', indent=1)
1940##        for key, value in constraint_kwargs.items():
1941####            dbg('%s:' % key, value)
1942####        dbg(indent=0)
1943
1944        # determine if changing parameters that should affect the entire control:
1945        for key in MaskedEditMixin.valid_ctrl_params.keys():
1946            if key in ( 'mask', 'fields' ): continue    # (processed separately)
1947            if key in ctrl_kwargs:
1948                setattr(self, '_' + key, ctrl_kwargs[key])
1949
1950        # Validate color parameters, converting strings to named colors and validating
1951        # result if appropriate:
1952        for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour',
1953                    'foregroundColour', 'signedForegroundColour'):
1954            if key in ctrl_kwargs:
1955                if isinstance(ctrl_kwargs[key], six.string_types):
1956                    c = wx.Colour(ctrl_kwargs[key])
1957                    if c.Get() == (-1, -1, -1):
1958                        raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
1959                    else:
1960                        # replace attribute with wxColour object:
1961                        setattr(self, '_' + key, c)
1962                        # attach a python dynamic attribute to wxColour for debug printouts
1963                        c._name = ctrl_kwargs[key]
1964
1965                elif type(ctrl_kwargs[key]) != type(wx.BLACK):
1966                    raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
1967
1968
1969##        dbg('self._retainFieldValidation:', self._retainFieldValidation)
1970        if not self._retainFieldValidation:
1971            # Build dictionary of any changing parameters which should be propagated to the
1972            # component fields:
1973            for arg in Field.propagating_params:
1974####                dbg('%s in kwargs?' % arg, arg in kwargs)
1975####                dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg))
1976                reset_args[arg] = arg in kwargs and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg)
1977####                dbg('reset_args[%s]?' % arg, reset_args[arg])
1978
1979        # Set the control-level constraints:
1980        self._ctrl_constraints._SetParameters(**constraint_kwargs)
1981
1982        # This routine does the bulk of the interdependent parameter processing, determining
1983        # the field extents of the mask if changed, resetting parameters as appropriate,
1984        # determining the overall template value for the control, etc.
1985        self._configure(mask, **reset_args)
1986
1987        # now that we've propagated the field constraints and mask portions to the
1988        # various fields, validate the constraints
1989        self._ctrl_constraints._ValidateParameters(**constraint_kwargs)
1990
1991        # Validate that all choices for given fields are at least of the
1992        # necessary length, and that they all would be valid pastes if pasted
1993        # into their respective fields:
1994####        dbg('validating choices')
1995        self._validateChoices()
1996
1997
1998        self._autofit = self._ctrl_constraints._autofit
1999        self._isNeg      = False
2000
2001        self._isDate     = 'D' in self._ctrl_constraints._formatcodes and _isDateType(mask)
2002        self._isTime     = 'T' in self._ctrl_constraints._formatcodes and _isTimeType(mask)
2003        if self._isDate:
2004            # Set _dateExtent, used in date validation to locate date in string;
2005            # always set as though year will be 4 digits, even if mask only has
2006            # 2 digits, so we can always properly process the intended year for
2007            # date validation (leap years, etc.)
2008            if self._mask.find('CCC') != -1: self._dateExtent = 11
2009            else:                            self._dateExtent = 10
2010
2011            self._4digityear = len(self._mask) > 8 and self._mask[9] == '#'
2012
2013        if self._isDate and self._autoformat:
2014            # Auto-decide datestyle:
2015            if self._autoformat.find('MDDY')    != -1: self._datestyle = 'MDY'
2016            elif self._autoformat.find('YMMD')  != -1: self._datestyle = 'YMD'
2017            elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD'
2018            elif self._autoformat.find('DMMY')  != -1: self._datestyle = 'DMY'
2019            elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY'
2020
2021        # Give derived controls a chance to react to parameter changes before
2022        # potentially changing current value of the control.
2023        self._OnCtrlParametersChanged()
2024
2025        if self.controlInitialized:
2026            # Then the base control is available for configuration;
2027            # take action on base control based on new settings, as appropriate.
2028            if 'useFixedWidthFont' in kwargs:
2029                # Set control font - fixed width by default
2030                self._setFont()
2031
2032            if 'reset_mask' in reset_args:
2033##                dbg('reset mask')
2034                curvalue = self._GetValue()
2035                if curvalue.strip():
2036                    try:
2037##                        dbg('attempting to _SetInitialValue(%s)' % self._GetValue())
2038                        self._SetInitialValue(self._GetValue())
2039                    except Exception as e:
2040##                        dbg('exception caught:', e)
2041##                        dbg("current value doesn't work; attempting to reset to template")
2042                        self._SetInitialValue()
2043                else:
2044##                    dbg('attempting to _SetInitialValue() with template')
2045                    self._SetInitialValue()
2046
2047            elif 'useParensForNegatives' in kwargs:
2048                newvalue = self._getSignedValue()[0]
2049
2050                if newvalue is not None:
2051                    # Adjust for new mask:
2052                    if len(newvalue) < len(self._mask):
2053                        newvalue += ' '
2054                    elif len(newvalue) > len(self._mask):
2055                        if newvalue[-1] in (' ', ')'):
2056                            newvalue = newvalue[:-1]
2057
2058##                    dbg('reconfiguring value for parens:"%s"' % newvalue)
2059                    self._SetValue(newvalue)
2060
2061                    if self._prevValue != newvalue:
2062                        self._prevValue = newvalue  # disallow undo of sign type
2063
2064            if self._autofit:
2065##                dbg('calculated size:', self._CalcSize())
2066                self.SetClientSize(self._CalcSize())
2067                width = self.GetSize().width
2068                height = self.GetBestSize().height
2069##                dbg('setting client size to:', (width, height))
2070                self.SetInitialSize((width, height))
2071
2072            # Set value/type-specific formatting
2073            self._applyFormatting()
2074##        dbg(indent=0, suspend=0)
2075
2076    def SetMaskParameters(self, **kwargs):
2077        """ old name for the SetCtrlParameters function  (DEPRECATED)"""
2078        return self.SetCtrlParameters(**kwargs)
2079
2080
2081    def GetCtrlParameter(self, paramname):
2082        """
2083        Routine for retrieving the value of any given parameter
2084        """
2085        if paramname.replace('Color','Colour') in MaskedEditMixin.valid_ctrl_params:
2086            return getattr(self, '_' + paramname.replace('Color', 'Colour'))
2087        elif paramname in Field.valid_params:
2088            return self._ctrl_constraints._GetParameter(paramname)
2089        else:
2090            TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname))
2091
2092    def GetMaskParameter(self, paramname):
2093        """ old name for the GetCtrlParameters function  (DEPRECATED)"""
2094        return self.GetCtrlParameter(paramname)
2095
2096
2097## This idea worked, but Boa was unable to use this solution...
2098##    def _attachMethod(self, func):
2099##        import new
2100##        setattr(self, func.__name__, new.instancemethod(func, self, self.__class__))
2101##
2102##
2103##    def _DefinePropertyFunctions(exposed_params):
2104##        for param in exposed_params:
2105##            propname = param[0].upper() + param[1:]
2106##
2107##            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
2108##            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
2109##            self._attachMethod(locals()['Set%s' % propname])
2110##            self._attachMethod(locals()['Get%s' % propname])
2111##
2112##            if param.find('Colour') != -1:
2113##                # add non-british spellings, for backward-compatibility
2114##                propname.replace('Colour', 'Color')
2115##
2116##                exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
2117##                exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
2118##                self._attachMethod(locals()['Set%s' % propname])
2119##                self._attachMethod(locals()['Get%s' % propname])
2120##
2121
2122
2123    def SetFieldParameters(self, field_index, **kwargs):
2124        """
2125        Routine provided to modify the parameters of a given field.
2126        Because changes to fields can affect the overall control,
2127        direct access to the fields is prevented, and the control
2128        is always "reconfigured" after setting a field parameter.
2129        (See maskededit module overview for the list of valid field-level
2130        parameters.)
2131        """
2132        if field_index not in self._field_indices:
2133            ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
2134            ie.index = field_index
2135            raise ie
2136        # set parameters as requested:
2137        self._fields[field_index]._SetParameters(**kwargs)
2138
2139        # Possibly reprogram control template due to resulting changes, and ensure
2140        # control-level params are still propagated to fields:
2141        self._configure(self._previous_mask)
2142        self._fields[field_index]._ValidateParameters(**kwargs)
2143
2144        if self.controlInitialized:
2145            if 'fillChar' in kwargs or 'defaultValue' in kwargs:
2146                self._SetInitialValue()
2147
2148                if self._autofit:
2149                    # this is tricky, because, as Robin explains:
2150                    # "Basically there are two sizes to deal with, that are potentially
2151                    #  different.  The client size is the inside size and may, depending
2152                    #  on platform, exclude the borders and such.  The normal size is
2153                    #  the outside size that does include the borders.  What you are
2154                    #  calculating (in _CalcSize) is the client size, but the sizers
2155                    #  deal with the full size and so that is the minimum size that
2156                    #  we need to set with SetInitialSize.  The root of the problem is
2157                    #  that in _calcSize the current client size height is returned,
2158                    #  instead of a height based on the current font.  So I suggest using
2159                    #  _calcSize to just get the width, and then use GetBestSize to
2160                    #  get the height."
2161                    self.SetClientSize(self._CalcSize())
2162                    width = self.GetSize().width
2163                    height = self.GetBestSize().height
2164                    self.SetInitialSize((width, height))
2165
2166
2167            # Set value/type-specific formatting
2168            self._applyFormatting()
2169
2170
2171    def GetFieldParameter(self, field_index, paramname):
2172        """
2173        Routine provided for getting a parameter of an individual field.
2174        """
2175        if field_index not in self._field_indices:
2176            ie = IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
2177            ie.index = field_index
2178            raise ie
2179        elif paramname in Field.valid_params:
2180            return self._fields[field_index]._GetParameter(paramname)
2181        else:
2182            ae = AttributeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
2183            ae.attribute = paramname
2184            raise ae
2185
2186
2187    def _SetKeycodeHandler(self, keycode, func):
2188        """
2189        This function adds and/or replaces key event handling functions
2190        used by the control.  <func> should take the event as argument
2191        and return False if no further action on the key is necessary.
2192        """
2193        if func:
2194            self._keyhandlers[keycode] = func
2195        elif keycode in self._keyhandlers:
2196            del self._keyhandlers[keycode]
2197
2198
2199    def _SetKeyHandler(self, char, func):
2200        """
2201        This function adds and/or replaces key event handling functions
2202        for ascii characters.  <func> should take the event as argument
2203        and return False if no further action on the key is necessary.
2204        """
2205        self._SetKeycodeHandler(ord(char), func)
2206
2207
2208    def _AddNavKeycode(self, keycode, handler=None):
2209        """
2210        This function allows a derived subclass to augment the list of
2211        keycodes that are considered "navigational" keys.
2212        """
2213        self._nav.append(keycode)
2214        if handler:
2215            self._keyhandlers[keycode] = handler
2216        elif keycode in self.keyhandlers:
2217            del self._keyhandlers[keycode]
2218
2219
2220
2221    def _AddNavKey(self, char, handler=None):
2222        """
2223        This function is a convenience function so you don't have to
2224        remember to call ord() for ascii chars to be used for navigation.
2225        """
2226        self._AddNavKeycode(ord(char), handler)
2227
2228
2229    def _GetNavKeycodes(self):
2230        """
2231        This function retrieves the current list of navigational keycodes for
2232        the control.
2233        """
2234        return self._nav
2235
2236
2237    def _SetNavKeycodes(self, keycode_func_tuples):
2238        """
2239        This function allows you to replace the current list of keycode processed
2240        as navigation keys, and bind associated optional keyhandlers.
2241        """
2242        self._nav = []
2243        for keycode, func in keycode_func_tuples:
2244            self._nav.append(keycode)
2245            if func:
2246                self._keyhandlers[keycode] = func
2247            elif keycode in self.keyhandlers:
2248                del self._keyhandlers[keycode]
2249
2250
2251    def _processMask(self, mask):
2252        """
2253        This subroutine expands {n} syntax in mask strings, and looks for escaped
2254        special characters and returns the expanded mask, and an dictionary
2255        of booleans indicating whether or not a given position in the mask is
2256        a mask character or not.
2257        """
2258##        dbg('_processMask: mask', mask, indent=1)
2259        # regular expression for parsing c{n} syntax:
2260        rex = re.compile('([' + "".join(maskchars) + '])\{(\d+)\}')
2261        s = mask
2262        match = rex.search(s)
2263        while match:    # found an(other) occurrence
2264            maskchr = s[match.start(1):match.end(1)]            # char to be repeated
2265            repcount = int(s[match.start(2):match.end(2)])      # the number of times
2266            replacement = "".join( maskchr * repcount)  # the resulting substr
2267            s = s[:match.start(1)] + replacement + s[match.end(2)+1:]   #account for trailing '}'
2268            match = rex.search(s)                               # look for another such entry in mask
2269
2270        self._decimalChar = self._ctrl_constraints._decimalChar
2271        self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar
2272
2273        self._isFloat      = _isFloatingPoint(s) and not self._ctrl_constraints._validRegex
2274        self._isInt      = _isInteger(s) and not self._ctrl_constraints._validRegex
2275        self._signOk     = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt)
2276        self._useParens  = self._ctrl_constraints._useParensForNegatives
2277        self._isNeg      = False
2278####        dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens)
2279####        dbg('isFloatingPoint(%s)?' % (s), _isFloatingPoint(s),
2280##            'ctrl regex:', self._ctrl_constraints._validRegex)
2281
2282        if self._signOk and s[0] != ' ':
2283            s = ' ' + s
2284            if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ':
2285                self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue
2286            self._signpos = 0
2287
2288            if self._useParens:
2289                s += ' '
2290                self._ctrl_constraints._defaultValue += ' '
2291
2292
2293        # Now, go build up a dictionary of booleans, indexed by position,
2294        # indicating whether or not a given position is masked or not.
2295        # Also, strip out any '|' chars, adjusting the mask as necessary,
2296        # marking the appropriate positions for field boundaries:
2297        ismasked = {}
2298        explicit_field_boundaries = []
2299        s = list(s)
2300        i = 0
2301        while i < len(s):
2302            if s[i] == '\\':            # if escaped character:
2303                ismasked[i] = False     #     mark position as not a mask char
2304                if i+1 < len(s):        #     if another char follows...
2305                    del s[i]            #         elide the '\'
2306                    if s[i] == '\\':    #         if next char also a '\', char is a literal '\'
2307                        del s[i]        #             elide the 2nd '\' as well
2308                i += 1                  # increment to next char
2309            elif s[i] == '|':
2310                del s[i]                    #         elide the '|'
2311                explicit_field_boundaries.append(i)
2312                                            # keep index where it is:
2313            else:                       # else if special char, mark position accordingly
2314                ismasked[i] = s[i] in maskchars
2315####                dbg('ismasked[%d]:' % i, ismasked[i], ''.join(s))
2316                i += 1                      # increment to next char
2317####        dbg('ismasked:', ismasked)
2318        s = ''.join(s)
2319##        dbg('new mask: "%s"' % s, indent=0)
2320
2321        return s, ismasked, explicit_field_boundaries
2322
2323
2324    def _calcFieldExtents(self):
2325        """
2326        Subroutine responsible for establishing/configuring field instances with
2327        indices and editable extents appropriate to the specified mask, and building
2328        the lookup table mapping each position to the corresponding field.
2329        """
2330        self._lookupField = {}
2331        if self._mask:
2332
2333            ## Create dictionary of positions,characters in mask
2334            self.maskdict = {}
2335            for charnum in range( len( self._mask)):
2336                self.maskdict[charnum] = self._mask[charnum:charnum+1]
2337
2338            # For the current mask, create an ordered list of field extents
2339            # and a dictionary of positions that map to field indices:
2340
2341            if self._signOk: start = 1
2342            else: start = 0
2343
2344            if self._isFloat:
2345                # Skip field "discovery", and just construct a 2-field control with appropriate
2346                # constraints for a floating-point entry.
2347
2348                # .setdefault always constructs 2nd argument even if not needed, so we do this
2349                # the old-fashioned way...
2350                if 0 not in self._fields:
2351                    self._fields[0] = Field()
2352                if 1 not in self._fields:
2353                    self._fields[1] = Field()
2354
2355                self._decimalpos = self._mask.find('.')
2356##                dbg('decimal pos =', self._decimalpos)
2357
2358                formatcodes = self._fields[0]._GetParameter('formatcodes')
2359                if 'R' not in formatcodes: formatcodes += 'R'
2360                self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos),
2361                                               mask=self._mask[start:self._decimalpos], formatcodes=formatcodes)
2362                end = len(self._mask)
2363                if self._signOk and self._useParens:
2364                    end -= 1
2365                self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end),
2366                                               mask=self._mask[self._decimalpos+1:end])
2367
2368                for i in range(self._decimalpos+1):
2369                    self._lookupField[i] = 0
2370
2371                for i in range(self._decimalpos+1, len(self._mask)+1):
2372                    self._lookupField[i] = 1
2373
2374            elif self._isInt:
2375                # Skip field "discovery", and just construct a 1-field control with appropriate
2376                # constraints for a integer entry.
2377                if 0 not in self._fields:
2378                    self._fields[0] = Field(index=0)
2379                end = len(self._mask)
2380                if self._signOk and self._useParens:
2381                    end -= 1
2382                self._fields[0]._SetParameters(index=0, extent=(start, end),
2383                                               mask=self._mask[start:end])
2384                for i in range(len(self._mask)+1):
2385                    self._lookupField[i] = 0
2386            else:
2387                # generic control; parse mask to figure out where the fields are:
2388                field_index = 0
2389                pos = 0
2390                i = self._findNextEntry(pos,adjustInsert=False)  # go to 1st entry point:
2391                if i < len(self._mask):   # no editable chars!
2392                    for j in range(pos, i+1):
2393                        self._lookupField[j] = field_index
2394                    pos = i       # figure out field for 1st editable space:
2395
2396                while i <= len(self._mask):
2397####                    dbg('searching: outer field loop: i = ', i)
2398                    if self._isMaskChar(i):
2399####                        dbg('1st char is mask char; recording edit_start=', i)
2400                        edit_start = i
2401                        # Skip to end of editable part of current field:
2402                        while i < len(self._mask) and self._isMaskChar(i):
2403                            self._lookupField[i] = field_index
2404                            i += 1
2405                            if i in self._explicit_field_boundaries:
2406                                break
2407####                        dbg('edit_end =', i)
2408                        edit_end = i
2409                        self._lookupField[i] = field_index
2410####                        dbg('%d in self._fields?' % field_index, field_index in self._fields)
2411                        if field_index not in self._fields:
2412                            kwargs = Field.valid_params.copy()
2413                            kwargs['index'] = field_index
2414                            kwargs['extent'] = (edit_start, edit_end)
2415                            kwargs['mask'] = self._mask[edit_start:edit_end]
2416                            self._fields[field_index] = Field(**kwargs)
2417                        else:
2418                            self._fields[field_index]._SetParameters(
2419                                                                index=field_index,
2420                                                                extent=(edit_start, edit_end),
2421                                                                mask=self._mask[edit_start:edit_end])
2422                    pos = i
2423                    i = self._findNextEntry(pos, adjustInsert=False)  # go to next field:
2424####                    dbg('next entry:', i)
2425                    if i > pos:
2426                        for j in range(pos, i+1):
2427                            self._lookupField[j] = field_index
2428                    if i >= len(self._mask):
2429                        break           # if past end, we're done
2430                    else:
2431                        field_index += 1
2432####                        dbg('next field:', field_index)
2433
2434        indices = list(self._fields.keys())
2435        indices.sort()
2436        self._field_indices = indices[1:]
2437####        dbg('lookupField map:', indent=1)
2438##        for i in range(len(self._mask)):
2439####            dbg('pos %d:' % i, self._lookupField[i])
2440####        dbg(indent=0)
2441
2442        # Verify that all field indices specified are valid for mask:
2443        for index in self._fields.keys():
2444            if index not in [-1] + list(self._lookupField.values()):
2445                ie = IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
2446                ie.index = index
2447                raise ie
2448
2449
2450
2451    def _calcTemplate(self, reset_fillchar, reset_default):
2452        """
2453        Subroutine for processing current fillchars and default values for
2454        whole control and individual fields, constructing the resulting
2455        overall template, and adjusting the current value as necessary.
2456        """
2457        default_set = False
2458        if self._ctrl_constraints._defaultValue:
2459            default_set = True
2460        else:
2461            for field in self._fields.values():
2462                if field._defaultValue and not reset_default:
2463                    default_set = True
2464##        dbg('default set?', default_set)
2465
2466        # Determine overall new template for control, and keep track of previous
2467        # values, so that current control value can be modified as appropriate:
2468        if self.controlInitialized: curvalue = list(self._GetValue())
2469        else:                       curvalue = None
2470
2471        if hasattr(self, '_fillChar'): old_fillchars = self._fillChar
2472        else:                          old_fillchars = None
2473
2474        if hasattr(self, '_template'): old_template = self._template
2475        else:                          old_template = None
2476
2477        self._template = ""
2478
2479        self._fillChar = {}
2480        reset_value = False
2481
2482        for field in self._fields.values():
2483            field._template = ""
2484
2485        for pos in range(len(self._mask)):
2486####            dbg('pos:', pos)
2487            field = self._FindField(pos)
2488####            dbg('field:', field._index)
2489            start, end = field._extent
2490
2491            if pos == 0 and self._signOk:
2492                self._template = ' ' # always make 1st 1st position blank, regardless of fillchar
2493            elif self._isFloat and pos == self._decimalpos:
2494                self._template += self._decimalChar
2495            elif self._isMaskChar(pos):
2496                if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar:
2497                    fillChar = field._fillChar
2498                else:
2499                    fillChar = self._ctrl_constraints._fillChar
2500                self._fillChar[pos] = fillChar
2501
2502                # Replace any current old fillchar with new one in current value;
2503                # if action required, set reset_value flag so we can take that action
2504                # after we're all done
2505                if self.controlInitialized and old_fillchars and pos in old_fillchars and curvalue:
2506                    if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar:
2507                        reset_value = True
2508                        curvalue[pos] = fillChar
2509
2510                if not field._defaultValue and not self._ctrl_constraints._defaultValue:
2511####                    dbg('no default value')
2512                    self._template += fillChar
2513                    field._template += fillChar
2514
2515                elif field._defaultValue and not reset_default:
2516####                    dbg('len(field._defaultValue):', len(field._defaultValue))
2517####                    dbg('pos-start:', pos-start)
2518                    if len(field._defaultValue) > pos-start:
2519####                        dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start])
2520                        self._template += field._defaultValue[pos-start]
2521                        field._template += field._defaultValue[pos-start]
2522                    else:
2523####                        dbg('field default not long enough; using fillChar')
2524                        self._template += fillChar
2525                        field._template += fillChar
2526                else:
2527                    if len(self._ctrl_constraints._defaultValue) > pos:
2528####                        dbg('using control default')
2529                        self._template += self._ctrl_constraints._defaultValue[pos]
2530                        field._template += self._ctrl_constraints._defaultValue[pos]
2531                    else:
2532####                        dbg('ctrl default not long enough; using fillChar')
2533                        self._template += fillChar
2534                        field._template += fillChar
2535####                dbg('field[%d]._template now "%s"' % (field._index, field._template))
2536####                dbg('self._template now "%s"' % self._template)
2537            else:
2538                self._template += self._mask[pos]
2539
2540        self._fields[-1]._template = self._template     # (for consistency)
2541
2542        if curvalue:    # had an old value, put new one back together
2543            newvalue = "".join(curvalue)
2544        else:
2545            newvalue = None
2546
2547        if default_set:
2548            self._defaultValue = self._template
2549##            dbg('self._defaultValue:', self._defaultValue)
2550            if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
2551####                dbg(indent=0)
2552                ve = ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name))
2553                ve.value = self._defaultValue
2554                raise ve
2555
2556            # if no fillchar change, but old value == old template, replace it:
2557            if newvalue == old_template:
2558                newvalue = self._template
2559                reset_value = True
2560        else:
2561            self._defaultValue = None
2562
2563        if reset_value:
2564##            dbg('resetting value to: "%s"' % newvalue)
2565            pos = self._GetInsertionPoint()
2566            sel_start, sel_to = self._GetSelection()
2567            self._SetValue(newvalue)
2568            self._SetInsertionPoint(pos)
2569            self._SetSelection(sel_start, sel_to)
2570
2571
2572    def _propagateConstraints(self, **reset_args):
2573        """
2574        Subroutine for propagating changes to control-level constraints and
2575        formatting to the individual fields as appropriate.
2576        """
2577        parent_codes = self._ctrl_constraints._formatcodes
2578        parent_includes = self._ctrl_constraints._includeChars
2579        parent_excludes = self._ctrl_constraints._excludeChars
2580        for i in self._field_indices:
2581            field = self._fields[i]
2582            inherit_args = {}
2583            if len(self._field_indices) == 1:
2584                inherit_args['formatcodes'] = parent_codes
2585                inherit_args['includeChars'] = parent_includes
2586                inherit_args['excludeChars'] = parent_excludes
2587            else:
2588                field_codes = current_codes = field._GetParameter('formatcodes')
2589                for c in parent_codes:
2590                    if c not in field_codes: field_codes += c
2591                if field_codes != current_codes:
2592                    inherit_args['formatcodes'] = field_codes
2593
2594                include_chars = current_includes = field._GetParameter('includeChars')
2595                for c in parent_includes:
2596                    if not c in include_chars: include_chars += c
2597                if include_chars != current_includes:
2598                    inherit_args['includeChars'] = include_chars
2599
2600                exclude_chars = current_excludes = field._GetParameter('excludeChars')
2601                for c in parent_excludes:
2602                    if not c in exclude_chars: exclude_chars += c
2603                if exclude_chars != current_excludes:
2604                    inherit_args['excludeChars'] = exclude_chars
2605
2606            if 'defaultValue' in reset_args and reset_args['defaultValue']:
2607                inherit_args['defaultValue'] = ""   # (reset for field)
2608
2609            for param in Field.propagating_params:
2610####                dbg('%s in reset_args?' % param, param in reset_args)
2611####                dbg('%(param)s in reset_args and reset_args[%(param)s]?' % locals(), param in reset_args and reset_args[param])
2612                if param in reset_args:
2613                    inherit_args[param] = self.GetCtrlParameter(param)
2614####                    dbg('inherit_args[%s]' % param, inherit_args[param])
2615
2616            if inherit_args:
2617                field._SetParameters(**inherit_args)
2618                field._ValidateParameters(**inherit_args)
2619
2620
2621    def _validateChoices(self):
2622        """
2623        Subroutine that validates that all choices for given fields are at
2624        least of the necessary length, and that they all would be valid pastes
2625        if pasted into their respective fields.
2626        """
2627        for field in self._fields.values():
2628            if field._choices:
2629                index = field._index
2630                if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices:
2631##                    dbg('skipping (duplicate) choice validation of field 0')
2632                    continue
2633####                dbg('checking for choices for field', field._index)
2634                start, end = field._extent
2635                field_length = end - start
2636####                dbg('start, end, length:', start, end, field_length)
2637                for choice in field._choices:
2638####                    dbg('testing "%s"' % choice)
2639                    valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
2640                    if not valid_paste:
2641####                        dbg(indent=0)
2642                        ve = ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name))
2643                        ve.value = choice
2644                        ve.index = index
2645                        raise ve
2646                    elif replace_to > end:
2647####                        dbg(indent=0)
2648                        ve = ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
2649                        ve.value = choice
2650                        ve.index = index
2651                        raise ve
2652
2653####                    dbg(choice, 'valid in field', index)
2654
2655
2656    def _configure(self, mask, **reset_args):
2657        """
2658        This function sets flags for automatic styling options.  It is
2659        called whenever a control or field-level parameter is set/changed.
2660
2661        This routine does the bulk of the interdependent parameter processing, determining
2662        the field extents of the mask if changed, resetting parameters as appropriate,
2663        determining the overall template value for the control, etc.
2664
2665        reset_args is supplied if called from control's .SetCtrlParameters()
2666        routine, and indicates which if any parameters which can be
2667        overridden by individual fields have been reset by request for the
2668        whole control.
2669
2670        """
2671##        dbg(suspend=1)
2672##        dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1)
2673
2674        # Preprocess specified mask to expand {n} syntax, handle escaped
2675        # mask characters, etc and build the resulting positionally keyed
2676        # dictionary for which positions are mask vs. template characters:
2677        self._mask, self._ismasked, self._explicit_field_boundaries = self._processMask(mask)
2678        self._masklength = len(self._mask)
2679####        dbg('processed mask:', self._mask)
2680
2681        # Preserve original mask specified, for subsequent reprocessing
2682        # if parameters change.
2683##        dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask)
2684        self._previous_mask = mask    # save unexpanded mask for next time
2685            # Set expanded mask and extent of field -1 to width of entire control:
2686        self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength))
2687
2688        # Go parse mask to determine where each field is, construct field
2689        # instances as necessary, configure them with those extents, and
2690        # build lookup table mapping each position for control to its corresponding
2691        # field.
2692####        dbg('calculating field extents')
2693
2694        self._calcFieldExtents()
2695
2696
2697        # Go process defaultValues and fillchars to construct the overall
2698        # template, and adjust the current value as necessary:
2699        reset_fillchar = 'fillChar' in reset_args and reset_args['fillChar']
2700        reset_default = 'defaultValue' in reset_args and reset_args['defaultValue']
2701
2702####        dbg('calculating template')
2703        self._calcTemplate(reset_fillchar, reset_default)
2704
2705        # Propagate control-level formatting and character constraints to each
2706        # field if they don't already have them; if only one field, propagate
2707        # control-level validation constraints to field as well:
2708####        dbg('propagating constraints')
2709        self._propagateConstraints(**reset_args)
2710
2711
2712        if self._isFloat and self._fields[0]._groupChar == self._decimalChar:
2713            raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' %
2714                                 (self._fields[0]._groupChar, self._decimalChar) )
2715
2716####        dbg('fields:', indent=1)
2717##        for i in [-1] + self._field_indices:
2718####            dbg('field %d:' % i, self._fields[i].__dict__)
2719####        dbg(indent=0)
2720
2721        # Set up special parameters for numeric control, if appropriate:
2722        if self._signOk:
2723            self._signpos = 0   # assume it starts here, but it will move around on floats
2724            signkeys = ['-', '+', ' ']
2725            if self._useParens:
2726                signkeys += ['(', ')']
2727            for key in signkeys:
2728                keycode = ord(key)
2729                if keycode not in self._keyhandlers:
2730                    self._SetKeyHandler(key, self._OnChangeSign)
2731        elif self._isInt or self._isFloat:
2732            signkeys = ['-', '+', ' ', '(', ')']
2733            for key in signkeys:
2734                keycode = ord(key)
2735                if keycode in self._keyhandlers and self._keyhandlers[keycode] == self._OnChangeSign:
2736                    self._SetKeyHandler(key, None)
2737
2738
2739
2740        if self._isFloat or self._isInt:
2741            if self.controlInitialized:
2742                value = self._GetValue()
2743####                dbg('value: "%s"' % value, 'len(value):', len(value),
2744##                    'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask))
2745                if len(value) < len(self._ctrl_constraints._mask):
2746                    newvalue = value
2747                    if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1:
2748                        newvalue += ' '
2749                    if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1:
2750                        newvalue = ' ' + newvalue
2751                    if len(newvalue) < len(self._ctrl_constraints._mask):
2752                        if self._ctrl_constraints._alignRight:
2753                            newvalue = newvalue.rjust(len(self._ctrl_constraints._mask))
2754                        else:
2755                            newvalue = newvalue.ljust(len(self._ctrl_constraints._mask))
2756##                    dbg('old value: "%s"' % value)
2757##                    dbg('new value: "%s"' % newvalue)
2758                    try:
2759                        self._ChangeValue(newvalue)
2760                    except Exception as e:
2761##                        dbg('exception raised:', e, 'resetting to initial value')
2762                        self._SetInitialValue()
2763
2764                elif len(value) > len(self._ctrl_constraints._mask):
2765                    newvalue = value
2766                    if not self._useParens and newvalue[-1] == ' ':
2767                        newvalue = newvalue[:-1]
2768                    if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask):
2769                        newvalue = newvalue[1:]
2770                    if not self._signOk:
2771                        newvalue, signpos, right_signpos = self._getSignedValue(newvalue)
2772
2773##                    dbg('old value: "%s"' % value)
2774##                    dbg('new value: "%s"' % newvalue)
2775                    try:
2776                        self._ChangeValue(newvalue)
2777                    except Exception as e:
2778##                        dbg('exception raised:', e, 'resetting to initial value')
2779                        self._SetInitialValue()
2780                elif not self._signOk and ('(' in value or '-' in value):
2781                    newvalue, signpos, right_signpos = self._getSignedValue(value)
2782##                    dbg('old value: "%s"' % value)
2783##                    dbg('new value: "%s"' % newvalue)
2784                    try:
2785                        self._ChangeValue(newvalue)
2786                    except e:
2787##                        dbg('exception raised:', e, 'resetting to initial value')
2788                        self._SetInitialValue()
2789
2790            # Replace up/down arrow default handling:
2791            # make down act like tab, up act like shift-tab:
2792
2793####            dbg('Registering numeric navigation and control handlers (if not already set)')
2794            if wx.WXK_DOWN not in self._keyhandlers:
2795                self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField)
2796            if wx.WXK_NUMPAD_DOWN not in self._keyhandlers:
2797                self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField)
2798            if wx.WXK_UP not in self._keyhandlers:
2799                self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric)  # (adds "shift" to up arrow, and calls _OnChangeField)
2800            if wx.WXK_NUMPAD_UP not in self._keyhandlers:
2801                self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric)  # (adds "shift" to up arrow, and calls _OnChangeField)
2802
2803            # On ., truncate contents right of cursor to decimal point (if any)
2804            # leaves cursor after decimal point if floating point, otherwise at 0.
2805            if ord(self._decimalChar) not in self._keyhandlers or self._keyhandlers[ord(self._decimalChar)] != self._OnDecimalPoint:
2806                self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint)
2807
2808            if ord(self._shiftDecimalChar) not in self._keyhandlers or self._keyhandlers[ord(self._shiftDecimalChar)] != self._OnChangeField:
2809                self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField)   # (Shift-'.' == '>' on US keyboards)
2810
2811            # Allow selective insert of groupchar in numbers:
2812            if ord(self._fields[0]._groupChar) not in self._keyhandlers or self._keyhandlers[ord(self._fields[0]._groupChar)] != self._OnGroupChar:
2813                self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar)
2814
2815##        dbg(indent=0, suspend=0)
2816
2817
2818    def _SetInitialValue(self, value=""):
2819        """
2820        fills the control with the generated or supplied default value.
2821        It will also set/reset the font if necessary and apply
2822        formatting to the control at this time.
2823        """
2824##        dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
2825        if not value:
2826            self._prevValue = self._curValue = self._template
2827            # don't apply external validation rules in this case, as template may
2828            # not coincide with "legal" value...
2829            try:
2830                if isinstance(self, wx.TextCtrl):
2831                    self._ChangeValue(self._curValue)  # note the use of "raw" ._ChangeValue()...
2832                else:
2833                    self._SetValue(self._curValue)  # note the use of "raw" ._SetValue()...
2834            except Exception as e:
2835##                dbg('exception thrown:', e, indent=0)
2836                raise
2837        else:
2838            # Otherwise apply validation as appropriate to passed value:
2839####            dbg('value = "%s", length:' % value, len(value))
2840            self._prevValue = self._curValue = value
2841            try:
2842                if isinstance(self, wx.TextCtrl):
2843                    self.ChangeValue(value)            # use public (validating) .SetValue()
2844                else:
2845                    self.SetValue(value)
2846            except Exception as e:
2847##                dbg('exception thrown:', e, indent=0)
2848                raise
2849
2850
2851        # Set value/type-specific formatting
2852        self._applyFormatting()
2853##        dbg(indent=0)
2854
2855
2856    def _calcSize(self, size=None):
2857        """ Calculate automatic size if allowed; must be called after the base control is instantiated"""
2858####        dbg('MaskedEditMixin::_calcSize', indent=1)
2859        cont = (size is None or size == wx.DefaultSize)
2860
2861        if cont and self._autofit:
2862####            dbg('isinstance(self, wx.lib.masked.numctrl.NumCtrl): "%s"' % self.__class__)
2863            # sizing of numctrl when using proportional font and
2864            # wxPython 2.9 is not working when using "M"
2865            # GetTextExtent returns a width which is way to large
2866            # instead we use '9' for numctrl and a selection of
2867            # characters instead of just 'M' for textctrl and combobox
2868            # where the mask is larger then 10 characters long
2869            if isinstance(self, wx.lib.masked.numctrl.NumCtrl):
2870                sizing_text = '9' * self._masklength
2871                wAdjust = 8
2872            elif isinstance(self, wx.lib.masked.combobox.ComboBox):
2873                if self._masklength > 10:
2874                    tC, sC = divmod(self._masklength, 10.0)
2875                    sizing_text = 'FDSJKLREUI' * int(tC)
2876                    sizing_text += 'M' * int(sC)
2877                    wAdjust = 26
2878                else:
2879                    sizing_text = ""
2880                    for cn in range(self._masklength):
2881                        if cn % 2:
2882                            sizing_text += "M"
2883                        else:
2884                            sizing_text += "I"
2885                    wAdjust = 4
2886            else:
2887                if self._masklength > 10:
2888                    tC, sC = divmod(self._masklength, 10.0)
2889                    sizing_text = 'FDSJKLREUI' * int(tC)
2890                    sizing_text += 'M' * int(sC)
2891                else:
2892                    sizing_text = ""
2893                    for cn in range(self._masklength):
2894                        if cn % 2:
2895                            sizing_text += "M"
2896                        else:
2897                            sizing_text += "I"
2898                wAdjust = 4
2899            if wx.Platform != "__WXMSW__":   # give it a little extra space
2900                sizing_text += 'M'
2901            if wx.Platform == "__WXMAC__":   # give it even a little more...
2902                sizing_text += 'M'
2903####            dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
2904            w, h = self.GetTextExtent(sizing_text)
2905            size = (w+wAdjust, self.GetSize().height)
2906####            dbg('size:', size, indent=0)
2907        return size
2908
2909
2910    def _setFont(self):
2911        """ Set the control's font typeface -- pass the font name as str."""
2912####        dbg('MaskedEditMixin::_setFont', indent=1)
2913        if not self._useFixedWidthFont:
2914            self._font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
2915        else:
2916            font = self.GetFont()   # get size, weight, etc from current font
2917            points = font.GetPointSize()
2918            if 'wxMac' in wx.PlatformInfo \
2919               and self.GetWindowVariant() == wx.WINDOW_VARIANT_MINI:
2920                points -= 1
2921
2922            # Set to teletype font (guaranteed to be mappable to all wxWindows
2923            # platforms:
2924            self._font = wx.Font( points, wx.FONTFAMILY_TELETYPE, font.GetStyle(),
2925                                 font.GetWeight(), font.GetUnderlined())
2926####            dbg('font string: "%s"' % font.GetNativeFontInfo().ToString())
2927
2928        self.SetFont(self._font)
2929####        dbg(indent=0)
2930
2931
2932    def _OnTextChange(self, event):
2933        """
2934        Handler for EVT_TEXT event.
2935        self._Change() is provided for subclasses, and may return False to
2936        skip this method logic.  This function returns True if the event
2937        detected was a legitimate event, or False if it was a "bogus"
2938        EVT_TEXT event.  (NOTE: There is currently an issue with calling
2939        .SetValue from within the EVT_CHAR handler that causes duplicate
2940        EVT_TEXT events for the same change.)
2941        """
2942        newvalue = self._GetValue()
2943##        dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1)
2944        bValid = False
2945        if self._ignoreChange:      # ie. if an "intermediate text change event"
2946##            dbg(indent=0)
2947            return bValid
2948
2949        ##! WS: For some inexplicable reason, every wx.TextCtrl.SetValue
2950        ## call is generating two (2) EVT_TEXT events.  On certain platforms,
2951        ## (eg. linux/GTK) the 1st is an empty string value.
2952        ## This is the only mechanism I can find to mask this problem:
2953        if newvalue == self._curValue or len(newvalue) == 0:
2954##            dbg('ignoring bogus text change event', indent=0)
2955            pass
2956        else:
2957##            dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue)))
2958            if self._Change():
2959                if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
2960##                    dbg('clearing self._isNeg')
2961                    self._isNeg = False
2962                    text, self._signpos, self._right_signpos = self._getSignedValue()
2963                self._CheckValid()  # Recolor control as appropriate
2964##            dbg('calling event.Skip()')
2965            event.Skip()
2966            bValid = True
2967        self._prevValue = self._curValue    # save for undo
2968        self._curValue = newvalue           # Save last seen value for next iteration
2969##        dbg(indent=0)
2970        return bValid
2971
2972
2973    def _OnKeyDown(self, event):
2974        """
2975        This function allows the control to capture Ctrl-events like Ctrl-tab,
2976        that are not normally seen by the "cooked" EVT_CHAR routine.
2977        """
2978        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
2979        key    = event.GetKeyCode()
2980        if key in self._nav and event.ControlDown():
2981            # then this is the only place we will likely see these events;
2982            # process them now:
2983##            dbg('MaskedEditMixin::OnKeyDown: calling _OnChar')
2984            self._OnChar(event)
2985            return
2986        # else allow regular EVT_CHAR key processing
2987        event.Skip()
2988
2989
2990    def _OnChar(self, event):
2991        """
2992        This is the engine of MaskedEdit controls.  It examines each keystroke,
2993        decides if it's allowed, where it should go or what action to take.
2994        """
2995##        dbg('MaskedEditMixin::_OnChar', indent=1)
2996
2997        # Get keypress value, adjusted by control options (e.g. convert to upper etc)
2998        key = event.GetKeyCode()
2999        orig_pos = self._GetInsertionPoint()
3000        orig_value = self._GetValue()
3001##        dbg('keycode = ', key)
3002##        dbg('current pos = ', orig_pos)
3003##        dbg('current selection = ', self._GetSelection())
3004
3005        if not self._Keypress(key):
3006##            dbg(indent=0)
3007            return
3008
3009        # If no format string for this control, or the control is marked as "read-only",
3010        # skip the rest of the special processing, and just "do the standard thing:"
3011        if not self._mask or not self._IsEditable():
3012            event.Skip()
3013##            dbg(indent=0)
3014            return
3015
3016        # Process navigation and control keys first, with
3017        # position/selection unadulterated:
3018        if key in self._nav + self._control:
3019            if key in self._keyhandlers:
3020                keep_processing = self._keyhandlers[key](event)
3021                if self._GetValue() != orig_value:
3022                    self.modified = True
3023                if not keep_processing:
3024##                    dbg(indent=0)
3025                    return
3026                self._applyFormatting()
3027##                dbg(indent=0)
3028                return
3029
3030        # Else... adjust the position as necessary for next input key,
3031        # and determine resulting selection:
3032        pos = self._adjustPos( orig_pos, key )    ## get insertion position, adjusted as needed
3033        sel_start, sel_to = self._GetSelection()                ## check for a range of selected text
3034##        dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to)
3035
3036        keep_processing = True
3037        # Capture user past end of format field
3038        if pos > len(self.maskdict):
3039##            dbg("field length exceeded:",pos)
3040            keep_processing = False
3041
3042        key = self._adjustKey(pos, key)     # apply formatting constraints to key:
3043
3044        if key in self._keyhandlers:
3045            # there's an override for default behavior; use override function instead
3046##            dbg('using supplied key handler:', self._keyhandlers[key])
3047            keep_processing = self._keyhandlers[key](event)
3048            if self._GetValue() != orig_value:
3049                self.modified = True
3050            if not keep_processing:
3051##                dbg(indent=0)
3052                return
3053            # else skip default processing, but do final formatting
3054        if key in wx_control_keycodes:
3055##            dbg('key in wx_control_keycodes')
3056            event.Skip()                # non-printable; let base control handle it
3057            keep_processing = False
3058        else:
3059            field = self._FindField(pos)
3060
3061            if key < 256:
3062                char = chr(key) # (must work if we got this far)
3063                if not six.PY3:
3064                    char = char.decode(self._defaultEncoding)
3065            else:
3066                char = unichr(event.GetUnicodeKey())
3067##                dbg('unicode char:', char)
3068
3069            excludes = six.text_type()
3070            if not isinstance(field._excludeChars, six.text_type):
3071                if six.PY3:
3072                    excludes += field._excludeChars
3073                else:
3074                    excludes += field._excludeChars.decode(self._defaultEncoding)
3075            if not isinstance(self._ctrl_constraints, six.text_type):
3076                if six.PY3:
3077                    excludes += field._excludeChars
3078                else:
3079                    excludes += self._ctrl_constraints._excludeChars.decode(self._defaultEncoding)
3080            else:
3081                excludes += self._ctrl_constraints._excludeChars
3082
3083            if char in excludes:
3084                keep_processing = False
3085
3086            if keep_processing and self._isCharAllowed( char, pos, checkRegex = True ):
3087##                dbg("key allowed by mask")
3088                # insert key into candidate new value, but don't change control yet:
3089                oldstr = self._GetValue()
3090                newstr, newpos, new_select_to, match_field, match_index = self._insertKey(
3091                                char, pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True)
3092##                dbg("str with '%s' inserted:" % char, '"%s"' % newstr)
3093                if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
3094##                    dbg('not valid; checking to see if adjusted string is:')
3095                    keep_processing = False
3096                    if self._isFloat and newstr != self._template:
3097                        newstr = self._adjustFloat(newstr)
3098##                        dbg('adjusted str:', newstr)
3099                        if self.IsValid(newstr):
3100##                            dbg("it is!")
3101                            keep_processing = True
3102                            wx.CallAfter(self._SetInsertionPoint, self._decimalpos)
3103                    if not keep_processing:
3104##                        dbg("key disallowed by validation")
3105                        if not wx.Validator.IsSilent() and orig_pos == pos:
3106                            wx.Bell()
3107
3108                if keep_processing:
3109                    unadjusted = newstr
3110
3111                    # special case: adjust date value as necessary:
3112                    if self._isDate and newstr != self._template:
3113                        newstr = self._adjustDate(newstr)
3114##                    dbg('adjusted newstr:', newstr)
3115
3116                    if newstr != orig_value:
3117                        self.modified = True
3118
3119                    wx.CallAfter(self._SetValue, newstr)
3120
3121                    # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits:
3122                    if not self.IsDefault() and self._isDate and self._4digityear:
3123                        year2dig = self._dateExtent - 2
3124                        if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]:
3125                            newpos = pos+2
3126
3127##                    dbg('queuing insertion point: (%d)' % newpos)
3128                    wx.CallAfter(self._SetInsertionPoint, newpos)
3129
3130                    if match_field is not None:
3131##                        dbg('matched field')
3132                        self._OnAutoSelect(match_field, match_index)
3133
3134                    if new_select_to != newpos:
3135##                        dbg('queuing selection: (%d, %d)' % (newpos, new_select_to))
3136                        wx.CallAfter(self._SetSelection, newpos, new_select_to)
3137                    else:
3138                        newfield = self._FindField(newpos)
3139                        if newfield != field and newfield._selectOnFieldEntry:
3140##                            dbg('queuing insertion point: (%d)' % newfield._extent[0])
3141                            wx.CallAfter(self._SetInsertionPoint, newfield._extent[0])
3142##                            dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1]))
3143                            wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
3144                        else:
3145                            wx.CallAfter(self._SetSelection, newpos, new_select_to)
3146                    keep_processing = False
3147
3148            elif keep_processing:
3149##                dbg('char not allowed')
3150                keep_processing = False
3151                if (not wx.Validator.IsSilent()) and orig_pos == pos:
3152                    wx.Bell()
3153
3154        self._applyFormatting()
3155
3156        # Move to next insertion point
3157        if keep_processing and key not in self._nav:
3158            pos = self._GetInsertionPoint()
3159            next_entry = self._findNextEntry( pos )
3160            if pos != next_entry:
3161##                dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals())
3162                wx.CallAfter(self._SetInsertionPoint, next_entry )
3163
3164            if self._isTemplateChar(pos):
3165                self._AdjustField(pos)
3166##        dbg(indent=0)
3167
3168
3169    def _FindFieldExtent(self, pos=None, getslice=False, value=None):
3170        """ returns editable extent of field corresponding to
3171        position pos, and, optionally, the contents of that field
3172        in the control or the value specified.
3173        Template chars are bound to the preceding field.
3174        For masks beginning with template chars, these chars are ignored
3175        when calculating the current field.
3176
3177        Eg: with template (###) ###-####,
3178        >>> self._FindFieldExtent(pos=0)
3179        1, 4
3180        >>> self._FindFieldExtent(pos=1)
3181        1, 4
3182        >>> self._FindFieldExtent(pos=5)
3183        1, 4
3184        >>> self._FindFieldExtent(pos=6)
3185        6, 9
3186        >>> self._FindFieldExtent(pos=10)
3187        10, 14
3188        etc.
3189        """
3190##        dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1)
3191
3192        field = self._FindField(pos)
3193        if not field:
3194            if getslice:
3195                return None, None, ""
3196            else:
3197                return None, None
3198        edit_start, edit_end = field._extent
3199        if getslice:
3200            if value is None: value = self._GetValue()
3201            slice = value[edit_start:edit_end]
3202##            dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice)
3203##            dbg(indent=0)
3204            return edit_start, edit_end, slice
3205        else:
3206##            dbg('edit_start:', edit_start, 'edit_end:', edit_end)
3207##            dbg(indent=0)
3208            return edit_start, edit_end
3209
3210
3211    def _FindField(self, pos=None):
3212        """
3213        Returns the field instance in which pos resides.
3214        Template chars are bound to the preceding field.
3215        For masks beginning with template chars, these chars are ignored
3216        when calculating the current field.
3217
3218        """
3219####        dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
3220        if pos is None: pos = self._GetInsertionPoint()
3221        elif pos < 0 or pos > self._masklength:
3222            raise IndexError('position %s out of range of control' % str(pos))
3223
3224        if len(self._fields) == 0:
3225##            dbg(indent=0)
3226            return None
3227
3228        # else...
3229####        dbg(indent=0)
3230        return self._fields[self._lookupField[pos]]
3231
3232    def SetForegroundColour(self, colour):
3233        super(MaskedEditMixin, self).SetForegroundColour(colour)
3234        self._foregroundColour = colour
3235
3236    def SetBackgroundColour(self, colour):
3237        super(MaskedEditMixin, self).SetBackgroundColour(colour)
3238        self._validBackgroundColour = colour
3239
3240    def ClearValue(self):
3241        """ Blanks the current control value by replacing it with the default value."""
3242##        dbg("MaskedEditMixin::ClearValue - value reset to default value (template)")
3243        self._SetValue( self._template )
3244        self._SetInsertionPoint(0)
3245        self.Refresh()
3246
3247    def ClearValueAlt(self):
3248        """ Blanks the current control value by replacing it with the default value.
3249        Using ChangeValue, so not to fire a change event"""
3250##        dbg("MaskedEditMixin::ClearValueAlt - value reset to default value (template)")
3251        self._ChangeValue( self._template )
3252        self._SetInsertionPoint(0)
3253        self.Refresh()
3254
3255    def _baseCtrlEventHandler(self, event):
3256        """
3257        This function is used whenever a key should be handled by the base control.
3258        """
3259        event.Skip()
3260        return False
3261
3262
3263    def _OnUpNumeric(self, event):
3264        """
3265        Makes up-arrow act like shift-tab should; ie. take you to start of
3266        previous field.
3267        """
3268##        dbg('MaskedEditMixin::_OnUpNumeric', indent=1)
3269        event.shiftDown = 1
3270##        dbg('event.ShiftDown()?', event.ShiftDown())
3271        self._OnChangeField(event)
3272##        dbg(indent=0)
3273
3274
3275    def _OnArrow(self, event):
3276        """
3277        Used in response to left/right navigation keys; makes these actions skip
3278        over mask template chars.
3279        """
3280##        dbg("MaskedEditMixin::_OnArrow", indent=1)
3281        pos = self._GetInsertionPoint()
3282        keycode = event.GetKeyCode()
3283        sel_start, sel_to = self._GetSelection()
3284        entry_end = self._goEnd(getPosOnly=True)
3285        if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN, wx.WXK_NUMPAD_RIGHT, wx.WXK_NUMPAD_DOWN):
3286            if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
3287                or ( self._isTemplateChar(pos) and pos >= entry_end) ):
3288##                dbg("can't advance", indent=0)
3289                return False
3290            elif self._isTemplateChar(pos):
3291                self._AdjustField(pos)
3292        elif keycode in (wx.WXK_LEFT, wx.WXK_UP, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1):
3293##            dbg('adjusting field')
3294            self._AdjustField(pos)
3295
3296        # treat as shifted up/down arrows as tab/reverse tab:
3297        if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN):
3298            # remove "shifting" and treat as (forward) tab:
3299            event.shiftDown = False
3300            keep_processing = self._OnChangeField(event)
3301
3302        elif self._FindField(pos)._selectOnFieldEntry:
3303            if( keycode in (wx.WXK_UP, wx.WXK_LEFT, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_LEFT)
3304                and sel_start != 0
3305                and self._isTemplateChar(sel_start-1)
3306                and sel_start != self._masklength
3307                and not self._signOk and not self._useParens):
3308
3309                # call _OnChangeField to handle "ctrl-shifted event"
3310                # (which moves to previous field and selects it.)
3311                event.shiftDown = True
3312                event.sontrolDown = True
3313                keep_processing = self._OnChangeField(event)
3314            elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT)
3315                  and sel_to != self._masklength
3316                  and self._isTemplateChar(sel_to)):
3317
3318                # when changing field to the right, ensure don't accidentally go left instead
3319                event.shiftDown = False
3320                keep_processing = self._OnChangeField(event)
3321            else:
3322                # treat arrows as normal, allowing selection
3323                # as appropriate:
3324##                dbg('using base ctrl event processing')
3325                event.Skip()
3326        else:
3327            if( (sel_to == self._fields[0]._extent[0] and keycode in (wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT) )
3328                or (sel_to == self._masklength and keycode in (wx.WXK_RIGHT, wx.WXK_NUMPAD_RIGHT) ) ):
3329                if not wx.Validator.IsSilent():
3330                    wx.Bell()
3331            else:
3332                # treat arrows as normal, allowing selection
3333                # as appropriate:
3334##                dbg('using base event processing')
3335                event.Skip()
3336
3337        keep_processing = False
3338##        dbg(indent=0)
3339        return keep_processing
3340
3341
3342    def _OnCtrl_S(self, event):
3343        """ Default Ctrl-S handler; prints value information if demo enabled. """
3344##        dbg("MaskedEditMixin::_OnCtrl_S")
3345        if self._demo:
3346            print('MaskedEditMixin.GetValue()       = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue()))
3347            print("Valid? => " + str(self.IsValid()))
3348            print("Current field, start, end, value =", str( self._FindFieldExtent(getslice=True)))
3349        return False
3350
3351
3352    def _OnCtrl_X(self, event=None):
3353        """ Handles ctrl-x keypress in control and Cut operation on context menu.
3354            Should return False to skip other processing. """
3355##        dbg("MaskedEditMixin::_OnCtrl_X", indent=1)
3356        self.Cut()
3357##        dbg(indent=0)
3358        return False
3359
3360    def _OnCtrl_C(self, event=None):
3361        """ Handles ctrl-C keypress in control and Copy operation on context menu.
3362            Uses base control handling. Should return False to skip other processing."""
3363        self.Copy()
3364        return False
3365
3366    def _OnCtrl_V(self, event=None):
3367        """ Handles ctrl-V keypress in control and Paste operation on context menu.
3368            Should return False to skip other processing. """
3369##        dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
3370        self.Paste()
3371##        dbg(indent=0)
3372        return False
3373
3374    def _OnInsert(self, event=None):
3375        """ Handles shift-insert and control-insert operations (paste and copy, respectively)"""
3376##        dbg("MaskedEditMixin::_OnInsert", indent=1)
3377        if event and isinstance(event, wx.KeyEvent):
3378            if event.ShiftDown():
3379                self.Paste()
3380            elif event.ControlDown():
3381                self.Copy()
3382            # (else do nothing)
3383        # (else do nothing)
3384##        dbg(indent=0)
3385        return False
3386
3387    def _OnDelete(self, event=None):
3388        """ Handles shift-delete and delete operations (cut and erase, respectively)"""
3389##        dbg("MaskedEditMixin::_OnDelete", indent=1)
3390        if event and isinstance(event, wx.KeyEvent):
3391            if event.ShiftDown():
3392                self.Cut()
3393            else:
3394                self._OnErase(event)
3395        else:
3396            self._OnErase(event)
3397##        dbg(indent=0)
3398        return False
3399
3400    def _OnCtrl_Z(self, event=None):
3401        """ Handles ctrl-Z keypress in control and Undo operation on context menu.
3402            Should return False to skip other processing. """
3403##        dbg("MaskedEditMixin::_OnCtrl_Z", indent=1)
3404        self.Undo()
3405##        dbg(indent=0)
3406        return False
3407
3408    def _OnCtrl_A(self,event=None):
3409        """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
3410        end = self._goEnd(getPosOnly=True)
3411        if not event or (isinstance(event, wx.KeyEvent) and event.ShiftDown()):
3412            wx.CallAfter(self._SetInsertionPoint, 0)
3413            wx.CallAfter(self._SetSelection, 0, self._masklength)
3414        else:
3415            wx.CallAfter(self._SetInsertionPoint, 0)
3416            wx.CallAfter(self._SetSelection, 0, end)
3417        return False
3418
3419
3420    def _OnErase(self, event=None, just_return_value=False):
3421        """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
3422##        dbg("MaskedEditMixin::_OnErase", indent=1)
3423        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
3424
3425        if event is None:   # called as action routine from Cut() operation.
3426            key = wx.WXK_DELETE
3427        else:
3428            key = event.GetKeyCode()
3429
3430        field = self._FindField(sel_to)
3431        start, end = field._extent
3432        value = self._GetValue()
3433        oldstart = sel_start
3434
3435        # If trying to erase beyond "legal" bounds, disallow operation:
3436        if( (sel_to == 0 and key == wx.WXK_BACK)
3437            or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK)
3438            or (sel_to == self._masklength and sel_start == sel_to and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight)
3439            or (self._signOk and self._useParens
3440                and sel_start == sel_to
3441                and sel_to == self._masklength - 1
3442                and value[sel_to] == ' ' and key in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and not field._insertRight) ):
3443            if not wx.Validator.IsSilent():
3444                wx.Bell()
3445##            dbg(indent=0)
3446            return False
3447
3448
3449        if( field._insertRight                                  # an insert-right field
3450            and value[start:end] != self._template[start:end]   # and field not empty
3451            and sel_start >= start                              # and selection starts in field
3452            and ((sel_to == sel_start                           # and no selection
3453                  and sel_to == end                             # and cursor at right edge
3454                  and key in (wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE))    # and either delete or backspace key
3455                 or                                             # or
3456                 (key == wx.WXK_BACK                               # backspacing
3457                    and (sel_to == end                          # and selection ends at right edge
3458                         or sel_to < end and field._allowInsert)) ) ):  # or allow right insert at any point in field
3459
3460##            dbg('delete left')
3461            # if backspace but left of cursor is empty, adjust cursor right before deleting
3462            while( key == wx.WXK_BACK
3463                   and sel_start == sel_to
3464                   and sel_start < end
3465                   and value[start:sel_start] == self._template[start:sel_start]):
3466                sel_start += 1
3467                sel_to = sel_start
3468
3469##            dbg('sel_start, start:', sel_start, start)
3470
3471            if sel_start == sel_to:
3472                keep = sel_start -1
3473            else:
3474                keep = sel_start
3475            newfield = value[start:keep] + value[sel_to:end]
3476
3477            # handle sign char moving from outside field into the field:
3478            move_sign_into_field = False
3479            if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('):
3480                signchar = value[0]
3481                newfield = signchar + newfield
3482                move_sign_into_field = True
3483##            dbg('cut newfield: "%s"' % newfield)
3484
3485            # handle what should fill in from the left:
3486            left = ""
3487            for i in range(start, end - len(newfield)):
3488                if field._padZero:
3489                    left += '0'
3490                elif( self._signOk and self._isNeg and i == 1
3491                      and ((self._useParens and newfield.find('(') == -1)
3492                           or (not self._useParens and newfield.find('-') == -1)) ):
3493                    left += ' '
3494                else:
3495                    left += self._template[i]   # this can produce strange results in combination with default values...
3496            newfield = left + newfield
3497##            dbg('filled newfield: "%s"' % newfield)
3498
3499            newstr = value[:start] + newfield + value[end:]
3500
3501            # (handle sign located in "mask position" in front of field prior to delete)
3502            if move_sign_into_field:
3503                newstr = ' ' + newstr[1:]
3504            pos = sel_to
3505        else:
3506            # handle erasure of (left) sign, moving selection accordingly...
3507            if self._signOk and sel_start == 0:
3508                newstr = value = ' ' + value[1:]
3509                sel_start += 1
3510
3511            if field._allowInsert and sel_start >= start:
3512                # selection (if any) falls within current insert-capable field:
3513                select_len = sel_to - sel_start
3514                # determine where cursor should end up:
3515                if key == wx.WXK_BACK:
3516                    if select_len == 0:
3517                        newpos = sel_start -1
3518                    else:
3519                        newpos = sel_start
3520                    erase_to = sel_to
3521                else:
3522                    newpos = sel_start
3523                    if sel_to == sel_start:
3524                        erase_to = sel_to + 1
3525                    else:
3526                        erase_to = sel_to
3527
3528                if self._isTemplateChar(newpos) and select_len == 0:
3529                    if self._signOk:
3530                        if value[newpos] in ('(', '-'):
3531                            newpos += 1     # don't move cusor
3532                            newstr = ' ' + value[newpos:]
3533                        elif value[newpos] == ')':
3534                            # erase right sign, but don't move cursor; (matching left sign handled later)
3535                            newstr = value[:newpos] + ' '
3536                        else:
3537                            # no deletion; just move cursor
3538                            newstr = value
3539                    else:
3540                        # no deletion; just move cursor
3541                        newstr = value
3542                else:
3543                    if erase_to > end: erase_to = end
3544                    erase_len = erase_to - newpos
3545
3546                    left = value[start:newpos]
3547##                    dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end])
3548                    right = value[erase_to:end] + self._template[end-erase_len:end]
3549                    pos_adjust = 0
3550                    if field._alignRight:
3551                        rstripped = right.rstrip()
3552                        if rstripped != right:
3553                            pos_adjust = len(right) - len(rstripped)
3554                        right = rstripped
3555
3556                    if not field._insertRight and value[-1] == ')' and end == self._masklength - 1:
3557                        # need to shift ) into the field:
3558                        right = right[:-1] + ')'
3559                        value = value[:-1] + ' '
3560
3561                    newfield = left+right
3562                    if pos_adjust:
3563                        newfield = newfield.rjust(end-start)
3564                        newpos += pos_adjust
3565##                    dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield))
3566                    newstr = value[:start] + newfield + value[end:]
3567
3568                pos = newpos
3569
3570            else:
3571                if sel_start == sel_to:
3572##                    dbg("current sel_start, sel_to:", sel_start, sel_to)
3573                    if key == wx.WXK_BACK:
3574                        sel_start, sel_to = sel_to-1, sel_to-1
3575##                        dbg("new sel_start, sel_to:", sel_start, sel_to)
3576
3577                    if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''):
3578                        # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0:
3579                        newchar = '0'
3580                    else:
3581                        newchar = self._template[sel_to] ## get an original template character to "clear" the current char
3582##                    dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start]))
3583
3584                    if self._isTemplateChar(sel_to):
3585                        if sel_to == 0 and self._signOk and value[sel_to] == '-':   # erasing "template" sign char
3586                            newstr = ' ' + value[1:]
3587                            sel_to += 1
3588                        elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('):
3589                            # allow "change sign" by removing both parens:
3590                            newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' '
3591                        else:
3592                            newstr = value
3593                        newpos = sel_to
3594                    else:
3595                        if field._insertRight and sel_start == sel_to:
3596                            # force non-insert-right behavior, by selecting char to be replaced:
3597                            sel_to += 1
3598                        newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value)
3599
3600                else:
3601                    # selection made
3602                    newstr = self._eraseSelection(value, sel_start, sel_to)
3603
3604                pos = sel_start  # put cursor back at beginning of selection
3605
3606        if self._signOk and self._useParens:
3607            # account for resultant unbalanced parentheses:
3608            left_signpos = newstr.find('(')
3609            right_signpos = newstr.find(')')
3610
3611            if left_signpos == -1 and right_signpos != -1:
3612                # erased left-sign marker; get rid of right sign marker:
3613                newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:]
3614
3615            elif left_signpos != -1 and right_signpos == -1:
3616                # erased right-sign marker; get rid of left-sign marker:
3617                newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:]
3618
3619##        dbg("oldstr:'%s'" % value, 'oldpos:', oldstart)
3620##        dbg("newstr:'%s'" % newstr, 'pos:', pos)
3621
3622        # if erasure results in an invalid field, disallow it:
3623##        dbg('field._validRequired?', field._validRequired)
3624##        dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end]))
3625        if field._validRequired and not field.IsValid(newstr[start:end]):
3626            if not wx.Validator.IsSilent():
3627                wx.Bell()
3628##            dbg(indent=0)
3629            return False
3630
3631        # If the caller just wants the erased value without validation
3632        # (because a just pressed key might make this string valid again)
3633        # then return it.
3634        if just_return_value:
3635##            dbg(indent=0)
3636            return newstr
3637
3638        # if erasure results in an invalid value, disallow it:
3639        if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
3640            if not wx.Validator.IsSilent():
3641                wx.Bell()
3642##            dbg(indent=0)
3643            return False
3644
3645        # else...
3646##        dbg('setting value (later) to', newstr)
3647        wx.CallAfter(self._SetValue, newstr)
3648##        dbg('setting insertion point (later) to', pos)
3649        wx.CallAfter(self._SetInsertionPoint, pos)
3650##        dbg(indent=0)
3651        if newstr != value:
3652            self.modified = True
3653        return False
3654
3655
3656    def _OnEnd(self,event):
3657        """ Handles End keypress in control. Should return False to skip other processing. """
3658##        dbg("MaskedEditMixin::_OnEnd", indent=1)
3659        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
3660        if not event.ControlDown():
3661            end = self._masklength  # go to end of control
3662            if self._signOk and self._useParens:
3663                end = end - 1       # account for reserved char at end
3664        else:
3665            end_of_input = self._goEnd(getPosOnly=True)
3666            sel_start, sel_to = self._GetSelection()
3667            if sel_to < pos: sel_to = pos
3668            field = self._FindField(sel_to)
3669            field_end = self._FindField(end_of_input)
3670
3671            # pick different end point if either:
3672            # - cursor not in same field
3673            # - or at or past last input already
3674            # - or current selection = end of current field:
3675####            dbg('field != field_end?', field != field_end)
3676####            dbg('sel_to >= end_of_input?', sel_to >= end_of_input)
3677            if field != field_end or sel_to >= end_of_input:
3678                edit_start, edit_end = field._extent
3679####                dbg('edit_end:', edit_end)
3680####                dbg('sel_to:', sel_to)
3681####                dbg('sel_to == edit_end?', sel_to == edit_end)
3682####                dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1])
3683
3684                if sel_to == edit_end and field._index < self._field_indices[-1]:
3685                    edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end))  # go to end of next field:
3686                    end = edit_end
3687##                    dbg('end moved to', end)
3688
3689                elif sel_to == edit_end and field._index == self._field_indices[-1]:
3690                    # already at edit end of last field; select to end of control:
3691                    end = self._masklength
3692##                    dbg('end moved to', end)
3693                else:
3694                    end = edit_end  # select to end of current field
3695##                    dbg('end moved to ', end)
3696            else:
3697                # select to current end of input
3698                end = end_of_input
3699
3700
3701####        dbg('pos:', pos, 'end:', end)
3702
3703        if event.ShiftDown():
3704            if not event.ControlDown():
3705##                dbg("shift-end; select to end of control")
3706                pass
3707            else:
3708##                dbg("shift-ctrl-end; select to end of non-whitespace")
3709                pass
3710            wx.CallAfter(self._SetInsertionPoint, pos)
3711            wx.CallAfter(self._SetSelection, pos, end)
3712        else:
3713            if not event.ControlDown():
3714##                dbg('go to end of control:')
3715                pass
3716            wx.CallAfter(self._SetInsertionPoint, end)
3717            wx.CallAfter(self._SetSelection, end, end)
3718
3719##        dbg(indent=0)
3720        return False
3721
3722
3723    def _OnReturn(self, event):
3724         """
3725         Swallows the return, issues a Navigate event instead, since
3726         masked controls are "single line" by defn.
3727         """
3728##         dbg('MaskedEditMixin::OnReturn')
3729         self.Navigate(True)
3730         return False
3731
3732
3733    def _OnHome(self,event):
3734        """ Handles Home keypress in control. Should return False to skip other processing."""
3735##        dbg("MaskedEditMixin::_OnHome", indent=1)
3736        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
3737        sel_start, sel_to = self._GetSelection()
3738
3739        # There are 5 cases here:
3740
3741        # 1) shift: select from start of control to end of current
3742        #    selection.
3743        if event.ShiftDown() and not event.ControlDown():
3744##            dbg("shift-home; select to start of control")
3745            start = 0
3746            end = sel_start
3747
3748        # 2) no shift, no control: move cursor to beginning of control.
3749        elif not event.ControlDown():
3750##            dbg("home; move to start of control")
3751            start = 0
3752            end = 0
3753
3754        # 3) No shift, control: move cursor back to beginning of field; if
3755        #    there already, go to beginning of previous field.
3756        # 4) shift, control, start of selection not at beginning of control:
3757        #    move sel_start back to start of field; if already there, go to
3758        #    start of previous field.
3759        elif( event.ControlDown()
3760              and (not event.ShiftDown()
3761                   or (event.ShiftDown() and sel_start > 0) ) ):
3762            if len(self._field_indices) > 1:
3763                field = self._FindField(sel_start)
3764                start, ignore = field._extent
3765                if sel_start == start and field._index != self._field_indices[0]:  # go to start of previous field:
3766                    start, ignore = self._FindFieldExtent(sel_start-1)
3767                elif sel_start == start:
3768                    start = 0   # go to literal beginning if edit start
3769                                # not at that point
3770                end_of_field = True
3771
3772            else:
3773                start = 0
3774
3775            if not event.ShiftDown():
3776##                dbg("ctrl-home; move to beginning of field")
3777                end = start
3778            else:
3779##                dbg("shift-ctrl-home; select to beginning of field")
3780                end = sel_to
3781
3782        else:
3783        # 5) shift, control, start of selection at beginning of control:
3784        #    unselect by moving sel_to backward to beginning of current field;
3785        #    if already there, move to start of previous field.
3786            start = sel_start
3787            if len(self._field_indices) > 1:
3788                # find end of previous field:
3789                field = self._FindField(sel_to)
3790                if sel_to > start and field._index != self._field_indices[0]:
3791                    ignore, end = self._FindFieldExtent(field._extent[0]-1)
3792                else:
3793                    end = start
3794                end_of_field = True
3795            else:
3796                end = start
3797                end_of_field = False
3798##            dbg("shift-ctrl-home; unselect to beginning of field")
3799
3800##        dbg('queuing new sel_start, sel_to:', (start, end))
3801        wx.CallAfter(self._SetInsertionPoint, start)
3802        wx.CallAfter(self._SetSelection, start, end)
3803##        dbg(indent=0)
3804        return False
3805
3806
3807    def _OnChangeField(self, event):
3808        """
3809        Primarily handles TAB events, but can be used for any key that
3810        designer wants to change fields within a masked edit control.
3811        """
3812##        dbg('MaskedEditMixin::_OnChangeField', indent = 1)
3813        # determine end of current field:
3814        pos = self._GetInsertionPoint()
3815##        dbg('current pos:', pos)
3816        sel_start, sel_to = self._GetSelection()
3817
3818        # no fields; process tab normally
3819        # sel_to == -1 would cause an index error in _FindField
3820        if self._masklength < 0 or sel_to == -1:
3821            if pos != -1:
3822                self._AdjustField(pos)
3823            if event.GetKeyCode() == wx.WXK_TAB:
3824##                dbg('tab to next ctrl')
3825                # As of 2.5.2, you don't call event.Skip() to do
3826                # this, but instead force explicit navigation, if
3827                # wx.TE_PROCESS_TAB is used (like in the masked edits)
3828                self.Navigate(True)
3829            #else: do nothing
3830##            dbg(indent=0)
3831            return False
3832
3833        field = self._FindField(sel_to)
3834        index = field._index
3835        field_start, field_end = field._extent
3836        slice = self._GetValue()[field_start:field_end]
3837
3838##        dbg('field._stopFieldChangeIfInvalid?', field._stopFieldChangeIfInvalid)
3839##        dbg('field.IsValid(slice)?', field.IsValid(slice))
3840
3841        if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
3842##            dbg('field invalid; field change disallowed')
3843            if not wx.Validator.IsSilent():
3844                wx.Bell()
3845##            dbg(indent=0)
3846            return False
3847
3848
3849        if event.ShiftDown():
3850
3851            # "Go backward"
3852
3853            # NOTE: doesn't yet work with SHIFT-tab under wx; the control
3854            # never sees this event! (But I've coded for it should it ever work,
3855            # and it *does* work for '.' in IpAddrCtrl.)
3856
3857            if pos < field_start:
3858##                dbg('cursor before 1st field; cannot change to a previous field')
3859                if not wx.Validator.IsSilent():
3860                    wx.Bell()
3861##                dbg(indent=0)
3862                return False
3863
3864            if event.ControlDown():
3865##                dbg('queuing select to beginning of field:', field_start, pos)
3866                wx.CallAfter(self._SetInsertionPoint, field_start)
3867                wx.CallAfter(self._SetSelection, field_start, pos)
3868##                dbg(indent=0)
3869                return False
3870
3871            elif index == 0:
3872                  # We're already in the 1st field; process shift-tab normally:
3873                self._AdjustField(pos)
3874                if event.GetKeyCode() == wx.WXK_TAB:
3875##                    dbg('tab to previous ctrl')
3876                    # As of 2.5.2, you don't call event.Skip() to do
3877                    # this, but instead force explicit navigation, if
3878                    # wx.TE_PROCESS_TAB is used (like in the masked edits)
3879                    self.Navigate(False)
3880                else:
3881##                    dbg('position at beginning')
3882                    wx.CallAfter(self._SetInsertionPoint, field_start)
3883##                dbg(indent=0)
3884                return False
3885            else:
3886                # find beginning of previous field:
3887                begin_prev = self._FindField(field_start-1)._extent[0]
3888                self._AdjustField(pos)
3889##                dbg('repositioning to', begin_prev)
3890                wx.CallAfter(self._SetInsertionPoint, begin_prev)
3891                if self._FindField(begin_prev)._selectOnFieldEntry:
3892                    edit_start, edit_end = self._FindFieldExtent(begin_prev)
3893##                    dbg('queuing selection to (%d, %d)' % (edit_start, edit_end))
3894                    wx.CallAfter(self._SetInsertionPoint, edit_start)
3895                    wx.CallAfter(self._SetSelection, edit_start, edit_end)
3896##                dbg(indent=0)
3897                return False
3898
3899        else:
3900            # "Go forward"
3901            if event.ControlDown():
3902##                dbg('queuing select to end of field:', pos, field_end)
3903                wx.CallAfter(self._SetInsertionPoint, pos)
3904                wx.CallAfter(self._SetSelection, pos, field_end)
3905##                dbg(indent=0)
3906                return False
3907            else:
3908                if pos < field_start:
3909##                    dbg('cursor before 1st field; go to start of field')
3910                    wx.CallAfter(self._SetInsertionPoint, field_start)
3911                    if field._selectOnFieldEntry:
3912                        wx.CallAfter(self._SetSelection, field_end, field_start)
3913                    else:
3914                        wx.CallAfter(self._SetSelection, field_start, field_start)
3915                    return False
3916                # else...
3917##                dbg('end of current field:', field_end)
3918##                dbg('go to next field')
3919                if field_end == self._fields[self._field_indices[-1]]._extent[1]:
3920                    self._AdjustField(pos)
3921                    if event.GetKeyCode() == wx.WXK_TAB:
3922##                        dbg('tab to next ctrl')
3923                        # As of 2.5.2, you don't call event.Skip() to do
3924                        # this, but instead force explicit navigation, if
3925                        # wx.TE_PROCESS_TAB is used (like in the masked edits)
3926                        self.Navigate(True)
3927                    else:
3928##                        dbg('position at end')
3929                        wx.CallAfter(self._SetInsertionPoint, field_end)
3930##                    dbg(indent=0)
3931                    return False
3932                else:
3933                    # we have to find the start of the next field
3934                    next_pos = self._findNextEntry(field_end)
3935                    if next_pos == field_end:
3936##                        dbg('already in last field')
3937                        self._AdjustField(pos)
3938                        if event.GetKeyCode() == wx.WXK_TAB:
3939##                            dbg('tab to next ctrl')
3940                            # As of 2.5.2, you don't call event.Skip() to do
3941                            # this, but instead force explicit navigation, if
3942                            # wx.TE_PROCESS_TAB is used (like in the masked edits)
3943                            self.Navigate(True)
3944                        #else: do nothing
3945##                        dbg(indent=0)
3946                        return False
3947                    else:
3948                        self._AdjustField( pos )
3949
3950                        # move cursor to appropriate point in the next field and select as necessary:
3951                        field = self._FindField(next_pos)
3952                        edit_start, edit_end = field._extent
3953                        if field._selectOnFieldEntry:
3954##                            dbg('move to ', next_pos)
3955                            wx.CallAfter(self._SetInsertionPoint, next_pos)
3956                            edit_start, edit_end = self._FindFieldExtent(next_pos)
3957##                            dbg('queuing select', edit_start, edit_end)
3958                            wx.CallAfter(self._SetSelection, edit_end, edit_start)
3959                        else:
3960                            if field._insertRight:
3961                                next_pos = field._extent[1]
3962##                            dbg('move to ', next_pos)
3963                            wx.CallAfter(self._SetInsertionPoint, next_pos)
3964##                        dbg(indent=0)
3965                        return False
3966##                dbg(indent=0)
3967
3968
3969    def _OnDecimalPoint(self, event):
3970##        dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
3971        field = self._FindField(self._GetInsertionPoint())
3972        start, end = field._extent
3973        slice = self._GetValue()[start:end]
3974
3975        if field._stopFieldChangeIfInvalid and not field.IsValid(slice):
3976            if not wx.Validator.IsSilent():
3977                wx.Bell()
3978            return False
3979
3980        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
3981
3982        if self._isFloat:       ## handle float value, move to decimal place
3983##            dbg('key == Decimal tab; decimal pos:', self._decimalpos)
3984            value = self._GetValue()
3985            if pos < self._decimalpos:
3986                clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:]
3987##                dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
3988                newstr = self._adjustFloat(clipped_text)
3989            else:
3990                newstr = self._adjustFloat(value)
3991            wx.CallAfter(self._SetValue, newstr)
3992            fraction = self._fields[1]
3993            start, end = fraction._extent
3994            wx.CallAfter(self._SetInsertionPoint, start)
3995            if fraction._selectOnFieldEntry:
3996##                dbg('queuing selection after decimal point to:', (start, end))
3997                wx.CallAfter(self._SetSelection, end, start)
3998            else:
3999                wx.CallAfter(self._SetSelection, start, start)
4000            keep_processing = False
4001
4002        if self._isInt:      ## handle integer value, truncate from current position
4003##            dbg('key == Integer decimal event')
4004            value = self._GetValue()
4005            clipped_text = value[0:pos]
4006##            dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
4007            newstr = self._adjustInt(clipped_text)
4008##            dbg('newstr: "%s"' % newstr)
4009            wx.CallAfter(self._SetValue, newstr)
4010            newpos = len(newstr.rstrip())
4011            if newstr.find(')') != -1:
4012                newpos -= 1     # (don't move past right paren)
4013            wx.CallAfter(self._SetInsertionPoint, newpos)
4014            wx.CallAfter(self._SetSelection, newpos, newpos)
4015            keep_processing = False
4016##        dbg(indent=0)
4017
4018
4019    def _OnChangeSign(self, event):
4020##        dbg('MaskedEditMixin::_OnChangeSign', indent=1)
4021        key = event.GetKeyCode()
4022        pos = self._adjustPos(self._GetInsertionPoint(), key)
4023        value = self._eraseSelection()
4024        integer = self._fields[0]
4025        start, end = integer._extent
4026        sel_start, sel_to = self._GetSelection()
4027
4028####        dbg('adjusted pos:', pos)
4029        if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos):
4030            cursign = self._isNeg
4031##            dbg('cursign:', cursign)
4032            if chr(key) in ('-','(', ')'):
4033                if sel_start <= self._signpos:
4034                    self._isNeg = True
4035                else:
4036                    self._isNeg = (not self._isNeg)   ## flip value
4037            else:
4038                self._isNeg = False
4039##            dbg('isNeg?', self._isNeg)
4040
4041            text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value)
4042##            dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos)
4043            if text is None:
4044                text = value
4045
4046            if self._isNeg and self._signpos is not None and self._signpos != -1:
4047                if self._useParens and self._right_signpos is not None:
4048                    text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:]
4049                else:
4050                    text = text[:self._signpos] + '-' + text[self._signpos+1:]
4051            else:
4052####                dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text))
4053                if self._useParens:
4054                    text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:]
4055                else:
4056                    text = text[:self._signpos] + ' ' + text[self._signpos+1:]
4057##                dbg('clearing self._isNeg')
4058                self._isNeg = False
4059
4060            wx.CallAfter(self._SetValue, text)
4061            wx.CallAfter(self._applyFormatting)
4062##            dbg('pos:', pos, 'signpos:', self._signpos)
4063            if pos == self._signpos or integer.IsEmpty(text[start:end]):
4064                wx.CallAfter(self._SetInsertionPoint, self._signpos+1)
4065            else:
4066                wx.CallAfter(self._SetInsertionPoint, pos)
4067
4068            keep_processing = False
4069        else:
4070            keep_processing = True
4071##        dbg(indent=0)
4072        return keep_processing
4073
4074
4075    def _OnGroupChar(self, event):
4076        """
4077        This handler is only registered if the mask is a numeric mask.
4078        It allows the insertion of ',' or '.' if appropriate.
4079        """
4080##        dbg('MaskedEditMixin::_OnGroupChar', indent=1)
4081        keep_processing = True
4082        pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
4083        sel_start, sel_to = self._GetSelection()
4084        groupchar = self._fields[0]._groupChar
4085        if not self._isCharAllowed(groupchar, pos, checkRegex=True):
4086            keep_processing = False
4087            if not wx.Validator.IsSilent():
4088                wx.Bell()
4089
4090        if keep_processing:
4091            newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() )
4092##            dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr)
4093            if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
4094                keep_processing = False
4095                if not wx.Validator.IsSilent():
4096                        wx.Bell()
4097
4098        if keep_processing:
4099            wx.CallAfter(self._SetValue, newstr)
4100            wx.CallAfter(self._SetInsertionPoint, newpos)
4101        keep_processing = False
4102##        dbg(indent=0)
4103        return keep_processing
4104
4105
4106    def _findNextEntry(self,pos, adjustInsert=True):
4107        """ Find the insertion point for the next valid entry character position."""
4108##        dbg('MaskedEditMixin::_findNextEntry', indent=1)
4109        if self._isTemplateChar(pos) or pos in self._explicit_field_boundaries:   # if changing fields, pay attn to flag
4110            adjustInsert = adjustInsert
4111        else:                           # else within a field; flag not relevant
4112            adjustInsert = False
4113
4114        while self._isTemplateChar(pos) and pos < self._masklength:
4115            pos += 1
4116
4117        # if changing fields, and we've been told to adjust insert point,
4118        # look at new field; if empty and right-insert field,
4119        # adjust to right edge:
4120        if adjustInsert and pos < self._masklength:
4121            field = self._FindField(pos)
4122            start, end = field._extent
4123            slice = self._GetValue()[start:end]
4124            if field._insertRight and field.IsEmpty(slice):
4125                pos = end
4126##        dbg('final pos:', pos, indent=0)
4127        return pos
4128
4129
4130    def _findNextTemplateChar(self, pos):
4131        """ Find the position of the next non-editable character in the mask."""
4132        while not self._isTemplateChar(pos) and pos < self._masklength:
4133            pos += 1
4134        return pos
4135
4136
4137    def _OnAutoCompleteField(self, event):
4138##        dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1)
4139        pos = self._GetInsertionPoint()
4140        field = self._FindField(pos)
4141        edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
4142
4143        match_index = None
4144        keycode = event.GetKeyCode()
4145
4146        if field._fillChar != ' ':
4147            text = slice.replace(field._fillChar, '')
4148        else:
4149            text = slice
4150        text = text.strip()
4151        keep_processing = True  # (assume True to start)
4152##        dbg('field._hasList?', field._hasList)
4153        if field._hasList:
4154##            dbg('choices:', field._choices)
4155##            dbg('compareChoices:', field._compareChoices)
4156            choices, choice_required = field._compareChoices, field._choiceRequired
4157            if keycode in (wx.WXK_PAGEUP, wx.WXK_UP, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_UP):
4158                direction = -1
4159            else:
4160                direction = 1
4161            match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex)
4162            if( match_index is None
4163                and (keycode in self._autoCompleteKeycodes + [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_PAGEDOWN]
4164                     or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown() ) ) ):
4165                # Select the 1st thing from the list:
4166                match_index = 0
4167
4168            if( match_index is not None
4169                and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, wx.WXK_NUMPAD_PAGEUP, wx.WXK_NUMPAD_PAGEDOWN]
4170                      or (keycode in [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN] and event.ShiftDown())
4171                      or (keycode in [wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN] and partial_match) ) ):
4172
4173                # We're allowed to auto-complete:
4174##                dbg('match found')
4175                value = self._GetValue()
4176                newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:]
4177##                dbg('setting value to "%s"' % newvalue)
4178                self._SetValue(newvalue)
4179                self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip())))
4180                self._OnAutoSelect(field, match_index)
4181                self._CheckValid()  # recolor as appopriate
4182
4183
4184        if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT,
4185                       wx.WXK_NUMPAD_UP, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_LEFT, wx.WXK_NUMPAD_RIGHT):
4186            # treat as left right arrow if unshifted, tab/shift tab if shifted.
4187            if event.ShiftDown():
4188                if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT, wx.WXK_NUMPAD_DOWN, wx.WXK_NUMPAD_RIGHT):
4189                    # remove "shifting" and treat as (forward) tab:
4190                    event.shiftDown = False
4191                keep_processing = self._OnChangeField(event)
4192            else:
4193                keep_processing = self._OnArrow(event)
4194        # else some other key; keep processing the key
4195
4196##        dbg('keep processing?', keep_processing, indent=0)
4197        return keep_processing
4198
4199
4200    def _OnAutoSelect(self, field, match_index = None):
4201        """
4202        Function called if autoselect feature is enabled and entire control
4203        is selected:
4204        """
4205##        dbg('MaskedEditMixin::OnAutoSelect', field._index)
4206        if match_index is not None:
4207            field._autoCompleteIndex = match_index
4208
4209
4210    def _autoComplete(self, direction, choices, value, compareNoCase, current_index):
4211        """
4212        This function gets called in response to Auto-complete events.
4213        It attempts to find a match to the specified value against the
4214        list of choices; if exact match, the index of then next
4215        appropriate value in the list, based on the given direction.
4216        If not an exact match, it will return the index of the 1st value from
4217        the choice list for which the partial value can be extended to match.
4218        If no match found, it will return None.
4219        The function returns a 2-tuple, with the 2nd element being a boolean
4220        that indicates if partial match was necessary.
4221        """
4222##        dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1)
4223        if value is None:
4224##            dbg('nothing to match against', indent=0)
4225            return (None, False)
4226
4227        partial_match = False
4228
4229        if compareNoCase:
4230            value = value.lower()
4231
4232        last_index = len(choices) - 1
4233        if value in choices:
4234##            dbg('"%s" in', choices)
4235            if current_index is not None and choices[current_index] == value:
4236                index = current_index
4237            else:
4238                index = choices.index(value)
4239
4240##            dbg('matched "%s" (%d)' % (choices[index], index))
4241            if direction == -1:
4242##                dbg('going to previous')
4243                if index == 0: index = len(choices) - 1
4244                else: index -= 1
4245            else:
4246                if index == len(choices) - 1: index = 0
4247                else: index += 1
4248##            dbg('change value to "%s" (%d)' % (choices[index], index))
4249            match = index
4250        else:
4251            partial_match = True
4252            value = value.strip()
4253##            dbg('no match; try to auto-complete:')
4254            match = None
4255##            dbg('searching for "%s"' % value)
4256            if current_index is None:
4257                indices = range(len(choices))
4258                if direction == -1:
4259                    indices.reverse()
4260            else:
4261                if direction == 1:
4262                    indices = range(current_index +1, len(choices)) + range(current_index+1)
4263##                    dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices)
4264                else:
4265                    indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1)
4266##                    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)
4267####            dbg('indices:', indices)
4268            for index in indices:
4269                choice = choices[index]
4270                if choice.find(value, 0) == 0:
4271##                    dbg('match found:', choice)
4272                    match = index
4273                    break
4274                else:
4275##                    dbg('choice: "%s" - no match' % choice)
4276                    pass
4277            if match is not None:
4278##                dbg('matched', match)
4279                pass
4280            else:
4281##                dbg('no match found')
4282                pass
4283##        dbg(indent=0)
4284        return (match, partial_match)
4285
4286
4287    def _AdjustField(self, pos):
4288        """
4289        This function gets called by default whenever the cursor leaves a field.
4290        The pos argument given is the char position before leaving that field.
4291        By default, floating point, integer and date values are adjusted to be
4292        legal in this function.  Derived classes may override this function
4293        to modify the value of the control in a different way when changing fields.
4294
4295        NOTE: these change the value immediately, and restore the cursor to
4296        the passed location, so that any subsequent code can then move it
4297        based on the operation being performed.
4298        """
4299        newvalue = value = self._GetValue()
4300        field = self._FindField(pos)
4301        start, end, slice = self._FindFieldExtent(getslice=True)
4302        newfield = field._AdjustField(slice)
4303        newvalue = value[:start] + newfield + value[end:]
4304
4305        if self._isFloat and newvalue != self._template:
4306            newvalue = self._adjustFloat(newvalue)
4307
4308        if self._ctrl_constraints._isInt and value != self._template:
4309            newvalue = self._adjustInt(value)
4310
4311        if self._isDate and value != self._template:
4312            newvalue = self._adjustDate(value, fixcentury=True)
4313            if self._4digityear:
4314                year2dig = self._dateExtent - 2
4315                if pos == year2dig and value[year2dig] != newvalue[year2dig]:
4316                    pos = pos+2
4317
4318        if newvalue != value:
4319##            dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue))
4320            self._SetValue(newvalue)
4321            self._SetInsertionPoint(pos)
4322
4323
4324    def _adjustKey(self, pos, key):
4325        """ Apply control formatting to the key (e.g. convert to upper etc). """
4326        field = self._FindField(pos)
4327        if field._forceupper and key in range(97,123):
4328            key = ord( chr(key).upper())
4329
4330        if field._forcelower and key in range(65,90):
4331            key = ord( chr(key).lower())
4332
4333        return key
4334
4335
4336    def _adjustPos(self, pos, key):
4337        """
4338        Checks the current insertion point position and adjusts it if
4339        necessary to skip over non-editable characters.
4340        """
4341##        dbg('_adjustPos', pos, key, indent=1)
4342        sel_start, sel_to = self._GetSelection()
4343        # If a numeric or decimal mask, and negatives allowed, reserve the
4344        # first space for sign, and last one if using parens.
4345        if( self._signOk
4346            and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) )
4347                 or (self._useParens and pos == self._masklength -1))):
4348##            dbg('adjusted pos:', pos, indent=0)
4349            return pos
4350
4351        if key not in self._nav:
4352            field = self._FindField(pos)
4353
4354##            dbg('field._insertRight?', field._insertRight)
4355##            if self._signOk: dbg('self._signpos:', self._signpos)
4356            if field._insertRight:              # if allow right-insert
4357                start, end = field._extent
4358                slice = self._GetValue()[start:end].strip()
4359                field_len = end - start
4360                if pos == end:                      # if cursor at right edge of field
4361                    # if not filled or supposed to stay in field, keep current position
4362####                    dbg('pos==end')
4363####                    dbg('len (slice):', len(slice))
4364####                    dbg('field_len?', field_len)
4365####                    dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
4366####                    dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
4367                    if( len(slice) == field_len and field._moveOnFieldFull
4368                        and (not field._stopFieldChangeIfInvalid or
4369                             field._stopFieldChangeIfInvalid and field.IsValid(slice))):
4370                        # move cursor to next field:
4371                        pos = self._findNextEntry(pos)
4372                        self._SetInsertionPoint(pos)
4373                        if pos < sel_to:
4374                            self._SetSelection(pos, sel_to)     # restore selection
4375                        else:
4376                            self._SetSelection(pos, pos)        # remove selection
4377                    else: # leave cursor alone
4378                        pass
4379                else:
4380                    # if at start of control, move to right edge
4381                    if (sel_to == sel_start
4382                        and (self._isTemplateChar(pos) or (pos == start and len(slice)+ 1 < field_len))
4383                        and pos != end):
4384                        pos = end                   # move to right edge
4385##                    elif sel_start <= start and sel_to == end:
4386##                        # select to right edge of field - 1 (to replace char)
4387##                        pos = end - 1
4388##                        self._SetInsertionPoint(pos)
4389##                        # restore selection
4390##                        self._SetSelection(sel_start, pos)
4391
4392                    # if selected to beginning and signed, and not changing sign explicitly:
4393                    elif self._signOk and sel_start == 0 and key not in (ord('-'), ord('+'), ord(' ')):
4394                        # adjust to past reserved sign position:
4395                        pos = self._fields[0]._extent[0]
4396##                        dbg('adjusting field to ', pos)
4397                        self._SetInsertionPoint(pos)
4398                        # but keep original selection, to allow replacement of any sign:
4399                        self._SetSelection(0, sel_to)
4400                    else:
4401                        pass    # leave position/selection alone
4402
4403            # else make sure the user is not trying to type over a template character
4404            # If they are, move them to the next valid entry position
4405            elif self._isTemplateChar(pos):
4406                if( (not field._moveOnFieldFull
4407                     and (not self._signOk
4408                          or (self._signOk and field._index == 0 and pos > 0) ) )
4409
4410                    or (field._stopFieldChangeIfInvalid
4411                        and not field.IsValid(self._GetValue()[start:end]) ) ):
4412
4413                    # don't move to next field without explicit cursor movement
4414                    pass
4415                else:
4416                    # find next valid position
4417                    pos = self._findNextEntry(pos)
4418                    self._SetInsertionPoint(pos)
4419                    if pos < sel_to:    # restore selection
4420                        self._SetSelection(pos, sel_to)
4421                    else:
4422                        self._SetSelection(pos, pos)
4423##        dbg('adjusted pos:', pos, indent=0)
4424        return pos
4425
4426
4427    def _adjustFloat(self, candidate=None):
4428        """
4429        'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
4430        """
4431##        dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1)
4432        lenInt,lenFraction  = [len(s) for s in self._mask.split('.')]  ## Get integer, fraction lengths
4433
4434        if candidate is None: value = self._GetValue()
4435        else: value = candidate
4436##        dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
4437        intStr, fracStr = value.split(self._decimalChar)
4438
4439        intStr = self._fields[0]._AdjustField(intStr)
4440##        dbg('adjusted intStr: "%s"' % intStr)
4441        lenInt = len(intStr)
4442        fracStr = fracStr + ('0'*(lenFraction-len(fracStr)))  # add trailing spaces to decimal
4443
4444##        dbg('intStr "%(intStr)s"' % locals())
4445##        dbg('lenInt:', lenInt)
4446
4447        intStr = intStr[-lenInt:].rjust(lenInt)
4448##        dbg('right-justifed intStr = "%(intStr)s"' % locals())
4449        newvalue = intStr + self._decimalChar + fracStr
4450
4451        if self._signOk:
4452            if len(newvalue) < self._masklength:
4453                newvalue = ' ' + newvalue
4454            signedvalue = self._getSignedValue(newvalue)[0]
4455            if signedvalue is not None: newvalue = signedvalue
4456
4457        # Finally, align string with decimal position, left-padding with
4458        # fillChar:
4459        newdecpos = newvalue.find(self._decimalChar)
4460        if newdecpos < self._decimalpos:
4461            padlen = self._decimalpos - newdecpos
4462            newvalue = ''.join([' ' * padlen] + [newvalue])
4463
4464        if self._signOk and self._useParens:
4465            if newvalue.find('(') != -1:
4466                newvalue = newvalue[:-1] + ')'
4467            else:
4468                newvalue = newvalue[:-1] + ' '
4469
4470##        dbg('newvalue = "%s"' % newvalue)
4471        if candidate is None:
4472            wx.CallAfter(self._SetValue, newvalue)
4473##        dbg(indent=0)
4474        return newvalue
4475
4476
4477    def _adjustInt(self, candidate=None):
4478        """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
4479##        dbg("MaskedEditMixin::_adjustInt", candidate)
4480        lenInt = self._masklength
4481        if candidate is None: value = self._GetValue()
4482        else: value = candidate
4483
4484        intStr = self._fields[0]._AdjustField(value)
4485        intStr = intStr.strip() # drop extra spaces
4486##        dbg('adjusted field: "%s"' % intStr)
4487
4488        if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1:
4489            if self._useParens:
4490                intStr = '(' + intStr + ')'
4491            else:
4492                intStr = '-' + intStr
4493        elif self._isNeg and intStr.find('-') != -1 and self._useParens:
4494            intStr = intStr.replace('-', '(')
4495
4496        if( self._signOk and ((self._useParens and intStr.find('(') == -1)
4497                                or (not self._useParens and intStr.find('-') == -1))):
4498            intStr = ' ' + intStr
4499            if self._useParens:
4500                intStr += ' '   # space for right paren position
4501
4502        elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1:
4503            # ensure closing right paren:
4504            intStr += ')'
4505
4506        if self._fields[0]._alignRight:     ## Only if right-alignment is enabled
4507            intStr = intStr.rjust( lenInt )
4508        else:
4509            intStr = intStr.ljust( lenInt )
4510
4511        if candidate is None:
4512            wx.CallAfter(self._SetValue, intStr )
4513        return intStr
4514
4515
4516    def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False):
4517        """
4518        'Fixes' a date control, expanding the year if it can.
4519        Applies various self-formatting options.
4520        """
4521##        dbg("MaskedEditMixin::_adjustDate", indent=1)
4522        if candidate is None: text    = self._GetValue()
4523        else: text = candidate
4524##        dbg('text=', text)
4525        if self._datestyle == "YMD":
4526            year_field = 0
4527        else:
4528            year_field = 2
4529
4530##        dbg('getYear: "%s"' % _getYear(text, self._datestyle))
4531        year    = _getYear( text, self._datestyle).replace(self._fields[year_field]._fillChar,"")  # drop extra fillChars
4532        month   = _getMonth( text, self._datestyle)
4533        day     = _getDay( text, self._datestyle)
4534##        dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day)
4535
4536        yearVal = None
4537        yearstart = self._dateExtent - 4
4538        if( len(year) < 4
4539            and (fixcentury
4540                 or force4digit_year
4541                 or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ')
4542                 or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ):
4543            ## user entered less than four digits and changing fields or past point where we could
4544            ## enter another digit:
4545            try:
4546                yearVal = int(year)
4547            except:
4548##                dbg('bad year=', year)
4549                year = text[yearstart:self._dateExtent]
4550
4551        if len(year) < 4 and yearVal:
4552            if len(year) == 2:
4553                # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the
4554                # years pass...
4555                now = wx.DateTime.Now()
4556                century = (now.GetYear() /100) * 100        # "this century"
4557                twodig_year = now.GetYear() - century       # "this year" (2 digits)
4558                # if separation between today's 2-digit year and typed value > 50,
4559                #      assume last century,
4560                # else assume this century.
4561                #
4562                # Eg: if 2003 and yearVal == 30, => 2030
4563                #     if 2055 and yearVal == 80, => 2080
4564                #     if 2010 and yearVal == 96, => 1996
4565                #
4566                if abs(yearVal - twodig_year) > 50:
4567                    yearVal = (century - 100) + yearVal
4568                else:
4569                    yearVal = century + yearVal
4570                year = str( yearVal )
4571            else:   # pad with 0's to make a 4-digit year
4572                year = "%04d" % yearVal
4573            if self._4digityear or force4digit_year:
4574                text = _makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:]
4575##        dbg('newdate: "%s"' % text, indent=0)
4576        return text
4577
4578
4579    def _goEnd(self, getPosOnly=False):
4580        """ Moves the insertion point to the end of user-entry """
4581##        dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
4582        text = self._GetValue()
4583####        dbg('text: "%s"' % text)
4584        i = 0
4585        if len(text.rstrip()):
4586            for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1):
4587####                dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i))
4588                if self._isMaskChar(i):
4589                    char = text[i]
4590####                    dbg("text[%d]: '%s'" % (i, char))
4591                    if char != ' ':
4592                        i += 1
4593                        break
4594
4595        if i == 0:
4596            pos = self._goHome(getPosOnly=True)
4597        else:
4598            pos = min(i,self._masklength)
4599
4600        field = self._FindField(pos)
4601        start, end = field._extent
4602        if field._insertRight and pos < end:
4603            pos = end
4604##        dbg('next pos:', pos)
4605##        dbg(indent=0)
4606        if getPosOnly:
4607            return pos
4608        else:
4609            self._SetInsertionPoint(pos)
4610
4611
4612    def _goHome(self, getPosOnly=False):
4613        """ Moves the insertion point to the beginning of user-entry """
4614##        dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1)
4615        text = self._GetValue()
4616        for i in range(self._masklength):
4617            if self._isMaskChar(i):
4618                break
4619        pos = max(i, 0)
4620##        dbg(indent=0)
4621        if getPosOnly:
4622            return pos
4623        else:
4624            self._SetInsertionPoint(max(i,0))
4625
4626
4627
4628    def _getAllowedChars(self, pos):
4629        """ Returns a string of all allowed user input characters for the provided
4630            mask character plus control options
4631        """
4632        maskChar = self.maskdict[pos]
4633        okchars = self.maskchardict[maskChar]    ## entry, get mask approved characters
4634
4635        # convert okchars to unicode if required; will force subsequent appendings to
4636        # result in unicode strings
4637        if not six.PY3 and not isinstance(okchars, six.text_type):
4638            okchars = okchars.decode(self._defaultEncoding)
4639
4640        field = self._FindField(pos)
4641        if okchars and field._okSpaces:          ## Allow spaces?
4642            okchars += " "
4643        if okchars and field._includeChars:      ## any additional included characters?
4644            okchars += field._includeChars
4645####        dbg('okchars[%d]:' % pos, okchars)
4646        return okchars
4647
4648
4649    def _isMaskChar(self, pos):
4650        """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
4651        """
4652        if pos < self._masklength:
4653            return self._ismasked[pos]
4654        else:
4655            return False
4656
4657
4658    def _isTemplateChar(self,Pos):
4659        """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#)
4660        """
4661        if Pos < self._masklength:
4662            return not self._isMaskChar(Pos)
4663        else:
4664            return False
4665
4666
4667    def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False):
4668        """ Returns True if character is allowed at the specific position, otherwise False."""
4669##        dbg('_isCharAllowed', char, pos, checkRegex, indent=1)
4670        field = self._FindField(pos)
4671        right_insert = False
4672
4673        if self.controlInitialized:
4674            sel_start, sel_to = self._GetSelection()
4675        else:
4676            sel_start, sel_to = pos, pos
4677
4678        if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight:
4679            start, end = field._extent
4680            field_len = end - start
4681            if self.controlInitialized:
4682                value = self._GetValue()
4683                fstr = value[start:end].strip()
4684                if field._padZero:
4685                    while fstr and fstr[0] == '0':
4686                        fstr = fstr[1:]
4687                input_len = len(fstr)
4688                if self._signOk and '-' in fstr or '(' in fstr:
4689                    input_len -= 1  # sign can move out of field, so don't consider it in length
4690            else:
4691                value = self._template
4692                input_len = 0   # can't get the current "value", so use 0
4693
4694
4695            # if entire field is selected or position is at end and field is not full,
4696            # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar
4697            # or the field is a singleton integer field and is currently 0 and we're at the end:
4698            if( (sel_start, sel_to) == field._extent
4699                or (pos == end and ((input_len < field_len)
4700                                     or (field_len == 1
4701                                         and input_len == field_len
4702                                         and field._isInt
4703                                         and value[end-1] == '0'
4704                                         )
4705                                    ) ) ):
4706                pos = end - 1
4707##                dbg('pos = end - 1 = ', pos, 'right_insert? 1')
4708                right_insert = True
4709            elif( field._allowInsert and sel_start == sel_to
4710                  and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar))
4711                  and input_len < field_len ):
4712                pos = sel_to - 1    # where character will go
4713##                dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1')
4714                right_insert = True
4715            # else leave pos alone...
4716            else:
4717##                dbg('pos stays ', pos, 'right_insert? 0')
4718                pass
4719
4720        if self._isTemplateChar( pos ):  ## if a template character, return empty
4721##            dbg('%d is a template character; returning False' % pos, indent=0)
4722            return False
4723
4724        if self._isMaskChar( pos ):
4725            okChars  = self._getAllowedChars(pos)
4726
4727            if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)):
4728                okChars += self._fields[0]._groupChar
4729
4730            if self._signOk:
4731                if self._isInt or (self._isFloat and pos < self._decimalpos):
4732                    okChars += '-'
4733                    if self._useParens:
4734                        okChars += '('
4735                elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)):
4736                    okChars += ')'
4737
4738####            dbg('%s in %s?' % (char, okChars), char in okChars)
4739            approved = (self.maskdict[pos] == '*' or char in okChars)
4740
4741            if approved and checkRegex:
4742##                dbg("checking appropriate regex's")
4743                value = self._eraseSelection(self._GetValue())
4744                if right_insert:
4745                    # move the position to the right side of the insertion:
4746                    at = pos+1
4747                else:
4748                    at = pos
4749                if allowAutoSelect:
4750                    newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True)
4751                else:
4752                    newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value)
4753##                dbg('newvalue: "%s"' % newvalue)
4754
4755                fields = [self._FindField(pos)] + [self._ctrl_constraints]
4756                for field in fields:    # includes fields[-1] == "ctrl_constraints"
4757                    if field._regexMask and field._filter:
4758##                        dbg('checking vs. regex')
4759                        start, end = field._extent
4760                        slice = newvalue[start:end]
4761                        approved = (re.match( field._filter, slice) is not None)
4762##                        dbg('approved?', approved)
4763                    if not approved: break
4764##            dbg(indent=0)
4765            return approved
4766        else:
4767##            dbg('%d is a !???! character; returning False', indent=0)
4768##            dbg(indent=0)
4769            return False
4770
4771
4772    def _applyFormatting(self):
4773        """ Apply formatting depending on the control's state.
4774            Need to find a way to call this whenever the value changes, in case the control's
4775            value has been changed or set programatically.
4776        """
4777##        dbg(suspend=1)
4778##        dbg('MaskedEditMixin::_applyFormatting', indent=1)
4779
4780        # Handle negative numbers
4781        if self._signOk:
4782            text, signpos, right_signpos = self._getSignedValue()
4783##            dbg('text: "%s", signpos:' % text, signpos)
4784            if text and signpos != self._signpos:
4785                self._signpos = signpos
4786            if not text or text[signpos] not in ('-','('):
4787                self._isNeg = False
4788##                dbg('no valid sign found; new sign:', self._isNeg)
4789            elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('):
4790##                dbg('setting _isNeg to True')
4791                self._isNeg = True
4792##            dbg('self._isNeg:', self._isNeg)
4793
4794        if self._signOk and self._isNeg:
4795            fc = self._signedForegroundColour
4796        else:
4797            fc = self._foregroundColour
4798
4799        if hasattr(fc, '_name'):
4800            c =fc._name
4801        else:
4802            c = fc
4803##        dbg('setting foreground to', c)
4804        self.SetForegroundColour(fc)
4805
4806        if self._valid:
4807##            dbg('valid')
4808            if self.IsEmpty():
4809                bc = self._emptyBackgroundColour
4810            else:
4811                bc = self._validBackgroundColour
4812        else:
4813##            dbg('invalid')
4814            bc = self._invalidBackgroundColour
4815        if hasattr(bc, '_name'):
4816            c =bc._name
4817        else:
4818            c = bc
4819##        dbg('setting background to', c)
4820        self.SetBackgroundColour(bc)
4821        self._Refresh()
4822##        dbg(indent=0, suspend=0)
4823
4824
4825    def _getAbsValue(self, candidate=None):
4826        """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s).
4827        """
4828##        dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1)
4829        if candidate is None: text = self._GetValue()
4830        else: text = candidate
4831        right_signpos = text.find(')')
4832
4833        if self._isInt:
4834            if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ':
4835                signpos = text.find('-')
4836                if signpos == -1:
4837##                    dbg('no - found; searching for (')
4838                    signpos = text.find('(')
4839                elif signpos != -1:
4840##                    dbg('- found at', signpos)
4841                    pass
4842
4843                if signpos == -1:
4844##                    dbg('signpos still -1')
4845##                    dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength)
4846                    if len(text) < self._masklength:
4847                        text = ' ' + text
4848                    if len(text) < self._masklength:
4849                        text += ' '
4850                    if len(text) > self._masklength and text[-1] in (')', ' '):
4851                        text = text[:-1]
4852                    else:
4853##                        dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength))
4854##                        dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1))
4855                        signpos = len(text) - (len(text.lstrip()) + 1)
4856
4857                        if self._useParens and not text.strip():
4858                            signpos -= 1    # empty value; use penultimate space
4859##                dbg('signpos:', signpos)
4860                if signpos >= 0:
4861                    text = text[:signpos] + ' ' + text[signpos+1:]
4862
4863            else:
4864                if self._signOk:
4865                    signpos = 0
4866                    text = self._template[0] + text[1:]
4867                else:
4868                    signpos = -1
4869
4870            if right_signpos != -1:
4871                if self._signOk:
4872                    text = text[:right_signpos] + ' ' + text[right_signpos+1:]
4873                elif len(text) > self._masklength:
4874                    text = text[:right_signpos] + text[right_signpos+1:]
4875                    right_signpos = -1
4876
4877
4878            elif self._useParens and self._signOk:
4879                # figure out where it ought to go:
4880                right_signpos = self._masklength - 1     # initial guess
4881                if not self._ctrl_constraints._alignRight:
4882##                    dbg('not right-aligned')
4883                    if len(text.strip()) == 0:
4884                        right_signpos = signpos + 1
4885                    elif len(text.strip()) < self._masklength:
4886                        right_signpos = len(text.rstrip())
4887##                dbg('right_signpos:', right_signpos)
4888
4889            groupchar = self._fields[0]._groupChar
4890            try:
4891                value = int(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', ''))
4892            except:
4893##                dbg('invalid number', indent=0)
4894                return None, signpos, right_signpos
4895
4896        else:   # float value
4897            try:
4898                groupchar = self._fields[0]._groupChar
4899                value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', ''))
4900##                dbg('value:', value)
4901            except:
4902                value = None
4903
4904            if value is not None and value < 0:
4905                signpos = text.find('-')
4906                if signpos == -1:
4907                    signpos = text.find('(')
4908
4909                text = text[:signpos] + self._template[signpos] + text[signpos+1:]
4910            else:
4911                # look forwards up to the decimal point for the 1st non-digit
4912##                dbg('decimal pos:', self._decimalpos)
4913##                dbg('text: "%s"' % text)
4914                if self._signOk:
4915                    signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1)
4916                    # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET
4917                    if len(text) >= signpos+1 and  text[signpos+1] in ('-','('):
4918                        signpos += 1
4919                else:
4920                    signpos = -1
4921##                dbg('signpos:', signpos)
4922
4923            if self._useParens:
4924                if self._signOk:
4925                    right_signpos = self._masklength - 1
4926                    text = text[:right_signpos] + ' '
4927                    if text[signpos] == '(':
4928                        text = text[:signpos] + ' ' + text[signpos+1:]
4929                else:
4930                    right_signpos = text.find(')')
4931                    if right_signpos != -1:
4932                        text = text[:-1]
4933                        right_signpos = -1
4934
4935            if value is None:
4936##                dbg('invalid number')
4937                text = None
4938
4939##        dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos)
4940##        dbg(indent=0)
4941        return text, signpos, right_signpos
4942
4943
4944    def _getSignedValue(self, candidate=None):
4945        """ Return a signed value by adding a "-" prefix if the value
4946            is set to negative, or a space if positive.
4947        """
4948##        dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1)
4949        if candidate is None: text = self._GetValue()
4950        else: text = candidate
4951
4952
4953        abstext, signpos, right_signpos = self._getAbsValue(text)
4954        if self._signOk:
4955            if abstext is None:
4956##                dbg(indent=0)
4957                return abstext, signpos, right_signpos
4958
4959            if self._isNeg or text[signpos] in ('-', '('):
4960                if self._useParens:
4961                    sign = '('
4962                else:
4963                    sign = '-'
4964            else:
4965                sign = ' '
4966            if abstext[signpos] not in string.digits:
4967                text = abstext[:signpos] + sign + abstext[signpos+1:]
4968            else:
4969                # this can happen if value passed is too big; sign assumed to be
4970                # in position 0, but if already filled with a digit, prepend sign...
4971                text = sign + abstext
4972            if self._useParens and text.find('(') != -1:
4973                text = text[:right_signpos] + ')' + text[right_signpos+1:]
4974        else:
4975            text = abstext
4976##        dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos)
4977##        dbg(indent=0)
4978        return text, signpos, right_signpos
4979
4980
4981    def GetPlainValue(self, candidate=None):
4982        """ Returns control's value stripped of the template text.
4983            plainvalue = MaskedEditMixin.GetPlainValue()
4984        """
4985##        dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1)
4986
4987        if candidate is None: text = self._GetValue()
4988        else: text = candidate
4989
4990        if self.IsEmpty():
4991##            dbg('returned ""', indent=0)
4992            return ""
4993        else:
4994            plain = ""
4995            for idx in range( min(len(self._template), len(text)) ):
4996                if self._mask[idx] in maskchars:
4997                    plain += text[idx]
4998
4999            if self._isFloat or self._isInt:
5000##                dbg('plain so far: "%s"' % plain)
5001                plain = plain.replace('(', '-').replace(')', ' ')
5002##                dbg('plain after sign regularization: "%s"' % plain)
5003
5004                if self._signOk and self._isNeg and plain.count('-') == 0:
5005                    # must be in reserved position; add to "plain value"
5006                    plain = '-' + plain.strip()
5007
5008                if self._fields[0]._alignRight:
5009                    lpad = plain.count(',')
5010                    plain = ' ' * lpad + plain.replace(',','')
5011                else:
5012                    plain = plain.replace(',','')
5013##                dbg('plain after pad and group:"%s"' % plain)
5014
5015##            dbg('returned "%s"' % plain.rstrip(), indent=0)
5016            return plain.rstrip()
5017
5018
5019    def IsEmpty(self, value=None):
5020        """
5021        Returns True if control is equal to an empty value.
5022        (Empty means all editable positions in the template == fillChar.)
5023        """
5024        if value is None: value = self._GetValue()
5025        if value == self._template and not self._defaultValue:
5026####            dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)")
5027            return True     # (all mask chars == fillChar by defn)
5028        elif value == self._template:
5029            empty = True
5030            for pos in range(len(self._template)):
5031####                dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos))
5032####                dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos])
5033                if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]):
5034                    empty = False
5035####            dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals())
5036            return empty
5037        else:
5038####            dbg("IsEmpty? 0 (value doesn't match template)")
5039            return False
5040
5041
5042    def IsDefault(self, value=None):
5043        """
5044        Returns True if the value specified (or the value of the control if not specified)
5045        is equal to the default value.
5046        """
5047        if value is None: value = self._GetValue()
5048        return value == self._template
5049
5050
5051    def IsValid(self, value=None):
5052        """ Indicates whether the value specified (or the current value of the control
5053        if not specified) is considered valid."""
5054####        dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1)
5055        if value is None: value = self._GetValue()
5056        ret = self._CheckValid(value)
5057####        dbg(indent=0)
5058        return ret
5059
5060
5061    def _eraseSelection(self, value=None, sel_start=None, sel_to=None):
5062        """ Used to blank the selection when inserting a new character. """
5063##        dbg("MaskedEditMixin::_eraseSelection", indent=1)
5064        if value is None: value = self._GetValue()
5065        if sel_start is None or sel_to is None:
5066            sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
5067##        dbg('value: "%s"' % value)
5068##        dbg("current sel_start, sel_to:", sel_start, sel_to)
5069
5070        newvalue = list(value)
5071        for i in range(sel_start, sel_to):
5072            if self._signOk and newvalue[i] in ('-', '(', ')'):
5073##                dbg('found sign (%s) at' % newvalue[i], i)
5074
5075                # balance parentheses:
5076                if newvalue[i] == '(':
5077                    right_signpos = value.find(')')
5078                    if right_signpos != -1:
5079                        newvalue[right_signpos] = ' '
5080
5081                elif newvalue[i] == ')':
5082                    left_signpos = value.find('(')
5083                    if left_signpos != -1:
5084                        newvalue[left_signpos] = ' '
5085
5086                newvalue[i] = ' '
5087
5088            elif self._isMaskChar(i):
5089                field = self._FindField(i)
5090                if field._padZero:
5091                    newvalue[i] = '0'
5092                else:
5093                    newvalue[i] = self._template[i]
5094
5095        value = "".join(newvalue)
5096##        dbg('new value: "%s"' % value)
5097##        dbg(indent=0)
5098        return value
5099
5100
5101    def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False):
5102        """ Handles replacement of the character at the current insertion point."""
5103##        dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
5104
5105        text = self._eraseSelection(value)
5106        field = self._FindField(pos)
5107        start, end = field._extent
5108        newtext = ""
5109        newpos = pos
5110
5111        # if >= 2 chars selected in a right-insert field, do appropriate erase on field,
5112        # then set selection to end, and do usual right insert.
5113        if sel_start != sel_to and sel_to >= sel_start+2:
5114            field = self._FindField(sel_start)
5115            if( field._insertRight                          # if right-insert
5116                and field._allowInsert                      # and allow insert at any point in field
5117                and field == self._FindField(sel_to) ):     # and selection all in same field
5118                text = self._OnErase(just_return_value=True)    # remove selection before insert
5119##                dbg('text after (left)erase: "%s"' % text)
5120                pos = sel_start = sel_to
5121
5122        if pos != sel_start and sel_start == sel_to:
5123            # adjustpos must have moved the position; make selection match:
5124            sel_start = sel_to = pos
5125
5126##        dbg('field._insertRight?', field._insertRight)
5127##        dbg('field._allowInsert?', field._allowInsert)
5128##        dbg('sel_start, end', sel_start, end)
5129        if sel_start < end:
5130##            dbg('text[sel_start] != field._fillChar?', text[sel_start] != field._fillChar)
5131            pass
5132
5133        if( field._insertRight                                  # field allows right insert
5134            and ((sel_start, sel_to) == field._extent           # and whole field selected
5135                 or (sel_start == sel_to                        # or nothing selected
5136                     and (sel_start == end                      # and cursor at right edge
5137                          or (field._allowInsert                # or field allows right-insert
5138                              and sel_start < end               # next to other char in field:
5139                              and text[sel_start] != field._fillChar) ) ) ) ):
5140##            dbg('insertRight')
5141            fstr = text[start:end]
5142            erasable_chars = [field._fillChar, ' ']
5143
5144            # if zero padding field, or a single digit, and currently a value of 0, allow erasure of 0:
5145            if field._padZero or (field._isInt and (end - start == 1) and fstr[0] == '0'):
5146                erasable_chars.append('0')
5147
5148            erased = ''
5149####            dbg("fstr[0]:'%s'" % fstr[0])
5150####            dbg('field_index:', field._index)
5151####            dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars)
5152####            dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", self._signOk and field._index == 0 and fstr[0] in ('-','('))
5153            if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')):
5154                erased = fstr[0]
5155####                dbg('value:      "%s"' % text)
5156####                dbg('fstr:       "%s"' % fstr)
5157####                dbg("erased:     '%s'" % erased)
5158                field_sel_start = sel_start - start
5159                field_sel_to = sel_to - start
5160##                dbg('left fstr:  "%s"' % fstr[1:field_sel_start])
5161##                dbg('right fstr: "%s"' % fstr[field_sel_to:end])
5162                fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end]
5163            if field._alignRight and sel_start != sel_to:
5164                field_len = end - start
5165##                pos += (field_len - len(fstr))    # move cursor right by deleted amount
5166                pos = sel_to
5167##                dbg('setting pos to:', pos)
5168                if field._padZero:
5169                    fstr = '0' * (field_len - len(fstr)) + fstr
5170                else:
5171                    fstr = fstr.rjust(field_len)   # adjust the field accordingly
5172##            dbg('field str: "%s"' % fstr)
5173
5174            newtext = text[:start] + fstr + text[end:]
5175            if erased in ('-', '(') and self._signOk:
5176                newtext = erased + newtext[1:]
5177##            dbg('newtext: "%s"' % newtext)
5178
5179            if self._signOk and field._index == 0:
5180                start -= 1             # account for sign position
5181
5182####            dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
5183####            dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
5184            if( field._moveOnFieldFull and pos == end
5185                and len(fstr.lstrip()) == end-start     # if field now full
5186                and (not field._stopFieldChangeIfInvalid     # and we either don't care about valid
5187                     or (field._stopFieldChangeIfInvalid     # or we do and the current field value is valid
5188                         and field.IsValid(fstr)))):
5189
5190                newpos = self._findNextEntry(end)       #   go to next field
5191            else:
5192                newpos = pos                            # else keep cursor at current position
5193
5194        if not newtext:
5195##            dbg('not newtext')
5196            if newpos != pos:
5197##                dbg('newpos:', newpos)
5198                pass
5199            if self._signOk and self._useParens:
5200                old_right_signpos = text.find(')')
5201
5202            if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start:
5203##                dbg('inserting within a left-insert-capable field')
5204                field_len = end - start
5205                before = text[start:sel_start]
5206                after = text[sel_to:end].strip()
5207####                dbg("current field:'%s'" % text[start:end])
5208####                dbg("before:'%s'" % before, "after:'%s'" % after)
5209                new_len = len(before) + len(after) + 1 # (for inserted char)
5210####                dbg('new_len:', new_len)
5211
5212                if new_len < field_len:
5213                    retained = after + self._template[end-(field_len-new_len):end]
5214                elif new_len > end-start:
5215                    retained = after[1:]
5216                else:
5217                    retained = after
5218
5219                left = text[0:start] + before
5220####                dbg("left:'%s'" % left, "retained:'%s'" % retained)
5221                right   = retained + text[end:]
5222            else:
5223                left  = text[0:pos]
5224                right   = text[pos+1:]
5225
5226            if not isinstance(char, six.text_type):
5227                # convert the keyboard constant to a unicode value, to
5228                # ensure it can be concatenated into the control value:
5229                if not six.PY3:
5230                    char = char.decode(self._defaultEncoding)
5231
5232            newtext = left + char + right
5233####            dbg('left:    "%s"' % left)
5234####            dbg('right:   "%s"' % right)
5235####            dbg('newtext: "%s"' % newtext)
5236
5237            if self._signOk and self._useParens:
5238                # Balance parentheses:
5239                left_signpos = newtext.find('(')
5240
5241                if left_signpos == -1:     # erased '('; remove ')'
5242                    right_signpos = newtext.find(')')
5243                    if right_signpos != -1:
5244                        newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:]
5245
5246                elif old_right_signpos != -1:
5247                    right_signpos = newtext.find(')')
5248
5249                    if right_signpos == -1: # just replaced right-paren
5250                        if newtext[pos] == ' ': # we just erased '); erase '('
5251                            newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:]
5252                        else:   # replaced with digit; move ') over
5253                            if self._ctrl_constraints._alignRight or self._isFloat:
5254                                newtext = newtext[:-1] + ')'
5255                            else:
5256                                rstripped_text = newtext.rstrip()
5257                                right_signpos = len(rstripped_text)
5258##                                dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos)
5259                                newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:]
5260
5261            if( field._insertRight                                  # if insert-right field (but we didn't start at right edge)
5262                and field._moveOnFieldFull                          # and should move cursor when full
5263                and len(newtext[start:end].strip()) == end-start    # and field now full
5264                and (not field._stopFieldChangeIfInvalid            # and we either don't care about valid
5265                     or (field._stopFieldChangeIfInvalid            # or we do and the current field value is valid
5266                         and field.IsValid(newtext[start:end].strip())))):
5267
5268                newpos = self._findNextEntry(end)                   #   go to next field
5269##                dbg('newpos = nextentry =', newpos)
5270            else:
5271##                dbg('pos:', pos, 'newpos:', pos+1)
5272                newpos = pos+1
5273
5274
5275        if allowAutoSelect:
5276            new_select_to = newpos     # (default return values)
5277            match_field = None
5278            match_index = None
5279
5280            if field._autoSelect:
5281                match_index, partial_match = self._autoComplete(1,  # (always forward)
5282                                                                field._compareChoices,
5283                                                                newtext[start:end],
5284                                                                compareNoCase=field._compareNoCase,
5285                                                                current_index = field._autoCompleteIndex-1)
5286                if match_index is not None and partial_match:
5287                    matched_str = newtext[start:end]
5288                    newtext = newtext[:start] + field._choices[match_index] + newtext[end:]
5289                    new_select_to = end
5290                    match_field = field
5291                    if field._insertRight:
5292                        # adjust position to just after partial match in field
5293                        newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip()))
5294
5295            elif self._ctrl_constraints._autoSelect:
5296                match_index, partial_match = self._autoComplete(
5297                                        1,  # (always forward)
5298                                        self._ctrl_constraints._compareChoices,
5299                                        newtext,
5300                                        self._ctrl_constraints._compareNoCase,
5301                                        current_index = self._ctrl_constraints._autoCompleteIndex - 1)
5302                if match_index is not None and partial_match:
5303                    matched_str = newtext
5304                    newtext = self._ctrl_constraints._choices[match_index]
5305                    edit_end = self._ctrl_constraints._extent[1]
5306                    new_select_to = min(edit_end, len(newtext.rstrip()))
5307                    match_field = self._ctrl_constraints
5308                    if self._ctrl_constraints._insertRight:
5309                        # adjust position to just after partial match in control:
5310                        newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip()))
5311
5312##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to)
5313##            dbg(indent=0)
5314            return newtext, newpos, new_select_to, match_field, match_index
5315        else:
5316##            dbg('newtext: "%s"' % newtext, 'newpos:', newpos)
5317##            dbg(indent=0)
5318            return newtext, newpos
5319
5320
5321    def _OnFocus(self,event):
5322        """
5323        This event handler is currently necessary to work around new default
5324        behavior as of wxPython2.3.3;
5325        The TAB key auto selects the entire contents of the wx.TextCtrl *after*
5326        the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
5327        *here*, because it hasn't happened yet.  So to prevent this behavior, and
5328        preserve the correct selection when the focus event is not due to tab,
5329        we need to pull the following trick:
5330        """
5331##        dbg('MaskedEditMixin::_OnFocus')
5332        if self.IsBeingDeleted() or self.GetParent().IsBeingDeleted():
5333            return
5334        wx.CallAfter(self._fixSelection)
5335        event.Skip()
5336        self.Refresh()
5337
5338
5339    def _CheckValid(self, candidate=None):
5340        """
5341        This is the default validation checking routine; It verifies that the
5342        current value of the control is a "valid value," and has the side
5343        effect of coloring the control appropriately.
5344        """
5345##        dbg(suspend=1)
5346##        dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1)
5347        oldValid = self._valid
5348        if candidate is None: value = self._GetValue()
5349        else: value = candidate
5350##        dbg('value: "%s"' % value)
5351        oldvalue = value
5352        valid = True    # assume True
5353
5354        if not self.IsDefault(value) and self._isDate:                    ## Date type validation
5355            valid = self._validateDate(value)
5356##            dbg("valid date?", valid)
5357
5358        elif not self.IsDefault(value) and self._isTime:
5359            valid = self._validateTime(value)
5360##            dbg("valid time?", valid)
5361
5362        elif not self.IsDefault(value) and (self._isInt or self._isFloat):  ## Numeric type
5363            valid = self._validateNumeric(value)
5364##            dbg("valid Number?", valid)
5365
5366        if valid:   # and not self.IsDefault(value):    ## generic validation accounts for IsDefault()
5367            ## valid so far; ensure also allowed by any list or regex provided:
5368            valid = self._validateGeneric(value)
5369##            dbg("valid value?", valid)
5370
5371##        dbg('valid?', valid)
5372
5373        if not candidate:
5374            self._valid = valid
5375            self._applyFormatting()
5376            if self._valid != oldValid:
5377##                dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
5378##                dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
5379                pass
5380##        dbg(indent=0, suspend=0)
5381        return valid
5382
5383
5384    def _validateGeneric(self, candidate=None):
5385        """ Validate the current value using the provided list or Regex filter (if any).
5386        """
5387        if candidate is None:
5388            text = self._GetValue()
5389        else:
5390            text = candidate
5391
5392        valid = True    # assume True
5393        for i in [-1] + self._field_indices:   # process global constraints first:
5394            field = self._fields[i]
5395            start, end = field._extent
5396            slice = text[start:end]
5397            valid = field.IsValid(slice)
5398            if not valid:
5399                break
5400
5401        return valid
5402
5403
5404    def _validateNumeric(self, candidate=None):
5405        """ Validate that the value is within the specified range (if specified.)"""
5406        if candidate is None: value = self._GetValue()
5407        else: value = candidate
5408        try:
5409            groupchar = self._fields[0]._groupChar
5410            if self._isFloat:
5411                number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', ''))
5412            else:
5413                number = int( value.replace(groupchar, '').replace('(', '-').replace(')', ''))
5414                if value.strip():
5415                    if self._fields[0]._alignRight:
5416                        require_digit_at = self._fields[0]._extent[1]-1
5417                    else:
5418                        require_digit_at = self._fields[0]._extent[0]
5419##                    dbg('require_digit_at:', require_digit_at)
5420##                    dbg("value[rda]: '%s'" % value[require_digit_at])
5421                    if value[require_digit_at] not in list(string.digits):
5422                        valid = False
5423                        return valid
5424                # else...
5425##            dbg('number:', number)
5426            if self._ctrl_constraints._hasRange:
5427                valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh
5428            else:
5429                valid = True
5430            groupcharpos = value.rfind(groupchar)
5431            if groupcharpos != -1:  # group char present
5432##                dbg('groupchar found at', groupcharpos)
5433                if self._isFloat and groupcharpos > self._decimalpos:
5434                    # 1st one found on right-hand side is past decimal point
5435##                    dbg('groupchar in fraction; illegal')
5436                    return False
5437                elif self._isFloat:
5438                    integer = value[:self._decimalpos].strip()
5439                else:
5440                    integer = value.strip()
5441##                dbg("integer:'%s'" % integer)
5442                if integer[0] in ('-', '('):
5443                    integer = integer[1:]
5444                if integer[-1] == ')':
5445                    integer = integer[:-1]
5446
5447                parts = integer.split(groupchar)
5448##                dbg('parts:', parts)
5449                for i in range(len(parts)):
5450                    if i == 0 and abs(int(parts[0])) > 999:
5451##                        dbg('group 0 too long; illegal')
5452                        valid = False
5453                        break
5454                    elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]):
5455##                        dbg('group %i (%s) not right size; illegal' % (i, parts[i]))
5456                        valid = False
5457                        break
5458        except ValueError:
5459##            dbg('value not a valid number')
5460            valid = False
5461        return valid
5462
5463
5464    def _validateDate(self, candidate=None):
5465        """ Validate the current date value using the provided Regex filter.
5466            Generally used for character types.BufferType
5467        """
5468##        dbg('MaskedEditMixin::_validateDate', indent=1)
5469        if candidate is None: value = self._GetValue()
5470        else: value = candidate
5471##        dbg('value = "%s"' % value)
5472        text = self._adjustDate(value, force4digit_year=True)     ## Fix the date up before validating it
5473##        dbg('text =', text)
5474        valid = True   # assume True until proven otherwise
5475
5476        try:
5477            # replace fillChar in each field with space:
5478            datestr = text[0:self._dateExtent]
5479            for i in range(3):
5480                field = self._fields[i]
5481                start, end = field._extent
5482                fstr = datestr[start:end]
5483                fstr.replace(field._fillChar, ' ')
5484                datestr = datestr[:start] + fstr + datestr[end:]
5485
5486            year, month, day = _getDateParts( datestr, self._datestyle)
5487            year = int(year)
5488##            dbg('self._dateExtent:', self._dateExtent)
5489            if self._dateExtent == 11:
5490                month = charmonths_dict[month.lower()]
5491            else:
5492                month = int(month)
5493            day = int(day)
5494##            dbg('year, month, day:', year, month, day)
5495
5496        except ValueError:
5497##            dbg('cannot convert string to integer parts')
5498            valid = False
5499        except KeyError:
5500##            dbg('cannot convert string to integer month')
5501            valid = False
5502
5503        if valid:
5504            # use wxDateTime to unambiguously try to parse the date:
5505            # ### Note: because wxDateTime is *brain-dead* and expects months 0-11,
5506            # rather than 1-12, so handle accordingly:
5507            if month > 12:
5508                valid = False
5509            else:
5510                month -= 1
5511                try:
5512##                    dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year))
5513                    dateHandler = wx.DateTime.FromDMY(day,month,year)
5514##                    dbg("succeeded")
5515                    dateOk = True
5516                except:
5517##                    dbg('cannot convert string to valid date')
5518                    dateOk = False
5519                if not dateOk:
5520                    valid = False
5521
5522            if valid:
5523                # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
5524                # so we eliminate them here:
5525                timeStr     = text[self._dateExtent+1:].strip()         ## time portion of the string
5526                if timeStr:
5527##                    dbg('timeStr: "%s"' % timeStr)
5528                    try:
5529                        checkTime    = dateHandler.ParseTime(timeStr)
5530                        valid = checkTime == len(timeStr)
5531                    except:
5532                        valid = False
5533                    if not valid:
5534##                        dbg('cannot convert string to valid time')
5535                        pass
5536##        if valid: dbg('valid date')
5537##        dbg(indent=0)
5538        return valid
5539
5540
5541    def _validateTime(self, candidate=None):
5542        """ Validate the current time value using the provided Regex filter.
5543            Generally used for character types.BufferType
5544        """
5545##        dbg('MaskedEditMixin::_validateTime', indent=1)
5546        # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
5547        # so we eliminate them here:
5548        if candidate is None: value = self._GetValue().strip()
5549        else: value = candidate.strip()
5550##        dbg('value = "%s"' % value)
5551        valid = True   # assume True until proven otherwise
5552
5553        dateHandler = wx.DateTime.Today()
5554        try:
5555            checkTime    = dateHandler.ParseTime(value)
5556##            dbg('checkTime:', checkTime, 'len(value)', len(value))
5557            valid = checkTime == len(value)
5558        except:
5559            valid = False
5560
5561        if not valid:
5562##            dbg('cannot convert string to valid time')
5563            pass
5564##        if valid: dbg('valid time')
5565##        dbg(indent=0)
5566        return valid
5567
5568
5569    def _OnKillFocus(self,event):
5570        """ Handler for EVT_KILL_FOCUS event.
5571        """
5572##        dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1)
5573        if self.IsBeingDeleted() or self.GetParent().IsBeingDeleted():
5574            return
5575        if self._mask and self._IsEditable():
5576            self._AdjustField(self._GetInsertionPoint())
5577            self._CheckValid()   ## Call valid handler
5578
5579        self._LostFocus()    ## Provided for subclass use
5580        event.Skip()
5581##        dbg(indent=0)
5582
5583
5584    def _fixSelection(self):
5585        """
5586        This gets called after the TAB traversal selection is made, if the
5587        focus event was due to this, but before the EVT_LEFT_* events if
5588        the focus shift was due to a mouse event.
5589
5590        The trouble is that, a priori, there's no explicit notification of
5591        why the focus event we received.  However, the whole reason we need to
5592        do this is because the default behavior on TAB traveral in a wx.TextCtrl is
5593        now to select the entire contents of the window, something we don't want.
5594        So we can *now* test the selection range, and if it's "the whole text"
5595        we can assume the cause, change the insertion point to the start of
5596        the control, and deselect.
5597        """
5598##        dbg('MaskedEditMixin::_fixSelection', indent=1)
5599        # can get here if called with wx.CallAfter after underlying
5600        # control has been destroyed on close, but after focus
5601        # events
5602        if not self or not self._mask or not self._IsEditable():
5603##            dbg(indent=0)
5604            return
5605
5606        sel_start, sel_to = self._GetSelection()
5607##        dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty())
5608
5609        if( sel_start == 0 and sel_to >= len( self._mask )   #(can be greater in numeric controls because of reserved space)
5610            and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ):
5611            # This isn't normally allowed, and so assume we got here by the new
5612            # "tab traversal" behavior, so we need to reset the selection
5613            # and insertion point:
5614##            dbg('entire text selected; resetting selection to start of control')
5615            self._goHome()
5616            field = self._FindField(self._GetInsertionPoint())
5617            edit_start, edit_end = field._extent
5618            if field._selectOnFieldEntry:
5619                if self._isFloat or self._isInt and field == self._fields[0]:
5620                    edit_start = 0
5621                self._SetInsertionPoint(edit_start)
5622                self._SetSelection(edit_start, edit_end)
5623
5624            elif field._insertRight:
5625                self._SetInsertionPoint(edit_end)
5626                self._SetSelection(edit_end, edit_end)
5627
5628        elif (self._isFloat or self._isInt):
5629
5630            text, signpos, right_signpos = self._getAbsValue()
5631            if text is None or text == self._template:
5632                integer = self._fields[0]
5633                edit_start, edit_end = integer._extent
5634
5635                if integer._selectOnFieldEntry:
5636##                    dbg('select on field entry:')
5637                    self._SetInsertionPoint(0)
5638                    self._SetSelection(0, edit_end)
5639
5640                elif integer._insertRight:
5641##                    dbg('moving insertion point to end')
5642                    self._SetInsertionPoint(edit_end)
5643                    self._SetSelection(edit_end, edit_end)
5644                else:
5645##                    dbg('numeric ctrl is empty; start at beginning after sign')
5646                    self._SetInsertionPoint(signpos+1)   ## Move past minus sign space if signed
5647                    self._SetSelection(signpos+1, signpos+1)
5648
5649        elif sel_start > self._goEnd(getPosOnly=True):
5650##            dbg('cursor beyond the end of the user input; go to end of it')
5651            self._goEnd()
5652        else:
5653##            dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength)
5654            pass
5655##        dbg(indent=0)
5656
5657
5658    def _Keypress(self,key):
5659        """ Method provided to override OnChar routine. Return False to force
5660            a skip of the 'normal' OnChar process. Called before class OnChar.
5661        """
5662        return True
5663
5664
5665    def _LostFocus(self):
5666        """ Method provided for subclasses. _LostFocus() is called after
5667            the class processes its EVT_KILL_FOCUS event code.
5668        """
5669        pass
5670
5671
5672    def _OnDoubleClick(self, event):
5673        """ selects field under cursor on dclick."""
5674        pos = self._GetInsertionPoint()
5675        field = self._FindField(pos)
5676        start, end = field._extent
5677        self._SetInsertionPoint(start)
5678        self._SetSelection(start, end)
5679
5680
5681    def _Change(self):
5682        """ Method provided for subclasses. Called by internal EVT_TEXT
5683            handler. Return False to override the class handler, True otherwise.
5684        """
5685        return True
5686
5687
5688    def _Cut(self):
5689        """
5690        Used to override the default Cut() method in base controls, instead
5691        copying the selection to the clipboard and then blanking the selection,
5692        leaving only the mask in the selected area behind.
5693        Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the
5694        derived control because the mixin functions can't override a method of
5695        a sibling class.
5696        """
5697##        dbg("MaskedEditMixin::_Cut", indent=1)
5698        value = self._GetValue()
5699##        dbg('current value: "%s"' % value)
5700        sel_start, sel_to = self._GetSelection()                   ## check for a range of selected text
5701##        dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
5702        do = wx.TextDataObject()
5703        do.SetText(value[sel_start:sel_to].strip())
5704        wx.TheClipboard.Open()
5705        wx.TheClipboard.SetData(do)
5706        wx.TheClipboard.Close()
5707
5708        if sel_to - sel_start != 0:
5709            self._OnErase()
5710##        dbg(indent=0)
5711
5712
5713# WS Note: overriding Copy is no longer necessary given that you
5714# can no longer select beyond the last non-empty char in the control.
5715#
5716##    def _Copy( self ):
5717##        """
5718##        Override the wx.TextCtrl's .Copy function, with our own
5719##        that does validation.  Need to strip trailing spaces.
5720##        """
5721##        sel_start, sel_to = self._GetSelection()
5722##        select_len = sel_to - sel_start
5723##        textval = wx.TextCtrl._GetValue(self)
5724##
5725##        do = wx.TextDataObject()
5726##        do.SetText(textval[sel_start:sel_to].strip())
5727##        wx.TheClipboard.Open()
5728##        wx.TheClipboard.SetData(do)
5729##        wx.TheClipboard.Close()
5730
5731
5732    def _getClipboardContents( self ):
5733        """ Subroutine for getting the current contents of the clipboard.
5734        """
5735        do = wx.TextDataObject()
5736        wx.TheClipboard.Open()
5737        success = wx.TheClipboard.GetData(do)
5738        wx.TheClipboard.Close()
5739
5740        if not success:
5741            return None
5742        else:
5743            # Remove leading and trailing spaces before evaluating contents
5744            return do.GetText().strip()
5745
5746
5747    def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False):
5748        """
5749        Used by paste routine and field choice validation to see
5750        if a given slice of paste text is legal for the area in question:
5751        returns validity, replacement text, and extent of paste in
5752        template.
5753        """
5754##        dbg(suspend=1)
5755##        dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1)
5756        select_length = sel_to - sel_start
5757        maxlength = select_length
5758##        dbg('sel_to - sel_start:', maxlength)
5759        if maxlength == 0:
5760            maxlength = self._masklength - sel_start
5761            item = 'control'
5762        else:
5763            item = 'selection'
5764##        dbg('maxlength:', maxlength)
5765        if not six.PY3 and not isinstance(paste_text, six.text_type):
5766            paste_text = paste_text.decode(self._defaultEncoding)
5767
5768        length_considered = len(paste_text)
5769        if length_considered > maxlength:
5770##            dbg('paste text will not fit into the %s:' % item, indent=0)
5771            if raise_on_invalid:
5772##                dbg(indent=0, suspend=0)
5773                if item == 'control':
5774                    ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
5775                    ve.value = paste_text
5776                    raise ve
5777                else:
5778                    ve = ValueError('"%s" will not fit into the selection' % paste_text)
5779                    ve.value = paste_text
5780                    raise ve
5781            else:
5782##                dbg(indent=0, suspend=0)
5783                return False, None, None
5784
5785        text = self._template
5786##        dbg('length_considered:', length_considered)
5787
5788        valid_paste = True
5789        replacement_text = ""
5790        replace_to = sel_start
5791        i = 0
5792        while valid_paste and i < length_considered and replace_to < self._masklength:
5793            if paste_text[i:] == self._template[replace_to:length_considered]:
5794                # remainder of paste matches template; skip char-by-char analysis
5795##                dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered))
5796                replacement_text += paste_text[i:]
5797                replace_to = i = length_considered
5798                continue
5799            # else:
5800            char = paste_text[i]
5801            field = self._FindField(replace_to)
5802            if not field._compareNoCase:
5803                if field._forceupper:   char = char.upper()
5804                elif field._forcelower: char = char.lower()
5805
5806##            dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to)
5807##            dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to))
5808            if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True):
5809                replacement_text += char
5810##                dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals())
5811##                dbg("replacement_text:", '"'+replacement_text+'"')
5812                i += 1
5813                replace_to += 1
5814            elif( char == self._template[replace_to]
5815                  or (self._signOk and
5816                          ( (i == 0 and (char == '-' or (self._useParens and char == '(')))
5817                            or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ):
5818                replacement_text += char
5819##                dbg("'%(char)s' == template(%(replace_to)d)" % locals())
5820##                dbg("replacement_text:", '"'+replacement_text+'"')
5821                i += 1
5822                replace_to += 1
5823            else:
5824                next_entry = self._findNextEntry(replace_to, adjustInsert=False)
5825                if next_entry == replace_to:
5826                    valid_paste = False
5827                else:
5828                    replacement_text += self._template[replace_to:next_entry]
5829##                    dbg("skipping template; next_entry =", next_entry)
5830##                    dbg("replacement_text:", '"'+replacement_text+'"')
5831                    replace_to = next_entry  # so next_entry will be considered on next loop
5832
5833        if not valid_paste and raise_on_invalid:
5834##            dbg('raising exception', indent=0, suspend=0)
5835            ve = ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
5836            ve.value = paste_text
5837            raise ve
5838
5839
5840        elif i < len(paste_text):
5841            valid_paste = False
5842            if raise_on_invalid:
5843##                dbg('raising exception', indent=0, suspend=0)
5844                ve = ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
5845                ve.value = paste_text
5846                raise ve
5847
5848##        dbg('valid_paste?', valid_paste)
5849        if valid_paste:
5850##            dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
5851            pass
5852##        dbg(indent=0, suspend=0)
5853        return valid_paste, replacement_text, replace_to
5854
5855
5856    def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
5857        """
5858        Used to override the base control's .Paste() function,
5859        with our own that does validation.
5860        Note: _Paste must be called from a Paste() override in the
5861        derived control because the mixin functions can't override a
5862        method of a sibling class.
5863        """
5864##        dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
5865        if value is None:
5866            paste_text = self._getClipboardContents()
5867        else:
5868            paste_text = value
5869
5870        if paste_text is not None:
5871
5872            if not six.PY3 and not isinstance(paste_text, six.text_type):
5873                paste_text = paste_text.decode(self._defaultEncoding)
5874
5875##            dbg('paste text: "%s"' % paste_text)
5876            # (conversion will raise ValueError if paste isn't legal)
5877            sel_start, sel_to = self._GetSelection()
5878##            dbg('selection:', (sel_start, sel_to))
5879
5880            # special case: handle allowInsert fields properly
5881            field = self._FindField(sel_start)
5882            edit_start, edit_end = field._extent
5883            new_pos = None
5884            if field._allowInsert and sel_to <= edit_end and (sel_start + len(paste_text) < edit_end or field._insertRight):
5885                if field._insertRight:
5886                    # want to paste to the left; see if it will fit:
5887                    left_text = self._GetValue()[edit_start:sel_start].lstrip()
5888##                    dbg('len(left_text):', len(left_text))
5889##                    dbg('len(paste_text):', len(paste_text))
5890##                    dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start)
5891                    if sel_start - (len(left_text) - (sel_to - sel_start) + len(paste_text)) >= edit_start:
5892                        # will fit! create effective paste text, and move cursor back to do so:
5893                        paste_text = left_text + paste_text
5894                        sel_start -= len(left_text)
5895                        paste_text = paste_text.rjust(sel_to - sel_start)
5896##                        dbg('modified paste_text to be: "%s"' % paste_text)
5897##                        dbg('modified selection to:', (sel_start, sel_to))
5898                    else:
5899##                        dbg("won't fit left;", 'paste text remains: "%s"' % paste_text)
5900                        pass
5901                else:
5902                    paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
5903##                    dbg("allow insert, but not insert right;", 'paste text set to: "%s"' % paste_text)
5904
5905
5906                new_pos = sel_start + len(paste_text)   # store for subsequent positioning
5907##                dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end)
5908##                dbg('expanded selection to:', (sel_start, sel_to))
5909
5910            # Another special case: paste won't fit, but it's a right-insert field where entire
5911            # non-empty value is selected, and there's room if the selection is expanded leftward:
5912            if( len(paste_text) > sel_to - sel_start
5913                and field._insertRight
5914                and sel_start > edit_start
5915                and sel_to >= edit_end
5916                and not self._GetValue()[edit_start:sel_start].strip() ):
5917                # text won't fit within selection, but left of selection is empty;
5918                # check to see if we can expand selection to accommodate the value:
5919                empty_space = sel_start - edit_start
5920                amount_needed = len(paste_text) - (sel_to - sel_start)
5921                if amount_needed <= empty_space:
5922                    sel_start -= amount_needed
5923##                    dbg('expanded selection to:', (sel_start, sel_to))
5924
5925
5926            # another special case: deal with signed values properly:
5927            if self._signOk:
5928                signedvalue, signpos, right_signpos = self._getSignedValue()
5929                paste_signpos = paste_text.find('-')
5930                if paste_signpos == -1:
5931                    paste_signpos = paste_text.find('(')
5932
5933                # if paste text will result in signed value:
5934####                dbg('paste_signpos != -1?', paste_signpos != -1)
5935####                dbg('sel_start:', sel_start, 'signpos:', signpos)
5936####                dbg('field._insertRight?', field._insertRight)
5937####                dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos)
5938                if paste_signpos != -1 and (sel_start <= signpos
5939                                            or (field._insertRight and sel_start - len(paste_text) <= signpos)):
5940                    signed = True
5941                else:
5942                    signed = False
5943                # remove "sign" from paste text, so we can auto-adjust for sign type after paste:
5944                paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','')
5945##                dbg('unsigned paste text: "%s"' % paste_text)
5946            else:
5947                signed = False
5948
5949            # another special case: deal with insert-right fields when selection is empty and
5950            # cursor is at end of field:
5951####            dbg('field._insertRight?', field._insertRight)
5952####            dbg('sel_start == edit_end?', sel_start == edit_end)
5953####            dbg('sel_start', sel_start, 'sel_to', sel_to)
5954            if field._insertRight and sel_start == edit_end and sel_start == sel_to:
5955                sel_start -= len(paste_text)
5956                if sel_start < 0:
5957                    sel_start = 0
5958##                dbg('adjusted selection:', (sel_start, sel_to))
5959
5960            raise_on_invalid = raise_on_invalid or field._raiseOnInvalidPaste
5961            try:
5962                valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
5963            except:
5964##                dbg('exception thrown', indent=0)
5965                raise
5966
5967            if not valid_paste:
5968##                dbg('paste text not legal for the selection or portion of the control following the cursor;')
5969                if not wx.Validator.IsSilent():
5970                    wx.Bell()
5971##                dbg(indent=0)
5972                return None, -1
5973            # else...
5974            text = self._eraseSelection()
5975
5976            new_text = text[:sel_start] + replacement_text + text[replace_to:]
5977            if new_text:
5978                new_text = new_text.ljust(self._masklength)
5979            if signed:
5980                new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text)
5981                if new_text:
5982                    if self._useParens:
5983                        new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:]
5984                    else:
5985                        new_text = new_text[:signpos] + '-' + new_text[signpos+1:]
5986                    if not self._isNeg:
5987                        self._isNeg = 1
5988
5989##            dbg("new_text:", '"'+new_text+'"')
5990
5991            if not just_return_value:
5992                if new_text != self._GetValue():
5993                    self.modified = True
5994                if new_text == '':
5995                    self.ClearValue()
5996                else:
5997                    wx.CallAfter(self._SetValue, new_text)
5998                    if new_pos is None:
5999                        new_pos = sel_start + len(replacement_text)
6000                    wx.CallAfter(self._SetInsertionPoint, new_pos)
6001            else:
6002##                dbg(indent=0)
6003                return new_text, replace_to
6004        elif just_return_value:
6005##            dbg(indent=0)
6006            return self._GetValue(), sel_to
6007##        dbg(indent=0)
6008
6009    def _Undo(self, value=None, prev=None, just_return_results=False):
6010        """ Provides an Undo() method in base controls. """
6011##        dbg("MaskedEditMixin::_Undo", indent=1)
6012        if value is None:
6013            value = self._GetValue()
6014        if prev is None:
6015            prev = self._prevValue
6016##        dbg('current value:  "%s"' % value)
6017##        dbg('previous value: "%s"' % prev)
6018        if prev is None:
6019##            dbg('no previous value', indent=0)
6020            return
6021
6022        elif value != prev:
6023            # Determine what to select: (relies on fixed-length strings)
6024            # (This is a lot harder than it would first appear, because
6025            # of mask chars that stay fixed, and so break up the "diff"...)
6026
6027            # Determine where they start to differ:
6028            i = 0
6029            length = len(value)     # (both are same length in masked control)
6030
6031            while( value[:i] == prev[:i] ):
6032                    i += 1
6033            sel_start = i - 1
6034
6035
6036            # handle signed values carefully, so undo from signed to unsigned or vice-versa
6037            # works properly:
6038            if self._signOk:
6039                text, signpos, right_signpos = self._getSignedValue(candidate=prev)
6040                if self._useParens:
6041                    if prev[signpos] == '(' and prev[right_signpos] == ')':
6042                        self._isNeg = True
6043                    else:
6044                        self._isNeg = False
6045                    # eliminate source of "far-end" undo difference if using balanced parens:
6046                    value = value.replace(')', ' ')
6047                    prev = prev.replace(')', ' ')
6048                elif prev[signpos] == '-':
6049                    self._isNeg = True
6050                else:
6051                    self._isNeg = False
6052
6053            # Determine where they stop differing in "undo" result:
6054            sm = difflib.SequenceMatcher(None, a=value, b=prev)
6055            i, j, k = sm.find_longest_match(sel_start, length, sel_start, length)
6056##            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] )
6057
6058            if k == 0:                              # no match found; select to end
6059                sel_to = length
6060            else:
6061                code_5tuples = sm.get_opcodes()
6062                for op, i1, i2, j1, j2 in code_5tuples:
6063##                    dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2]))
6064                    pass
6065
6066                diff_found = False
6067                # look backward through operations needed to produce "previous" value;
6068                # first change wins:
6069                for next_op in range(len(code_5tuples)-1, -1, -1):
6070                    op, i1, i2, j1, j2 = code_5tuples[next_op]
6071##                    dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2])
6072                    field = self._FindField(i2)
6073                    if op == 'insert' and prev[j1:j2] != self._template[j1:j2]:
6074##                        dbg('insert found: selection =>', (j1, j2))
6075                        sel_start = j1
6076                        sel_to = j2
6077                        diff_found = True
6078                        break
6079                    elif op == 'delete' and value[i1:i2] != self._template[i1:i2]:
6080                        edit_start, edit_end = field._extent
6081                        if field._insertRight and (field._allowInsert or i2 == edit_end):
6082                            sel_start = i2
6083                            sel_to = i2
6084                        else:
6085                            sel_start = i1
6086                            sel_to = j1
6087##                        dbg('delete found: selection =>', (sel_start, sel_to))
6088                        diff_found = True
6089                        break
6090                    elif op == 'replace':
6091                        if not prev[i1:i2].strip() and field._insertRight:
6092                            sel_start = sel_to = j2
6093                        else:
6094                            sel_start = j1
6095                            sel_to = j2
6096##                        dbg('replace found: selection =>', (sel_start, sel_to))
6097                        diff_found = True
6098                        break
6099
6100
6101                if diff_found:
6102                    # now go forwards, looking for earlier changes:
6103##                    dbg('searching forward...')
6104                    for next_op in range(len(code_5tuples)):
6105                        op, i1, i2, j1, j2 = code_5tuples[next_op]
6106                        field = self._FindField(i1)
6107                        if op == 'equal':
6108                            continue
6109                        elif op == 'replace':
6110                            if field._insertRight:
6111                                # if replace with spaces in an insert-right control, ignore "forward" replace
6112                                if not prev[i1:i2].strip():
6113                                    continue
6114                                elif j1 < i1:
6115##                                    dbg('setting sel_start to', j1)
6116                                    sel_start = j1
6117                                else:
6118##                                    dbg('setting sel_start to', i1)
6119                                    sel_start = i1
6120                            else:
6121##                                dbg('setting sel_start to', i1)
6122                                sel_start = i1
6123##                            dbg('saw replace; breaking')
6124                            break
6125                        elif op == 'insert' and not value[i1:i2]:
6126##                            dbg('forward %s found' % op)
6127                            if prev[j1:j2].strip():
6128##                                dbg('item to insert non-empty; setting sel_start to', j1)
6129                                sel_start = j1
6130                                break
6131                            elif not field._insertRight:
6132##                                dbg('setting sel_start to inserted space:', j1)
6133                                sel_start = j1
6134                                break
6135                        elif op == 'delete':
6136##                            dbg('delete; field._insertRight?', field._insertRight, 'value[%d:%d].lstrip: "%s"' % (i1,i2,value[i1:i2].lstrip()))
6137                            if field._insertRight:
6138                                if value[i1:i2].lstrip():
6139##                                    dbg('setting sel_start to ', j1)
6140                                    sel_start = j1
6141##                                    dbg('breaking loop')
6142                                    break
6143                                else:
6144                                    continue
6145                            else:
6146##                                dbg('saw delete; breaking')
6147                                break
6148                        else:
6149##                            dbg('unknown code!')
6150                            # we've got what we need
6151                            break
6152
6153
6154                if not diff_found:
6155##                    dbg('no insert,delete or replace found (!)')
6156                    # do "left-insert"-centric processing of difference based on l.c.s.:
6157                    if i == j and j != sel_start:         # match starts after start of selection
6158                        sel_to = sel_start + (j-sel_start)  # select to start of match
6159                    else:
6160                        sel_to = j                          # (change ends at j)
6161
6162
6163            # There are several situations where the calculated difference is
6164            # not what we want to select.  If changing sign, or just adding
6165            # group characters, we really don't want to highlight the characters
6166            # changed, but instead leave the cursor where it is.
6167            # Also, there a situations in which the difference can be ambiguous;
6168            # Consider:
6169            #
6170            # current value:    11234
6171            # previous value:   1111234
6172            #
6173            # Where did the cursor actually lie and which 1s were selected on the delete
6174            # operation?
6175            #
6176            # Also, difflib can "get it wrong;" Consider:
6177            #
6178            # current value:    "       128.66"
6179            # previous value:   "       121.86"
6180            #
6181            # difflib produces the following opcodes, which are sub-optimal:
6182            #    equal value[0:9] (       12) prev[0:9] (       12)
6183            #   insert value[9:9] () prev[9:11] (1.)
6184            #    equal value[9:10] (8) prev[11:12] (8)
6185            #   delete value[10:11] (.) prev[12:12] ()
6186            #    equal value[11:12] (6) prev[12:13] (6)
6187            #   delete value[12:13] (6) prev[13:13] ()
6188            #
6189            # This should have been:
6190            #    equal value[0:9] (       12) prev[0:9] (       12)
6191            #  replace value[9:11] (8.6) prev[9:11] (1.8)
6192            #    equal value[12:13] (6) prev[12:13] (6)
6193            #
6194            # But it didn't figure this out!
6195            #
6196            # To get all this right, we use the previous selection recorded to help us...
6197
6198            if (sel_start, sel_to) != self._prevSelection:
6199##                dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection)
6200
6201                prev_sel_start, prev_sel_to = self._prevSelection
6202                field = self._FindField(sel_start)
6203                if( self._signOk
6204                      and sel_start < self._masklength
6205                      and (prev[sel_start] in ('-', '(', ')')
6206                                     or value[sel_start] in ('-', '(', ')')) ):
6207                    # change of sign; leave cursor alone...
6208##                    dbg("prev[sel_start] in ('-', '(', ')')?", prev[sel_start] in ('-', '(', ')'))
6209##                    dbg("value[sel_start] in ('-', '(', ')')?", value[sel_start] in ('-', '(', ')'))
6210##                    dbg('setting selection to previous one')
6211                    sel_start, sel_to = self._prevSelection
6212
6213                elif field._groupdigits and (value[sel_start:sel_to] == field._groupChar
6214                                             or prev[sel_start:sel_to] == field._groupChar):
6215                    # do not highlight grouping changes
6216##                    dbg('value[sel_start:sel_to] == field._groupChar?', value[sel_start:sel_to] == field._groupChar)
6217##                    dbg('prev[sel_start:sel_to] == field._groupChar?', prev[sel_start:sel_to] == field._groupChar)
6218##                    dbg('setting selection to previous one')
6219                    sel_start, sel_to = self._prevSelection
6220
6221                else:
6222                    calc_select_len = sel_to - sel_start
6223                    prev_select_len = prev_sel_to - prev_sel_start
6224
6225##                    dbg('sel_start == prev_sel_start', sel_start == prev_sel_start)
6226##                    dbg('sel_to > prev_sel_to', sel_to > prev_sel_to)
6227
6228                    if prev_select_len >= calc_select_len:
6229                        # old selection was bigger; trust it:
6230##                        dbg('prev_select_len >= calc_select_len?', prev_select_len >= calc_select_len)
6231                        if not field._insertRight:
6232##                            dbg('setting selection to previous one')
6233                            sel_start, sel_to = self._prevSelection
6234                        else:
6235                            sel_to = self._prevSelection[1]
6236##                            dbg('setting selection to', (sel_start, sel_to))
6237
6238                    elif( sel_to > prev_sel_to                  # calculated select past last selection
6239                          and prev_sel_to < len(self._template) # and prev_sel_to not at end of control
6240                          and sel_to == len(self._template) ):  # and calculated selection goes to end of control
6241
6242                        i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length)
6243##                        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] )
6244                        if k > 0:
6245                            # difflib must not have optimized opcodes properly;
6246                            sel_to = j
6247
6248                    else:
6249                        # look for possible ambiguous diff:
6250
6251                        # if last change resulted in no selection, test from resulting cursor position:
6252                        if prev_sel_start == prev_sel_to:
6253                            calc_select_len = sel_to - sel_start
6254                            field = self._FindField(prev_sel_start)
6255
6256                            # determine which way to search from last cursor position for ambiguous change:
6257                            if field._insertRight:
6258                                test_sel_start = prev_sel_start
6259                                test_sel_to = prev_sel_start + calc_select_len
6260                            else:
6261                                test_sel_start = prev_sel_start - calc_select_len
6262                                test_sel_to = prev_sel_start
6263                        else:
6264                            test_sel_start, test_sel_to = prev_sel_start, prev_sel_to
6265
6266##                        dbg('test selection:', (test_sel_start, test_sel_to))
6267##                        dbg('calc change: "%s"' % prev[sel_start:sel_to])
6268##                        dbg('test change: "%s"' % prev[test_sel_start:test_sel_to])
6269
6270                        # if calculated selection spans characters, and same characters
6271                        # "before" the previous insertion point are present there as well,
6272                        # select the ones related to the last known selection instead.
6273                        if( sel_start != sel_to
6274                            and test_sel_to < len(self._template)
6275                            and prev[test_sel_start:test_sel_to] == prev[sel_start:sel_to] ):
6276
6277                            sel_start, sel_to = test_sel_start, test_sel_to
6278
6279                # finally, make sure that the old and new values are
6280                # different where we say they're different:
6281                while( sel_to - 1 > 0
6282                        and sel_to > sel_start
6283                        and value[sel_to-1:] == prev[sel_to-1:]):
6284                    sel_to -= 1
6285                while( sel_start + 1 < self._masklength
6286                        and sel_start < sel_to
6287                        and value[:sel_start+1] == prev[:sel_start+1]):
6288                    sel_start += 1
6289
6290##            dbg('sel_start, sel_to:', sel_start, sel_to)
6291##            dbg('previous value: "%s"' % prev)
6292##            dbg(indent=0)
6293            if just_return_results:
6294                return prev, (sel_start, sel_to)
6295            # else...
6296            self._SetValue(prev)
6297            self._SetInsertionPoint(sel_start)
6298            self._SetSelection(sel_start, sel_to)
6299
6300        else:
6301##            dbg('no difference between previous value')
6302##            dbg(indent=0)
6303            if just_return_results:
6304                return prev, self._GetSelection()
6305
6306
6307    def _OnClear(self, event):
6308        """ Provides an action for context menu delete operation """
6309        self.ClearValue()
6310
6311
6312    def _OnContextMenu(self, event):
6313##        dbg('MaskedEditMixin::OnContextMenu()', indent=1)
6314        menu = wx.Menu()
6315        menu.Append(wx.ID_UNDO, "Undo", "")
6316        menu.AppendSeparator()
6317        menu.Append(wx.ID_CUT, "Cut", "")
6318        menu.Append(wx.ID_COPY, "Copy", "")
6319        menu.Append(wx.ID_PASTE, "Paste", "")
6320        menu.Append(wx.ID_CLEAR, "Delete", "")
6321        menu.AppendSeparator()
6322        menu.Append(wx.ID_SELECTALL, "Select All", "")
6323
6324        wx.EVT_MENU(menu, wx.ID_UNDO, self._OnCtrl_Z)
6325        wx.EVT_MENU(menu, wx.ID_CUT, self._OnCtrl_X)
6326        wx.EVT_MENU(menu, wx.ID_COPY, self._OnCtrl_C)
6327        wx.EVT_MENU(menu, wx.ID_PASTE, self._OnCtrl_V)
6328        wx.EVT_MENU(menu, wx.ID_CLEAR, self._OnClear)
6329        wx.EVT_MENU(menu, wx.ID_SELECTALL, self._OnCtrl_A)
6330
6331        # ## WSS: The base control apparently handles
6332        # enable/disable of wx.ID_CUT, wx.ID_COPY, wx.ID_PASTE
6333        # and wx.ID_CLEAR menu items even if the menu is one
6334        # we created.  However, it doesn't do undo properly,
6335        # so we're keeping track of previous values ourselves.
6336        # Therefore, we have to override the default update for
6337        # that item on the menu:
6338        wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self._UndoUpdateUI)
6339        self._contextMenu = menu
6340
6341        self.PopupMenu(menu)
6342        menu.Destroy()
6343        self._contextMenu = None
6344##        dbg(indent=0)
6345
6346    def _UndoUpdateUI(self, event):
6347        if self._prevValue is None or self._prevValue == self._curValue:
6348            self._contextMenu.Enable(wx.ID_UNDO, False)
6349        else:
6350            self._contextMenu.Enable(wx.ID_UNDO, True)
6351
6352
6353    def _OnCtrlParametersChanged(self):
6354        """
6355        Overridable function to allow derived classes to take action as a
6356        result of parameter changes prior to possibly changing the value
6357        of the control.
6358        """
6359        pass
6360
6361 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
6362class MaskedEditAccessorsMixin:
6363    """
6364    To avoid a ton of boiler-plate, and to automate the getter/setter generation
6365    for each valid control parameter so we never forget to add the functions when
6366    adding parameters, this class programmatically adds the masked edit mixin
6367    parameters to itself.
6368    (This makes it easier for Designers like Boa to deal with masked controls.)
6369
6370    To further complicate matters, this is done with an extra level of inheritance,
6371    so that "general" classes like masked.TextCtrl can have all possible attributes,
6372    while derived classes, like masked.TimeCtrl and masked.NumCtrl can prevent
6373    exposure of those optional attributes of their base class that do not make
6374    sense for their derivation.
6375
6376    Therefore, we define:
6377        BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin)
6378    and
6379        masked.TextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin).
6380
6381    This allows us to then derive:
6382        masked.NumCtrl( BaseMaskedTextCtrl )
6383
6384    and not have to expose all the same accessor functions for the
6385    derived control when they don't all make sense for it.
6386
6387    """
6388
6389    # Define the default set of attributes exposed by the most generic masked controls:
6390    exposed_basectrl_params = list(MaskedEditMixin.valid_ctrl_params.keys()) + list(Field.valid_params.keys())
6391    exposed_basectrl_params.remove('index')
6392    exposed_basectrl_params.remove('extent')
6393    exposed_basectrl_params.remove('foregroundColour')   # (base class already has this)
6394
6395    for param in exposed_basectrl_params:
6396        propname = param[0].upper() + param[1:]
6397        exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
6398        exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
6399
6400        if param.find('Colour') != -1:
6401            # add non-british spellings, for backward-compatibility
6402            propname.replace('Colour', 'Color')
6403
6404            exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param))
6405            exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
6406
6407
6408
6409
6410## ---------- ---------- ---------- ---------- ---------- ---------- ----------
6411## these are helper subroutines:
6412
6413def _movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '):
6414    """ addseparators = add separator character every three numerals if True
6415    """
6416    fmt0 = fmtstring.split('.')
6417    fmt1 = fmt0[0]
6418    fmt2 = fmt0[1]
6419    val  = origvalue.split('.')[0].strip()
6420    ret  = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2)
6421    if neg:
6422        ret = '-' + ret[1:]
6423    return (ret,len(fmt1))
6424
6425
6426def _isDateType( fmtstring ):
6427    """ Checks the mask and returns True if it fits an allowed
6428        date or datetime format.
6429    """
6430    dateMasks = ("^##/##/####",
6431                 "^##-##-####",
6432                 "^##.##.####",
6433                 "^####/##/##",
6434                 "^####-##-##",
6435                 "^####.##.##",
6436                 "^##/CCC/####",
6437                 "^##.CCC.####",
6438                 "^##/##/##$",
6439                 "^##/##/## ",
6440                 "^##/CCC/##$",
6441                 "^##.CCC.## ",)
6442    reString  = "|".join(dateMasks)
6443    filter = re.compile( reString)
6444    if re.match(filter,fmtstring): return True
6445    return False
6446
6447def _isTimeType( fmtstring ):
6448    """ Checks the mask and returns True if it fits an allowed
6449        time format.
6450    """
6451    reTimeMask = "^##:##(:##)?( (AM|PM))?"
6452    filter = re.compile( reTimeMask )
6453    if re.match(filter,fmtstring): return True
6454    return False
6455
6456
6457def _isFloatingPoint( fmtstring):
6458    filter = re.compile("[ ]?[#]+\.[#]+\n")
6459    if re.match(filter,fmtstring+"\n"): return True
6460    return False
6461
6462
6463def _isInteger( fmtstring ):
6464    filter = re.compile("[#]+\n")
6465    if re.match(filter,fmtstring+"\n"): return True
6466    return False
6467
6468
6469def _getDateParts( dateStr, dateFmt ):
6470    if len(dateStr) > 11: clip = dateStr[0:11]
6471    else:                 clip = dateStr
6472    if clip[-2] not in string.digits:
6473        clip = clip[:-1]    # (got part of time; drop it)
6474
6475    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
6476    slices  = clip.split(dateSep)
6477    if dateFmt == "MDY":
6478        y,m,d = (slices[2],slices[0],slices[1])  ## year, month, date parts
6479    elif dateFmt == "DMY":
6480        y,m,d = (slices[2],slices[1],slices[0])  ## year, month, date parts
6481    elif dateFmt == "YMD":
6482        y,m,d = (slices[0],slices[1],slices[2])  ## year, month, date parts
6483    else:
6484        y,m,d = None, None, None
6485    if not y:
6486        return None
6487    else:
6488        return y,m,d
6489
6490
6491def _getDateSepChar(dateStr):
6492    clip   = dateStr[0:10]
6493    dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
6494    return dateSep
6495
6496
6497def _makeDate( year, month, day, dateFmt, dateStr):
6498    sep    = _getDateSepChar( dateStr)
6499    if dateFmt == "MDY":
6500        return "%s%s%s%s%s" % (month,sep,day,sep,year)  ## year, month, date parts
6501    elif dateFmt == "DMY":
6502        return "%s%s%s%s%s" % (day,sep,month,sep,year)  ## year, month, date parts
6503    elif dateFmt == "YMD":
6504        return "%s%s%s%s%s" % (year,sep,month,sep,day)  ## year, month, date parts
6505    else:
6506        return None
6507
6508
6509def _getYear(dateStr,dateFmt):
6510    parts = _getDateParts( dateStr, dateFmt)
6511    return parts[0]
6512
6513def _getMonth(dateStr,dateFmt):
6514    parts = _getDateParts( dateStr, dateFmt)
6515    return parts[1]
6516
6517def _getDay(dateStr,dateFmt):
6518    parts = _getDateParts( dateStr, dateFmt)
6519    return parts[2]
6520
6521## ---------- ---------- ---------- ---------- ---------- ---------- ----------
6522class __test(wx.App):
6523        def OnInit(self):
6524            from wx.lib.rcsizer import RowColSizer
6525            self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
6526            self.panel = wx.Panel( self.frame, -1)
6527            self.sizer = RowColSizer()
6528            self.labels = []
6529            self.editList  = []
6530            rowcount    = 4
6531
6532            id, id1 = wx.NewIdRef(2)
6533            self.command1  = wx.Button( self.panel, id, "&Close" )
6534            self.command2  = wx.Button( self.panel, id1, "&AutoFormats" )
6535            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
6536            self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5)
6537            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 )
6538##            self.panel.SetDefaultItem(self.command1 )
6539            self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2)
6540
6541            self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" )
6542            self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" )
6543            self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 )
6544            self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 )
6545            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 )
6546            self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 )
6547
6548
6549            label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field.
6550Note that all controls have been auto-sized by including F in the format code.
6551Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status)."""
6552            label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)."
6553
6554            self.label1 = wx.StaticText( self.panel, -1, label)
6555            self.label2 = wx.StaticText( self.panel, -1, "Description")
6556            self.label3 = wx.StaticText( self.panel, -1, "Mask Value")
6557            self.label4 = wx.StaticText( self.panel, -1, "Format")
6558            self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)")
6559            self.label6 = wx.StaticText( self.panel, -1, "MaskedEdit Ctrl")
6560            self.label7 = wx.StaticText( self.panel, -1, label2)
6561            self.label7.SetForegroundColour("Blue")
6562            self.label1.SetForegroundColour("Blue")
6563            self.label2.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6564            self.label3.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6565            self.label4.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6566            self.label5.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6567            self.label6.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6568
6569            self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5)
6570            self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5)
6571            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
6572            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
6573            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
6574            self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5)
6575            self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5)
6576
6577            # The following list is of the controls for the demo. Feel free to play around with
6578            # the options!
6579            controls = [
6580            #description        mask                    excl format     regexp                              range,list,initial
6581           ("Phone No",         "(###) ###-#### x:###", "", 'F!^-R',    "^\(\d\d\d\) \d\d\d-\d\d\d\d",    (),[],''),
6582           ("Last Name Only",   "C{14}",                "", 'F {list}', '^[A-Z][a-zA-Z]+',                  (),('Smith','Jones','Williams'),''),
6583           ("Full Name",        "C{14}",                "", 'F_',       '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+',   (),[],''),
6584           ("Social Sec#",      "###-##-####",          "", 'F',        "\d{3}-\d{2}-\d{4}",                (),[],''),
6585           ("U.S. Zip+4",       "#{5}-#{4}",            "", 'F',        "\d{5}-(\s{4}|\d{4})",(),[],''),
6586           ("U.S. State (2 char)\n(with default)","AA",                 "", 'F!',       "[A-Z]{2}",                         (),states, 'AZ'),
6587           ("Customer No",      "\CAA-###",              "", 'F!',      "C[A-Z]{2}-\d{3}",                   (),[],''),
6588           ("Date (MDY) + Time\n(with default)",      "##/##/#### ##:## AM",  'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"",                (),[], r'03/05/2003 12:00 AM'),
6589           ("Invoice Total",    "#{9}.##",              "", 'F-R,',     "",                                 (),[], ''),
6590           ("Integer (signed)\n(with default)", "#{6}",                 "", 'F-R',      "",                                 (),[], '0     '),
6591           ("Integer (unsigned)\n(with default), 1-399", "######",      "", 'F',        "",                                 (1,399),[], '1     '),
6592           ("Month selector",   "XXX",                  "", 'F',        "",                                 (),
6593                ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""),
6594           ("fraction selector","#/##",                 "", 'F',        "^\d\/\d\d?",                       (),
6595                ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "")
6596           ]
6597
6598            for control in controls:
6599                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
6600                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
6601                self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL)
6602                self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL)
6603
6604                if control in controls[:]:#-2]:
6605                    newControl  = MaskedTextCtrl( self.panel, -1, "",
6606                                                    mask         = control[1],
6607                                                    excludeChars = control[2],
6608                                                    formatcodes  = control[3],
6609                                                    includeChars = "",
6610                                                    validRegex   = control[4],
6611                                                    validRange   = control[5],
6612                                                    choices      = control[6],
6613                                                    defaultValue = control[7],
6614                                                    demo         = True)
6615                    if control[6]: newControl.SetCtrlParameters(choiceRequired = True)
6616                else:
6617                    newControl = MaskedComboBox(  self.panel, -1, "",
6618                                                    choices = control[7],
6619                                                    choiceRequired  = True,
6620                                                    mask         = control[1],
6621                                                    formatcodes  = control[3],
6622                                                    excludeChars = control[2],
6623                                                    includeChars = "",
6624                                                    validRegex   = control[4],
6625                                                    validRange   = control[5],
6626                                                    demo         = True)
6627                self.editList.append( newControl )
6628
6629                self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5)
6630                rowcount += 1
6631
6632            self.sizer.AddGrowableCol(4)
6633
6634            self.panel.SetSizer(self.sizer)
6635            self.panel.SetAutoLayout(1)
6636
6637            self.frame.Show(1)
6638            self.MainLoop()
6639
6640            return True
6641
6642        def onClick(self, event):
6643            self.frame.Close()
6644
6645        def onClickPage(self, event):
6646            self.page2 = __test2(self.frame,-1,"")
6647            self.page2.Show(True)
6648
6649        def _onCheck1(self,event):
6650            """ Set required value on/off """
6651            value = event.IsChecked()
6652            if value:
6653                for control in self.editList:
6654                    control.SetCtrlParameters(emptyInvalid=True)
6655                    control.Refresh()
6656            else:
6657                for control in self.editList:
6658                    control.SetCtrlParameters(emptyInvalid=False)
6659                    control.Refresh()
6660            self.panel.Refresh()
6661
6662        def _onCheck2(self,event):
6663            """ Highlight empty values"""
6664            value = event.IsChecked()
6665            if value:
6666                for control in self.editList:
6667                    control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine')
6668                    control.Refresh()
6669            else:
6670                for control in self.editList:
6671                    control.SetCtrlParameters( emptyBackgroundColour = 'White')
6672                    control.Refresh()
6673            self.panel.Refresh()
6674
6675
6676## ---------- ---------- ---------- ---------- ---------- ---------- ----------
6677
6678class __test2(wx.Frame):
6679        def __init__(self, parent, id, caption):
6680            wx.Frame.__init__( self, parent, id, "MaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
6681            from wx.lib.rcsizer import RowColSizer
6682            self.panel = wx.Panel( self, -1)
6683            self.sizer = RowColSizer()
6684            self.labels = []
6685            self.texts  = []
6686            rowcount    = 4
6687
6688            label = """\
6689All these controls have been created by passing a single parameter, the AutoFormat code.
6690The class contains an internal dictionary of types and formats (autoformats).
6691To see a great example of validations in action, try entering a bad email address, then tab out."""
6692
6693            self.label1 = wx.StaticText( self.panel, -1, label)
6694            self.label2 = wx.StaticText( self.panel, -1, "Description")
6695            self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code")
6696            self.label4 = wx.StaticText( self.panel, -1, "MaskedEdit Control")
6697            self.label1.SetForegroundColour("Blue")
6698            self.label2.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6699            self.label3.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6700            self.label4.SetFont(wx.Font(9,wx.FONTFAMILY_SWISS,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_BOLD))
6701
6702            self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5)
6703            self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
6704            self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
6705            self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
6706
6707            id, id1 = wx.NewIdRef(2)
6708            self.command1  = wx.Button( self.panel, id, "&Close")
6709            self.command2  = wx.Button( self.panel, id1, "&Print Formats")
6710            self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1)
6711            self.panel.SetDefaultItem(self.command1)
6712            self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2)
6713
6714            # The following list is of the controls for the demo. Feel free to play around with
6715            # the options!
6716            controls = [
6717           ("Phone No","USPHONEFULLEXT"),
6718           ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"),
6719           ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
6720           ("Time (with seconds)","TIMEHHMMSS"),
6721           ("Military Time\n(without seconds)","24HRTIMEHHMM"),
6722           ("Social Sec#","USSOCIALSEC"),
6723           ("Credit Card","CREDITCARD"),
6724           ("Expiration MM/YY","EXPDATEMMYY"),
6725           ("Percentage","PERCENT"),
6726           ("Person's Age","AGE"),
6727           ("US Zip Code","USZIP"),
6728           ("US Zip+4","USZIPPLUS4"),
6729           ("Email Address","EMAIL"),
6730           ("IP Address", "(derived control IpAddrCtrl)")
6731           ]
6732
6733            for control in controls:
6734                self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
6735                self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
6736                if control in controls[:-1]:
6737                    self.sizer.Add( MaskedTextCtrl( self.panel, -1, "",
6738                                                      autoformat  = control[1],
6739                                                      demo        = True),
6740                                row=rowcount,col=2,flag=wx.ALL,border=5)
6741                else:
6742                    self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ),
6743                                    row=rowcount,col=2,flag=wx.ALL,border=5)
6744                rowcount += 1
6745
6746            self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
6747            self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5)
6748            self.sizer.AddGrowableCol(3)
6749
6750            self.panel.SetSizer(self.sizer)
6751            self.panel.SetAutoLayout(1)
6752
6753        def onClick(self, event):
6754            self.Close()
6755
6756        def onClickPrint(self, event):
6757            for format in masktags.keys():
6758                sep = "+------------------------+"
6759                print("%s\n%s  \n  Mask: %s \n  RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']))
6760
6761## ---------- ---------- ---------- ---------- ---------- ---------- ----------
6762
6763if __name__ == "__main__":
6764    app = __test(False)
6765
6766__i=0
6767##
6768## Current Issues:
6769## ===================================
6770##
6771## 1. WS: For some reason I don't understand, the control is generating two (2)
6772##      EVT_TEXT events for every one (1) .SetValue() of the underlying control.
6773##      I've been unsuccessful in determining why or in my efforts to make just one
6774##      occur.  So, I've added a hack to save the last seen value from the
6775##      control in the EVT_TEXT handler, and if *different*, call event.Skip()
6776##      to propagate it down the event chain, and let the application see it.
6777##
6778## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the
6779##      behavior of the underlying control that I can't fix.  The problems are:
6780##      a) The background coloring doesn't work in the text field of the control;
6781##         instead, there's a only border around it that assumes the correct color.
6782##      b) The control will not pass WXK_TAB to the event handler, no matter what
6783##         I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to
6784##         indicate that we want these events.  As a result, MaskedComboBox
6785##         doesn't do the nice field-tabbing that MaskedTextCtrl does.
6786##      c) Auto-complete had to be reimplemented for the control because programmatic
6787##         setting of the value of the text field does not set up the auto complete
6788##         the way that the control processing keystrokes does.  (But I think I've
6789##         implemented a fairly decent approximation.)  Because of this the control
6790##         also won't auto-complete on dropdown, and there's no event I can catch
6791##         to work around this problem.
6792##      d) There is no method provided for getting the selection; the hack I've
6793##         implemented has its flaws, not the least of which is that due to the
6794##         strategy that I'm using, the paste buffer is always replaced by the
6795##         contents of the control's selection when in focus, on each keystroke;
6796##         this makes it impossible to paste anything into a MaskedComboBox
6797##         at the moment... :-(
6798##      e) The other deficient behavior, likely induced by the workaround for (d),
6799##         is that you can can't shift-left to select more than one character
6800##         at a time.
6801##
6802##
6803## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their
6804##      EVT_KEY_DOWN or EVT_CHAR event handlers.  Until this is fixed in
6805##      wxWindows, shift-tab won't take you backwards through the fields of
6806##      a MaskedTextCtrl like it should.  Until then Shifted arrow keys will
6807##      work like shift-tab and tab ought to.
6808##
6809
6810## To-Do's:
6811## =============================##
6812##  1. Add Popup list for auto-completable fields that simulates combobox on individual
6813##     fields.  Example: City validates against list of cities, or zip vs zip code list.
6814##  2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal"
6815##     control.
6816##  3. Fix shift-left selection for MaskedComboBox.
6817##  5. Transform notion of "decimal control" to be less "entire control"-centric,
6818##     so that monetary symbols can be included and still have the appropriate
6819##     semantics.  (Big job, as currently written, but would make control even
6820##     more useful for business applications.)
6821
6822
6823## CHANGELOG:
6824## ====================
6825##  Version 1.13
6826##  1. Added parameter option stopFieldChangeIfInvalid, which can be used to relax the
6827##     validation rules for a control, but make best efforts to stop navigation out of
6828##     that field should its current value be invalid.  Note: this does not prevent the
6829##     value from remaining invalid if focus for the control is lost, via mousing etc.
6830##
6831##  Version 1.12
6832##  1. Added proper support for NUMPAD keypad keycodes for navigation and control.
6833##
6834##  Version 1.11
6835##  1. Added value member to ValueError exceptions, so that people can catch them
6836##     and then display their own errors, and added attribute raiseOnInvalidPaste,
6837##     so one doesn't have to subclass the controls simply to force generation of
6838##     a ValueError on a bad paste operation.
6839##  2. Fixed handling of unicode charsets by converting to explicit control char
6840##     set testing for passing those keystrokes to the base control, and then
6841##     changing the semantics of the * maskchar to indicate any visible char.
6842##  3. Added '|' mask specification character, which allows splitting of contiguous
6843##     mask characters into separate fields, allowing finer control of behavior
6844##     of a control.
6845##
6846##
6847##  Version 1.10
6848##  1. Added handling for WXK_DELETE and WXK_INSERT, such that shift-delete
6849##     cuts, shift-insert pastes, and ctrl-insert copies.
6850##
6851##  Version 1.9
6852##  1. Now ignores kill focus events when being destroyed.
6853##  2. Added missing call to set insertion point on changing fields.
6854##  3. Modified SetKeyHandler() to accept None as means of removing one.
6855##  4. Fixed keyhandler processing for group and decimal character changes.
6856##  5. Fixed a problem that prevented input into the integer digit of a
6857##     integerwidth=1 numctrl, if the current value was 0.
6858##  6. Fixed logic involving processing of "_signOk" flag, to remove default
6859##     sign key handlers if false, so that SetAllowNegative(False) in the
6860##     NumCtrl works properly.
6861##  7. Fixed selection logic for numeric controls so that if selectOnFieldEntry
6862##     is true, and the integer portion of an integer format control is selected
6863##     and the sign position is selected, the sign keys will always result in a
6864##     negative value, rather than toggling the previous sign.
6865##
6866##
6867##  Version 1.8
6868##  1. Fixed bug involving incorrect variable name, causing combobox autocomplete to fail.
6869##  2. Added proper support for unicode version of wxPython
6870##  3. Added * as mask char meaning "all ansi chars" (ordinals 32-255).
6871##  4. Converted doc strings to use reST format, for ePyDoc documentation.
6872##  5. Renamed helper functions, classes, etc. not intended to be visible in public
6873##     interface to code.
6874##
6875##  Version 1.7
6876##  1. Fixed intra-right-insert-field erase, such that it doesn't leave a hole, but instead
6877##     shifts the text to the left accordingly.
6878##  2. Fixed _SetValue() to place cursor after last character inserted, rather than end of
6879##     mask.
6880##  3. Fixed some incorrect undo behavior for right-insert fields, and allowed derived classes
6881##     (eg. numctrl) to pass modified values for undo processing (to handle/ignore grouping
6882##     chars properly.)
6883##  4. Fixed autoselect behavior to work similarly to (2) above, so that combobox
6884##     selection will only select the non-empty text, as per request.
6885##  5. Fixed tabbing to work with 2.5.2 semantics.
6886##  6. Fixed size calculation to handle changing fonts
6887##
6888##  Version 1.6
6889##  1. Reorganized masked controls into separate package, renamed things accordingly
6890##  2. Split actual controls out of this file into their own files.
6891##  Version 1.5
6892##  (Reported) bugs fixed:
6893##   1. Crash ensues if you attempt to change the mask of a read-only
6894##      MaskedComboBox after initial construction.
6895##   2. Changed strategy of defining Get/Set property functions so that
6896##      these are now generated dynamically at runtime, rather than as
6897##      part of the class definition.  (This makes it possible to have
6898##      more general base classes that have many more options for configuration
6899##      without requiring that derivations support the same options.)
6900##   3. Fixed IsModified for _Paste() and _OnErase().
6901##
6902##   Enhancements:
6903##   1. Fixed "attribute function inheritance," since base control is more
6904##      generic than subsequent derivations, not all property functions of a
6905##      generic control should be exposed in those derivations.  New strategy
6906##      uses base control classes (eg. BaseMaskedTextCtrl) that should be
6907##      used to derive new class types, and mixed with their own mixins to
6908##      only expose those attributes from the generic masked controls that
6909##      make sense for the derivation.  (This makes Boa happier.)
6910##   2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less
6911##      "parochial."
6912##
6913##  Version 1.4
6914##  (Reported) bugs fixed:
6915##   1. Right-click menu allowed "cut" operation that destroyed mask
6916##      (was implemented by base control)
6917##   2. MaskedComboBox didn't allow .Append() of mixed-case values; all
6918##      got converted to lower case.
6919##   3. MaskedComboBox selection didn't deal with spaces in values
6920##      properly when autocompleting, and didn't have a concept of "next"
6921##      match for handling choice list duplicates.
6922##   4. Size of MaskedComboBox was always default.
6923##   5. Email address regexp allowed some "non-standard" things, and wasn't
6924##      general enough.
6925##   6. Couldn't easily reset MaskedComboBox contents programmatically.
6926##   7. Couldn't set emptyInvalid during construction.
6927##   8. Under some versions of wxPython, readonly comboboxes can apparently
6928##      return a GetInsertionPoint() result (655535), causing masked control
6929##      to fail.
6930##   9. Specifying an empty mask caused the controls to traceback.
6931##  10. Can't specify float ranges for validRange.
6932##  11. '.' from within a the static portion of a restricted IP address
6933##      destroyed the mask from that point rightward; tab when cursor is
6934##      before 1st field takes cursor past that field.
6935##
6936##  Enhancements:
6937##  12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.)
6938##  13. Added auto-select option on char input for masked controls with
6939##      choice lists.
6940##  14. Added '>' formatcode, allowing insert within a given or each field
6941##      as appropriate, rather than requiring "overwrite".  This makes single
6942##      field controls that just have validation rules (eg. EMAIL) much more
6943##      friendly.  The same flag controls left shift when deleting vs just
6944##      blanking the value, and for right-insert fields, allows right-insert
6945##      at any non-blank (non-sign) position in the field.
6946##  15. Added option to use to indicate negative values for numeric controls.
6947##  16. Improved OnFocus handling of numeric controls.
6948##  17. Enhanced Home/End processing to allow operation on a field level,
6949##      using ctrl key.
6950##  18. Added individual Get/Set functions for control parameters, for
6951##      simplified integration with Boa Constructor.
6952##  19. Standardized "Colour" parameter names to match wxPython, with
6953##      non-british spellings still supported for backward-compatibility.
6954##  20. Added '&' mask specification character for punctuation only (no letters
6955##      or digits).
6956##  21. Added (in a separate file) wx.MaskedCtrl() factory function to provide
6957##      unified interface to the masked edit subclasses.
6958##
6959##
6960##  Version 1.3
6961##   1. Made it possible to configure grouping, decimal and shift-decimal characters,
6962##      to make controls more usable internationally.
6963##   2. Added code to smart "adjust" value strings presented to .SetValue()
6964##      for right-aligned numeric format controls if they are shorter than
6965##      than the control width,  prepending the missing portion, prepending control
6966##      template left substring for the missing characters, so that setting
6967##      numeric values is easier.
6968##   3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved
6969##      for b-c), as this makes more sense.
6970##
6971##  Version 1.2
6972##   1. Fixed .SetValue() to replace the current value, rather than the current
6973##      selection. Also changed it to generate ValueError if presented with
6974##      either a value which doesn't follow the format or won't fit.  Also made
6975##      set value adjust numeric and date controls as if user entered the value.
6976##      Expanded doc explaining how SetValue() works.
6977##   2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
6978##      use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
6979##   3. Made all date autoformats automatically pick implied "datestyle".
6980##   4. Added IsModified override, since base wx.TextCtrl never reports modified if
6981##      .SetValue used to change the value, which is what the masked edit controls
6982##      use internally.
6983##   5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when
6984##      using tab to "leave field" and auto-adjust.
6985##   6. Fixed bug in _isCharAllowed() for negative number insertion on pastes,
6986##      and bug in ._Paste() that didn't account for signs in signed masks either.
6987##   7. Fixed issues with _adjustPos for right-insert fields causing improper
6988##      selection/replacement of values
6989##   8. Fixed _OnHome handler to properly handle extending current selection to
6990##      beginning of control.
6991##   9. Exposed all (valid) autoformats to demo, binding descriptions to
6992##      autoformats.
6993##  10. Fixed a couple of bugs in email regexp.
6994##  11. Made maskchardict an instance var, to make mask chars to be more
6995##      amenable to international use.
6996##  12. Clarified meaning of '-' formatcode in doc.
6997##  13. Fixed a couple of coding bugs being flagged by Python2.1.
6998##  14. Fixed several issues with sign positioning, erasure and validity
6999##      checking for "numeric" masked controls.
7000##  15. Added validation to IpAddrCtrl.SetValue().
7001##
7002##  Version 1.1
7003##   1. Changed calling interface to use boolean "useFixedWidthFont" (True by default)
7004##      vs. literal font facename, and use wxTELETYPE as the font family
7005##      if so specified.
7006##   2. Switched to use of dbg module vs. locally defined version.
7007##   3. Revamped entire control structure to use Field classes to hold constraint
7008##      and formatting data, to make code more hierarchical, allow for more
7009##      sophisticated masked edit construction.
7010##   4. Better strategy for managing options, and better validation on keywords.
7011##   5. Added 'V' format code, which requires that in order for a character
7012##      to be accepted, it must result in a string that passes the validRegex.
7013##   6. Added 'S' format code which means "select entire field when navigating
7014##      to new field."
7015##   7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment)
7016##   8. Added '<' format code to allow fields to require explicit cursor movement
7017##      to leave field.
7018##   9. Added validFunc option to other validation mechanisms, that allows derived
7019##      classes to add dynamic validation constraints to the control.
7020##  10. Fixed bug in validatePaste code causing possible IndexErrors, and also
7021##      fixed failure to obey case conversion codes when pasting.
7022##  11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere...
7023##  12. Removed condition from OnDecimalPoint, so that it always truncates right on '.'
7024##  13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal,
7025##      individual field validation to prevent field values > 255, and require explicit
7026##      tab/. to change fields.
7027##  14. Added handler for left double-click to select field under cursor.
7028##  15. Fixed handling for "Read-only" styles.
7029##  16. Separated signedForegroundColor from 'R' style, and added foregroundColor
7030##      attribute, for more consistent and controllable coloring.
7031##  17. Added retainFieldValidation parameter, allowing top-level constraints
7032##      such as "validRequired" to be set independently of field-level equivalent.
7033##      (needed in TimeCtrl for bounds constraints.)
7034##  18. Refactored code a bit, cleaned up and commented code more heavily, fixed
7035##      some of the logic for setting/resetting parameters, eg. fillChar, defaultValue,
7036##      etc.
7037##  19. Fixed maskchar setting for upper/lowercase, to work in all locales.
7038##
7039##
7040##  Version 1.0
7041##   1. Decimal point behavior restored for decimal and integer type controls:
7042##      decimal point now trucates the portion > 0.
7043##   2. Return key now works like the tab character and moves to the next field,
7044##      provided no default button is set for the form panel on which the control
7045##      resides.
7046##   3. Support added in _FindField() for subclasses controls (like timecontrol)
7047##      to determine where the current insertion point is within the mask (i.e.
7048##      which sub-'field'). See method documentation for more info and examples.
7049##   4. Added Field class and support for all constraints to be field-specific
7050##      in addition to being globally settable for the control.
7051##      Choices for each field are validated for length and pastability into
7052##      the field in question, raising ValueError if not appropriate for the control.
7053##      Also added selective additional validation based on individual field constraints.
7054##      By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PAGEUP and WXK_PAGEDOWN all
7055##      auto-complete fields with choice lists, supplying the 1st entry in
7056##      the choice list if the field is empty, and cycling through the list in
7057##      the appropriate direction if already a match.  WXK_DOWN will also auto-
7058##      complete if the field is partially completed and a match can be made.
7059##      SHIFT-WXK_UP/DOWN will also take you to the next field after any
7060##      auto-completion performed.
7061##   5. Added autoCompleteKeycodes=[] parameters for allowing further
7062##      customization of the control.  Any keycode supplied as a member
7063##      of the _autoCompleteKeycodes list will be treated like WXK_PAGEDOWN.  If
7064##      requireFieldChoice is set, then a valid value from each non-empty
7065##      choice list will be required for the value of the control to validate.
7066##   6. Fixed "auto-sizing" to be relative to the font actually used, rather
7067##      than making assumptions about character width.
7068##   7. Fixed GetMaskParameter(), which was non-functional in previous version.
7069##   8. Fixed exceptions raised to provide info on which control had the error.
7070##   9. Fixed bug in choice management of MaskedComboBox.
7071##  10. Fixed bug in IpAddrCtrl causing traceback if field value was of
7072##     the form '# #'.  Modified control code for IpAddrCtrl so that '.'
7073##     in the middle of a field clips the rest of that field, similar to
7074##     decimal and integer controls.
7075##
7076##
7077##  Version 0.0.7
7078##   1. "-" is a toggle for sign; "+" now changes - signed numerics to positive.
7079##   2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333).
7080##   3. New support for selecting text within the control.(thanks Will Sadkin!)
7081##      Shift-End and Shift-Home now select text as you would expect
7082##      Control-Shift-End selects to the end of the mask string, even if value not entered.
7083##      Control-A selects all *entered* text, Shift-Control-A selects everything in the control.
7084##   4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed-
7085##      for some reason I couldn't find the original email but thanks!!!)
7086##   5. All major key-handling code moved to their own methods for easier subclassing: OnHome,
7087##      OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc.
7088##   6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!).
7089##   (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...)
7090##   7. New mechanism for replacing default behavior for any given key, using
7091##      ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available
7092##      for easier subclassing of the control.
7093##   8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs
7094##      with insertion point/selection modification.  Changed Ctrl-X to use standard "cut"
7095##      semantics, erasing the selection, rather than erasing the entire control.
7096##   9. Added option for an "default value" (ie. the template) for use when a single fillChar
7097##      is not desired in every position.  Added IsDefault() function to mean "does the value
7098##      equal the template?" and modified .IsEmpty() to mean "do all of the editable
7099##      positions in the template == the fillChar?"
7100##  10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox,
7101##      now included.
7102##  11. MaskedComboBox now adds the capability to validate from list of valid values.
7103##      Example: City validates against list of cities, or zip vs zip code list.
7104##  12. Fixed oversight in EVT_TEXT handler that prevented the events from being
7105##      passed to the next handler in the event chain, causing updates to the
7106##      control to be invisible to the parent code.
7107##  13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within
7108##      the control, that auto-reformats as you move between cells.
7109##  14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'.
7110##  15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14}
7111##  16. Fixed major bugs in date validation, due to the fact that
7112##      wxDateTime.ParseDate is too liberal, and will accept any form that
7113##      makes any kind of sense, regardless of the datestyle you specified
7114##      for the control.  Unfortunately, the strategy used to fix it only
7115##      works for versions of wxPython post 2.3.3.1, as a C++ assert box
7116##      seems to show up on an invalid date otherwise, instead of a catchable
7117##      exception.
7118##  17. Enhanced date adjustment to automatically adjust heuristic based on
7119##      current year, making last century/this century determination on
7120##      2-digit year based on distance between today's year and value;
7121##      if > 50 year separation, assume last century (and don't assume last
7122##      century is 20th.)
7123##  18. Added autoformats and support for including HHMMSS as well as HHMM for
7124##      date times, and added similar time, and militaray time autoformats.
7125##  19. Enhanced tabbing logic so that tab takes you to the next field if the
7126##      control is a multi-field control.
7127##  20. Added stub method called whenever the control "changes fields", that
7128##      can be overridden by subclasses (eg. IpAddrCtrl.)
7129##  21. Changed a lot of code to be more functionally-oriented so side-effects
7130##      aren't as problematic when maintaining code and/or adding features.
7131##      Eg: IsValid() now does not have side-effects; it merely reflects the
7132##      validity of the value of the control; to determine validity AND recolor
7133##      the control, _CheckValid() should be used with a value argument of None.
7134##      Similarly, made most reformatting function take an optional candidate value
7135##      rather than just using the current value of the control, and only
7136##      have them change the value of the control if a candidate is not specified.
7137##      In this way, you can do validation *before* changing the control.
7138##  22. Changed validRequired to mean "disallow chars that result in invalid
7139##      value."  (Old meaning now represented by emptyInvalid.)  (This was
7140##      possible once I'd made the changes in (19) above.)
7141##  23. Added .SetMaskParameters and .GetMaskParameter methods, so they
7142##      can be set/modified/retrieved after construction.  Removed individual
7143##      parameter setting functions, in favor of this mechanism, so that
7144##      all adjustment of the control based on changing parameter values can
7145##      be handled in one place with unified mechanism.
7146##  24. Did a *lot* of testing and fixing re: numeric values.  Added ability
7147##      to type "grouping char" (ie. ',') and validate as appropriate.
7148##  25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9.
7149##  26. Fixed assumption about "decimal or integer" masks so that they're only
7150##      made iff there's no validRegex associated with the field.  (This
7151##      is so things like zipcodes which look like integers can have more
7152##      restrictive validation (ie. must be 5 digits.)
7153##  27. Added a ton more doc strings to explain use and derivation requirements
7154##      and did regularization of the naming conventions.
7155##  28. Fixed a range bug in _adjustKey preventing z from being handled properly.
7156##  29. Changed behavior of '.' (and shift-.) in numeric controls to move to
7157##      reformat the value and move the next field as appropriate. (shift-'.',
7158##      ie. '>' moves to the previous field.
7159
7160##  Version 0.0.6
7161##   1. Fixed regex bug that caused autoformat AGE to invalidate any age ending
7162##      in '0'.
7163##   2. New format character 'D' to trigger date type. If the user enters 2 digits in the
7164##      year position, the control will expand the value to four digits, using numerals below
7165##      50 as 21st century (20+nn) and less than 50 as 20th century (19+nn).
7166##      Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM}
7167##   3. revalid parameter renamed validRegex to conform to standard for all validation
7168##      parameters (see 2 new ones below).
7169##   4. New optional init parameter = validRange. Used only for int/dec (numeric) types.
7170##      Allows the developer to specify a valid low/high range of values.
7171##   5. New optional init parameter = validList. Used for character types. Allows developer
7172##      to send a list of values to the control to be used for specific validation.
7173##      See the Last Name Only example - it is list restricted to Smith/Jones/Williams.
7174##   6. Date type fields now use wxDateTime's parser to validate the date and time.
7175##      This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing
7176##      me toward this solution!
7177##   7. Date fields now automatically expand 2-digit years when it can. For example,
7178##      if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year
7179##      date is entered it will be expanded in any case when the user tabs out of the
7180##      field.
7181##   8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor,
7182##      SetSignedForeColor allow accessto override default class coloring behavior.
7183##   9. Documentation updated and improved.
7184##  10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better.
7185##      Two new options (checkboxes) - test highlight empty and disallow empty.
7186##  11. Home and End now work more intuitively, moving to the first and last user-entry
7187##      value, respectively.
7188##  12. New class function: SetRequired(bool). Sets the control's entry required flag
7189##      (i.e. disallow empty values if True).
7190##
7191##  Version 0.0.5
7192##   1. get_plainValue method renamed to GetPlainValue following the wxWindows
7193##      StudlyCaps(tm) standard (thanks Paul Moore).  ;)
7194##   2. New format code 'F' causes the control to auto-fit (auto-size) itself
7195##      based on the length of the mask template.
7196##   3. Class now supports "autoformat" codes. These can be passed to the class
7197##      on instantiation using the parameter autoformat="code". If the code is in
7198##      the dictionary, it will self set the mask, formatting, and validation string.
7199##      I have included a number of samples, but I am hoping that someone out there
7200##      can help me to define a whole bunch more.
7201##   4. I have added a second page to the demo (as well as a second demo class, test2)
7202##      to showcase how autoformats work. The way they self-format and self-size is,
7203##      I must say, pretty cool.
7204##   5. Comments added and some internal cosmetic revisions re: matching the code
7205##      standards for class submission.
7206##   6. Regex validation is now done in real time - field turns yellow immediately
7207##      and stays yellow until the entered value is valid
7208##   7. Cursor now skips over template characters in a more intuitive way (before the
7209##      next keypress).
7210##   8. Change, Keypress and LostFocus methods added for convenience of subclasses.
7211##      Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR,
7212##      and EVT_KILL_FOCUS, respectively.
7213##   9. Decimal and numeric handlers have been rewritten and now work more intuitively.
7214##
7215##  Version 0.0.4
7216##   1. New .IsEmpty() method returns True if the control's value is equal to the
7217##      blank template string
7218##   2. Control now supports a new init parameter: revalid. Pass a regular expression
7219##      that the value will have to match when the control loses focus. If invalid,
7220##      the control's BackgroundColor will turn yellow, and an internal flag is set (see next).
7221##   3. Demo now shows revalid functionality. Try entering a partial value, such as a
7222##      partial social security number.
7223##   4. New .IsValid() value returns True if the control is empty, or if the value matches
7224##      the revalid expression. If not, .IsValid() returns False.
7225##   5. Decimal values now collapse to decimal with '.00' on losefocus if the user never
7226##      presses the decimal point.
7227##   6. Cursor now goes to the beginning of the field if the user clicks in an
7228##      "empty" field intead of leaving the insertion point in the middle of the
7229##      field.
7230##   7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9.
7231##   8. New formatcodes init parameter replaces other init params and adds functions.
7232##      String passed to control on init controls:
7233##        _ Allow spaces
7234##        ! Force upper
7235##        ^ Force lower
7236##        R Show negative #s in red
7237##        , Group digits
7238##        - Signed numerals
7239##        0 Numeric fields get leading zeros
7240##   9. Ctrl-X in any field clears the current value.
7241##   10. Code refactored and made more modular (esp in OnChar method). Should be more
7242##       easy to read and understand.
7243##   11. Demo enhanced.
7244##   12. Now has _doc_.
7245##
7246##  Version 0.0.3
7247##   1. GetPlainValue() now returns the value without the template characters;
7248##      so, for example, a social security number (123-33-1212) would return as
7249##      123331212; also removes white spaces from numeric/decimal values, so
7250##      "-   955.32" is returned "-955.32". Press ctrl-S to see the plain value.
7251##   2. Press '.' in an integer style masked control and truncate any trailing digits.
7252##   3. Code moderately refactored. Internal names improved for clarity. Additional
7253##      internal documentation.
7254##   4. Home and End keys now supported to move cursor to beginning or end of field.
7255##   5. Un-signed integers and decimals now supported.
7256##   6. Cosmetic improvements to the demo.
7257##   7. Class renamed to MaskedTextCtrl.
7258##   8. Can now specify include characters that will override the basic
7259##      controls: for example, includeChars = "@." for email addresses
7260##   9. Added mask character 'C' -> allow any upper or lowercase character
7261##   10. .SetSignColor(str:color) sets the foreground color for negative values
7262##       in signed controls (defaults to red)
7263##   11. Overview documentation written.
7264##
7265##  Version 0.0.2
7266##   1. Tab now works properly when pressed in last position
7267##   2. Decimal types now work (e.g. #####.##)
7268##   3. Signed decimal or numeric values supported (i.e. negative numbers)
7269##   4. Negative decimal or numeric values now can show in red.
7270##   5. Can now specify an "exclude list" with the excludeChars parameter.
7271##      See date/time formatted example - you can only enter A or P in the
7272##      character mask space (i.e. AM/PM).
7273##   6. Backspace now works properly, including clearing data from a selected
7274##      region but leaving template characters intact. Also delete key.
7275##   7. Left/right arrows now work properly.
7276##   8. Removed EventManager call from test so demo should work with wxPython 2.3.3
7277##
7278