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