]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxPython/lib/maskededit.py
Applied patch that converts the throbber to using timers instead of threads
[wxWidgets.git] / wxPython / wxPython / lib / maskededit.py
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
6 # Copyright: (c) 2003 by Jeff Childers, 2003
7 # Portions: (c) 2002 by Will Sadkin, 2002-2003
8 # RCS-ID: $Id$
9 # License: wxWindows license
10 #----------------------------------------------------------------------------
11 # NOTE:
12 # This was written way it is because of the lack of masked edit controls
13 # in wxWindows/wxPython.
14 #
15 # wxMaskedEdit controls are based on a suggestion made on [wxPython-Users] by
16 # Jason Hihn, and borrows liberally from Will Sadkin's original masked edit
17 # control for time entry (wxTimeCtrl).
18 #
19 # wxMaskedEdit controls do not normally use validators, because they do
20 # careful manipulation of the cursor in the text window on each keystroke,
21 # and validation is cursor-position specific, so the control intercepts the
22 # key codes before the validator would fire. However, validators can be
23 # provided to do data transfer to the controls.
24 ##
25
26 """\
27 <b>Masked Edit Overview:
28 =====================</b>
29 <b>wxMaskedTextCtrl</b>
30 is a sublassed text control that can carefully control
31 the user's input based on a mask string you provide.
32
33 General usage example:
34 control = wxMaskedTextCtrl( win, -1, '', mask = '(###) ###-####')
35
36 The example above will create a text control that allows only numbers to
37 be entered and then only in the positions indicated in the mask by the #
38 character.
39
40 <b>wxMaskedComboBox</b>
41 is a similar subclass of wxComboBox that allows the
42 same sort of masking, but also can do auto-complete of values, and can
43 require the value typed to be in the list of choices to be colored
44 appropriately.
45
46 <b>wxIpAddrCtrl</b>
47 is a special subclass of wxMaskedTextCtrl that handles
48 cursor movement and natural typing of IP addresses.
49
50
51 <b>INITILIZATION PARAMETERS
52 ========================
53 mask=</b>
54 Allowed mask characters and function:
55 Character Function
56 # Allow numeric only (0-9)
57 N Allow letters and numbers (0-9)
58 A Allow uppercase letters only
59 a Allow lowercase letters only
60 C Allow any letter, upper or lower
61 X Allow string.letters, string.punctuation, string.digits
62
63
64 These controls define these sets of characters using string.letters,
65 string.uppercase, etc. These sets are affected by the system locale
66 setting, so in order to have the masked controls accept characters
67 that are specific to your users' language, your application should
68 set the locale.
69 For example, to allow international characters to be used in the
70 above masks, you can place the following in your code as part of
71 your application's initialization code:
72
73 import locale
74 locale.setlocale(locale.LC_ALL, '')
75
76
77 Using these mask characters, a variety of template masks can be built. See
78 the demo for some other common examples include date+time, social security
79 number, etc. If any of these characters are needed as template rather
80 than mask characters, they can be escaped with \, ie. \N means "literal N".
81 (use \\ for literal backslash, as in: r'CCC\\NNN'.)
82
83
84 <b>Note:</b>
85 Masks containing only # characters and one optional decimal point
86 character are handled specially, as "numeric" controls. Such
87 controls have special handling for typing the '-' key, handling
88 the "decimal point" character as truncating the ordinal portion,
89 optionally allowing grouping characters and so forth.
90 There are several parameters and format codes that only make sense
91 when combined with such masks, eg. groupChar, decimalChar, and so
92 forth (see below). These allow you to construct reasonable
93 numeric entry controls.
94
95 <b>Note:</b>
96 Changing the mask for a control deletes any previous field classes
97 (and any associated validation or formatting constraints) for them.
98
99 <b>useFixedWidthFont=</b>
100 By default, masked edit controls use a fixed width font, so that
101 the mask characters are fixed within the control, regardless of
102 subsequent modifications to the value. Set to False if having
103 the control font be the same as other controls is required.
104
105
106 <b>formatcodes=</b>
107 These other properties can be passed to the class when instantiating it:
108 Formatcodes are specified as a string of single character formatting
109 codes that modify behavior of the control:
110 _ Allow spaces
111 ! Force upper
112 ^ Force lower
113 R right-align field(s)
114 r right-insert in field(s) (implies R)
115 &lt; stay in field until explicit navigation out of it
116 , Allow grouping character in integer fields of numeric controls
117 and auto-group/regroup digits (if the result fits) when leaving
118 such a field. (If specified, .SetValue() will attempt to
119 auto-group as well.)
120 ',' is also the default grouping character. To change the
121 grouping character and/or decimal character, use the groupChar
122 and decimalChar parameters, respectively.
123 Note: typing the "decimal point" character in such fields will
124 clip the value to that left of the cursor for integer
125 fields of controls with "integer" or "floating point" masks.
126 If the ',' format code is specified, this will also cause the
127 resulting digits to be regrouped properly, using the current
128 grouping character.
129 - Prepend and reserve leading space for sign to mask and allow
130 signed values (negative #s shown in red by default)
131 0 integer fields get leading zeros
132 D Date[/time] field
133 T Time field
134 F Auto-Fit: the control calulates its size from
135 the length of the template mask
136 V validate entered chars against validRegex before allowing them
137 to be entered vs. being allowed by basic mask and then having
138 the resulting value just colored as invalid.
139 (See USSTATE autoformat demo for how this can be used.)
140 S select entire field when navigating to new field
141
142 <b>fillChar=
143 defaultValue=</b>
144 These controls have two options for the initial state of the control.
145 If a blank control with just the non-editable characters showing
146 is desired, simply leave the constructor variable fillChar as its
147 default (' '). If you want some other character there, simply
148 change the fillChar to that value. Note: changing the control's fillChar
149 will implicitly reset all of the fields' fillChars to this value.
150
151 If you need different default characters in each mask position,
152 you can specify a defaultValue parameter in the constructor, or
153 set them for each field individually.
154 This value must satisfy the non-editable characters of the mask,
155 but need not conform to the replaceable characters.
156
157 <b>groupChar=
158 decimalChar=</b>
159 These parameters govern what character is used to group numbers
160 and is used to indicate the decimal point for numeric format controls.
161 The default groupChar is ',', the default decimalChar is '.'
162 By changing these, you can customize the presentation of numbers
163 for your location.
164 eg: formatcodes = ',', groupChar="'" allows 12'345.34
165 formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34
166
167 <b>shiftDecimalChar=</b>
168 The default "shiftDecimalChar" (used for "backwards-tabbing" until
169 shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for
170 other keyboards, you may want to customize this, eg '?' for shift ',' on
171 AZERTY keyboards, ':' or ';' for other European keyboards, etc.
172
173 <b>autoCompleteKeycodes=[]</b>
174 By default, DownArrow, PageUp and PageDown will auto-complete a
175 partially entered field. Shift-DownArrow, Shift-UpArrow, PageUp
176 and PageDown will also auto-complete, but if the field already
177 contains a matched value, these keys will cycle through the list
178 of choices forward or backward as appropriate. Shift-Up and
179 Shift-Down also take you to the next/previous field after any
180 auto-complete action.
181
182 Additional auto-complete keys can be specified via this parameter.
183 Any keys so specified will act like PageDown.
184
185
186
187 <b>Validating User Input:
188 ======================</b>
189 There are a variety of initialization parameters that are used to validate
190 user input. These parameters can apply to the control as a whole, and/or
191 to individual fields:
192
193 excludeChars= A string of characters to exclude even if otherwise allowed
194 includeChars= A string of characters to allow even if otherwise disallowed
195 validRegex= Use a regular expression to validate the contents of the text box
196 validRange= Pass a rangeas list (low,high) to limit numeric fields/values
197 choiceRequired= value must be member of choices list
198 compareNoCase= Perform case-insensitive matching when validating against list
199 emptyInvalid= Boolean indicating whether an empty value should be considered invalid
200
201 validFunc= A function to call of the form: bool = func(candidate_value)
202 which will return True if the candidate_value satisfies some
203 external criteria for the control in addition to the the
204 other validation, or False if not. (This validation is
205 applied last in the chain of validations.)
206
207 validRequired= Boolean indicating whether or not keys that are allowed by the
208 mask, but result in an invalid value are allowed to be entered
209 into the control. Setting this to True implies that a valid
210 default value is set for the control.
211
212 retainFieldValidation=
213 False by default; if True, this allows individual fields to
214 retain their own validation constraints independently of any
215 subsequent changes to the control's overall parameters.
216
217 validator= Validators are not normally needed for masked controls, because
218 of the nature of the validation and control of input. However,
219 you can supply one to provide data transfer routines for the
220 controls.
221
222
223 <b>Coloring Behavior:
224 ==================</b>
225 The following parameters have been provided to allow you to change the default
226 coloring behavior of the control. These can be set at construction, or via
227 the .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow':
228
229 emptyBackgroundColor= Control Background color when identified as empty. Default=White
230 invalidBackgroundColor= Control Background color when identified as Not valid. Default=Yellow
231 validBackgroundColor= Control Background color when identified as Valid. Default=white
232
233
234 The following parameters control the default foreground color coloring behavior of the
235 control. Pass a color as string e.g. 'Yellow':
236 foregroundColor= Control foreground color when value is not negative. Default=Black
237 signedForegroundColor= Control foreground color when value is negative. Default=Red
238
239
240 <b>Fields:
241 =======</b>
242 Each part of the mask that allows user input is considered a field. The fields
243 are represented by their own class instances. You can specify field-specific
244 constraints by constructing or accessing the field instances for the control
245 and then specifying those constraints via parameters.
246
247 <b>fields=</b>
248 This parameter allows you to specify Field instances containing
249 constraints for the individual fields of a control, eg: local
250 choice lists, validation rules, functions, regexps, etc.
251 It can be either an ordered list or a dictionary. If a list,
252 the fields will be applied as fields 0, 1, 2, etc.
253 If a dictionary, it should be keyed by field index.
254 the values should be a instances of maskededit.Field.
255
256 Any field not represented by the list or dictionary will be
257 implicitly created by the control.
258
259 eg:
260 fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ]
261 or
262 fields = {
263 1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']),
264 3: ( Field(choices=['01', '02', '03'], choiceRequired=True)
265 }
266
267 The following parameters are available for individual fields, with the
268 same semantics as for the whole control but applied to the field in question:
269
270 fillChar # if set for a field, it will override the control's fillChar for that field
271 groupChar # if set for a field, it will override the control's default
272 defaultValue # sets field-specific default value; overrides any default from control
273 compareNoCase # overrides control's settings
274 emptyInvalid # determines whether field is required to be filled at all times
275 validRequired # if set, requires field to contain valid value
276
277 If any of the above parameters are subsequently specified for the control as a
278 whole, that new value will be propagated to each field, unless the
279 retainFieldValidation control-level parameter is set.
280
281 formatcodes # Augments control's settings
282 excludeChars # ' ' '
283 includeChars # ' ' '
284 validRegex # ' ' '
285 validRange # ' ' '
286 choices # ' ' '
287 choiceRequired # ' ' '
288 validFunc # ' ' '
289
290
291
292 <b>Control Class Functions:
293 ========================
294 .GetPlainValue(value=None)</b>
295 Returns the value specified (or the control's text value
296 not specified) without the formatting text.
297 In the example above, might return phone no='3522640075',
298 whereas control.GetValue() would return '(352) 264-0075'
299 <b>.ClearValue()</b>
300 Returns the control's value to its default, and places the
301 cursor at the beginning of the control.
302 <b>.SetValue()</b>
303 Does "smart replacement" of passed value into the control, as does
304 the .Paste() method. As with other text entry controls, the
305 .SetValue() text replacement begins at left-edge of the control,
306 with missing mask characters inserted as appropriate.
307 .SetValue will also adjust integer, float or date mask entry values,
308 adding commas, auto-completing years, etc. as appropriate.
309 For "right-aligned" numeric controls, it will also now automatically
310 right-adjust any value whose length is less than the width of the
311 control before attempting to set the value.
312 If a value does not follow the format of the control's mask, or will
313 not fit into the control, a ValueError exception will be raised.
314 Eg:
315 mask = '(###) ###-####'
316 .SetValue('1234567890') => '(123) 456-7890'
317 .SetValue('(123)4567890') => '(123) 456-7890'
318 .SetValue('(123)456-7890') => '(123) 456-7890'
319 .SetValue('123/4567-890') => illegal paste; ValueError
320
321 mask = '#{6}.#{2}', formatcodes = '_,-',
322 .SetValue('111') => ' 111 . '
323 .SetValue(' %9.2f' % -111.12345 ) => ' -111.12'
324 .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00'
325 .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError
326
327 mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control
328 .SetValue('111') => padded value misalignment ValueError: " 111" will not fit
329 .SetValue('%.2f' % 111 ) => ' 111.00'
330 .SetValue('%.2f' % -111.12345 ) => ' -111.12'
331
332
333 <b>.IsValid(value=None)</b>
334 Returns True if the value specified (or the value of the control
335 if not specified) passes validation tests
336 <b>.IsEmpty(value=None)</b>
337 Returns True if the value specified (or the value of the control
338 if not specified) is equal to an "empty value," ie. all
339 editable characters == the fillChar for their respective fields.
340 <b>.IsDefault(value=None)</b>
341 Returns True if the value specified (or the value of the control
342 if not specified) is equal to the initial value of the control.
343
344 <b>.Refresh()</b>
345 Recolors the control as appropriate to its current settings.
346
347 <b>.SetCtrlParameters(**kwargs)</b>
348 This function allows you to set up and/or change the control parameters
349 after construction; it takes a list of key/value pairs as arguments,
350 where the keys can be any of the mask-specific parameters in the constructor.
351 Eg:
352 ctl = wxMaskedTextCtrl( self, -1 )
353 ctl.SetCtrlParameters( mask='###-####',
354 defaultValue='555-1212',
355 formatcodes='F')
356
357 <b>.GetCtrlParameter(parametername)</b>
358 This function allows you to retrieve the current value of a parameter
359 from the control.
360
361 <b>.SetFieldParameters(field_index, **kwargs)</b>
362 This function allows you to specify change individual field
363 parameters after construction. (Indices are 0-based.)
364
365 <b>.GetFieldParameter(field_index, parametername)</b>
366 Allows the retrieval of field parameters after construction
367
368
369 The control detects certain common constructions. In order to use the signed feature
370 (negative numbers and coloring), the mask has to be all numbers with optionally one
371 decimal. Without a decimal (e.g. '######', the control will treat it as an integer
372 value. With a decimal (e.g. '###.##'), the control will act as a decimal control
373 (i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
374 integer control truncates the value.
375
376
377 Check your controls by calling each control's .IsValid() function and the
378 .IsEmpty() function to determine which controls have been a) filled in and
379 b) filled in properly.
380
381
382 Regular expression validations can be used flexibly and creatively.
383 Take a look at the demo; the zip-code validation succeeds as long as the
384 first five numerals are entered. the last four are optional, but if
385 any are entered, there must be 4 to be valid.
386
387 """
388
389 """
390 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
391 DEVELOPER COMMENTS:
392
393 Naming Conventions
394 ------------------
395 All methods of the Mixin that are not meant to be exposed to the external
396 interface are prefaced with '_'. Those functions that are primarily
397 intended to be internal subroutines subsequently start with a lower-case
398 letter; those that are primarily intended to be used and/or overridden
399 by derived subclasses start with a capital letter.
400
401 The following methods must be used and/or defined when deriving a control
402 from wxMaskedEditMixin. NOTE: if deriving from a *masked edit* control
403 (eg. class wxIpAddrCtrl(wxMaskedTextCtrl) ), then this is NOT necessary,
404 as it's already been done for you in the base class.
405
406 ._SetInitialValue()
407 This function must be called after the associated base
408 control has been initialized in the subclass __init__
409 function. It sets the initial value of the control,
410 either to the value specified if non-empty, the
411 default value if specified, or the "template" for
412 the empty control as necessary. It will also set/reset
413 the font if necessary and apply formatting to the
414 control at this time.
415
416 ._GetSelection()
417 REQUIRED
418 Each class derived from wxMaskedEditMixin must define
419 the function for getting the start and end of the
420 current text selection. The reason for this is
421 that not all controls have the same function name for
422 doing this; eg. wxTextCtrl uses .GetSelection(),
423 whereas we had to write a .GetMark() function for
424 wxComboBox, because .GetSelection() for the control
425 gets the currently selected list item from the combo
426 box, and the control doesn't (yet) natively provide
427 a means of determining the text selection.
428 ._SetSelection()
429 REQUIRED
430 Similarly to _GetSelection, each class derived from
431 wxMaskedEditMixin must define the function for setting
432 the start and end of the current text selection.
433 (eg. .SetSelection() for wxMaskedTextCtrl, and .SetMark() for
434 wxMaskedComboBox.
435
436 ._GetInsertionPoint()
437 ._SetInsertionPoint()
438 REQUIRED
439 For consistency, and because the mixin shouldn't rely
440 on fixed names for any manipulations it does of any of
441 the base controls, we require each class derived from
442 wxMaskedEditMixin to define these functions as well.
443
444 ._GetValue()
445 ._SetValue() REQUIRED
446 Each class derived from wxMaskedEditMixin must define
447 the functions used to get and set the raw value of the
448 control.
449 This is necessary so that recursion doesn't take place
450 when setting the value, and so that the mixin can
451 call the appropriate function after doing all its
452 validation and manipulation without knowing what kind
453 of base control it was mixed in with.
454
455 .Cut()
456 .Paste()
457 .SetValue() REQUIRED
458 Each class derived from wxMaskedEditMixin must redefine
459 these functions to call the _Cut(), _Paste() and _Paste()
460 methods, respectively for the control, so as to prevent
461 programmatic corruption of the control's value. This
462 must be done in each derivation, as the mixin cannot
463 itself override a member of a sibling class.
464
465 ._Refresh() REQUIRED
466 Each class derived from wxMaskedEditMixin must define
467 the function used to refresh the base control.
468
469 .Refresh() REQUIRED
470 Each class derived from wxMaskedEditMixin must redefine
471 this function so that it checks the validity of the
472 control (via self._CheckValid) and then refreshes
473 control using the base class method.
474
475 ._IsEditable() REQUIRED
476 Each class derived from wxMaskedEditMixin must define
477 the function used to determine if the base control is
478 editable or not. (For wxMaskedComboBox, this has to
479 be done with code, rather than specifying the proper
480 function in the base control, as there isn't one...)
481
482
483
484 Event Handling
485 --------------
486 Event handlers are "chained", and wxMaskedEditMixin usually
487 swallows most of the events it sees, thereby preventing any other
488 handlers from firing in the chain. It is therefore required that
489 each class derivation using the mixin to have an option to hook up
490 the event handlers itself or forego this operation and let a
491 subclass of the masked control do so. For this reason, each
492 subclass should probably include the following code:
493
494 if setupEventHandling:
495 ## Setup event handlers
496 EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
497 EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
498 EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick
499 EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
500 EVT_CHAR( self, self._OnChar ) ## handle each keypress
501 EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately
502
503 where setupEventHandling is an argument to its constructor.
504
505 These 5 handlers must be "wired up" for the wxMaskedEdit
506 control to provide default behavior. (The setupEventHandling
507 is an argument to wxMaskedTextCtrl and wxMaskedComboBox, so
508 that controls derived from *them* may replace one of these
509 handlers if they so choose.)
510
511 If your derived control wants to preprocess events before
512 taking action, it should then set up the event handling itself,
513 so it can be first in the event handler chain.
514
515
516 The following routines are available to facilitate changing
517 the default behavior of wxMaskedEdit controls:
518
519 ._SetKeycodeHandler(keycode, func)
520 ._SetKeyHandler(char, func)
521 Use to replace default handling for any given keycode.
522 func should take the key event as argument and return
523 False if no further action is required to handle the
524 key. Eg:
525 self._SetKeycodeHandler(WXK_UP, self.IncrementValue)
526 self._SetKeyHandler('-', self._OnChangeSign)
527
528 "Navigation" keys are assumed to change the cursor position, and
529 therefore don't cause automatic motion of the cursor as insertable
530 characters do.
531
532 ._AddNavKeycode(keycode, handler=None)
533 ._AddNavKey(char, handler=None)
534 Allows controls to specify other keys (and optional handlers)
535 to be treated as navigational characters. (eg. '.' in wxIpAddrCtrl)
536
537 ._GetNavKeycodes() Returns the current list of navigational keycodes.
538
539 ._SetNavKeycodes(key_func_tuples)
540 Allows replacement of the current list of keycode
541 processed as navigation keys, and bind associated
542 optional keyhandlers. argument is a list of key/handler
543 tuples. Passing a value of None for the handler in a
544 given tuple indicates that default processing for the key
545 is desired.
546
547 ._FindField(pos) Returns the Field object associated with this position
548 in the control.
549
550 ._FindFieldExtent(pos, getslice=False, value=None)
551 Returns edit_start, edit_end of the field corresponding
552 to the specified position within the control, and
553 optionally also returns the current contents of that field.
554 If value is specified, it will retrieve the slice the corresponding
555 slice from that value, rather than the current value of the
556 control.
557
558 ._AdjustField(pos)
559 This is, the function that gets called for a given position
560 whenever the cursor is adjusted to leave a given field.
561 By default, it adjusts the year in date fields if mask is a date,
562 It can be overridden by a derived class to
563 adjust the value of the control at that time.
564 (eg. wxIpAddrCtrl reformats the address in this way.)
565
566 ._Change() Called by internal EVT_TEXT handler. Return False to force
567 skip of the normal class change event.
568 ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force
569 skip of the normal class keypress event.
570 ._LostFocus() Called by internal EVT_KILL_FOCUS handler
571
572 ._OnKeyDown(event)
573 This is the default EVT_KEY_DOWN routine; it just checks for
574 "navigation keys", and if event.ControlDown(), it fires the
575 mixin's _OnChar() routine, as such events are not always seen
576 by the "cooked" EVT_CHAR routine.
577
578 ._OnChar(event) This is the main EVT_CHAR handler for the
579 wxMaskedEditMixin.
580
581 The following routines are used to handle standard actions
582 for control keys:
583 _OnArrow(event) used for arrow navigation events
584 _OnCtrl_A(event) 'select all'
585 _OnCtrl_S(event) 'save' (does nothing)
586 _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste
587 _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection
588
589 _OnChangeField(event) primarily used for tab events, but can be
590 used for other keys (eg. '.' in wxIpAddrCtrl)
591
592 _OnErase(event) used for backspace and delete
593 _OnHome(event)
594 _OnEnd(event)
595
596 """
597
598 from wxPython.wx import *
599 import string, re, copy
600
601 from wxPython.tools.dbg import Logger
602 dbg = Logger()
603 dbg(enable=0)
604
605 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
606
607 ## Constants for identifying control keys and classes of keys:
608
609 WXK_CTRL_X = (ord('X')+1) - ord('A') ## These keys are not already defined in wx
610 WXK_CTRL_V = (ord('V')+1) - ord('A')
611 WXK_CTRL_C = (ord('C')+1) - ord('A')
612 WXK_CTRL_S = (ord('S')+1) - ord('A')
613 WXK_CTRL_A = (ord('A')+1) - ord('A')
614
615 nav = (WXK_BACK, WXK_LEFT, WXK_RIGHT, WXK_UP, WXK_DOWN, WXK_TAB, WXK_HOME, WXK_END, WXK_RETURN, WXK_PRIOR, WXK_NEXT)
616 control = (WXK_BACK, WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, WXK_CTRL_X)
617
618
619 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
620
621 ## Constants for masking. This is where mask characters
622 ## are defined.
623 ## maskchars used to identify valid mask characters from all others
624 ## #- allow numeric 0-9 only
625 ## A- allow uppercase only. Combine with forceupper to force lowercase to upper
626 ## a- allow lowercase only. Combine with forcelower to force upper to lowercase
627 ## X- allow any character (string.letters, string.punctuation, string.digits)
628 ## Note: locale settings affect what "uppercase", lowercase, etc comprise.
629 ##
630 maskchars = ("#","A","a","X","C","N")
631
632 months = '(01|02|03|04|05|06|07|08|09|10|11|12)'
633 charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
634 charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
635 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
636
637 days = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)'
638 hours = '(0\d| \d|1[012])'
639 milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)'
640 minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
641 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\
642 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\
643 56|57|58|59)"""
644 seconds = minutes
645 am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde'
646
647 states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',')
648
649
650 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
651
652 ## The following table defines the current set of autoformat codes:
653
654 masktags = {
655 # Name: (mask, excludeChars, formatcodes, validRegex, choices, choiceRequired, description)
656 "USPHONEFULLEXT":("(###) ###-#### x:###","",'F^-R',"^\(\d{3}\) \d{3}-\d{4}",[], False, "Phone Number w/opt. ext"),
657 "USPHONETIGHTEXT":("###-###-#### x:###","",'F^-R',"^\d{3}-\d{3}-\d{4}",[], False, "Phone Number\n (w/hyphens and opt. ext)"),
658 "USPHONEFULL":("(###) ###-####","",'F^-R',"^\(\d{3}\) \d{3}-\d{4}",[], False, "Phone Number only"),
659 "USPHONETIGHT":("###-###-####","",'F^-R',"^\d{3}-\d{3}-\d{4}",[], False, "Phone Number\n(w/hyphens)"),
660 "USSTATE":("AA","",'F!V',"([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), states, True, "US State"),
661
662 "USDATETIMEMMDDYYYY/HHMMSS":("##/##/#### ##:##:## AM",am_pm_exclude,'DF!','^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "US Date + Time"),
663 "USDATETIMEMMDDYYYY-HHMMSS":("##-##-#### ##:##:## AM",am_pm_exclude,'DF!','^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "US Date + Time\n(w/hypens)"),
664 "USDATEMILTIMEMMDDYYYY/HHMMSS":("##/##/#### ##:##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[], False, "US Date + Military Time"),
665 "USDATEMILTIMEMMDDYYYY-HHMMSS":("##-##-#### ##:##:##",'','DF','^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[], False, "US Date + Military Time\n(w/hypens)"),
666 "USDATETIMEMMDDYYYY/HHMM":("##/##/#### ##:## AM",am_pm_exclude,'DF!','^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[], False, "US Date + Time\n(without seconds)"),
667 "USDATEMILTIMEMMDDYYYY/HHMM":("##/##/#### ##:##",'','DF','^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,[], False, "US Date + Military Time\n(without seconds)"),
668 "USDATETIMEMMDDYYYY-HHMM":("##-##-#### ##:## AM",am_pm_exclude,'DF!','^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[], False, "US Date + Time\n(w/hypens and w/o secs)"),
669 "USDATEMILTIMEMMDDYYYY-HHMM":("##-##-#### ##:##",'','DF','^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes,[], False, "US Date + Military Time\n(w/hyphens and w/o seconds)"),
670 "USDATEMMDDYYYY/":("##/##/####",'','DF','^' + months + '/' + days + '/' + '\d{4}',[], False, "US Date\n(MMDDYYYY)"),
671 "USDATEMMDDYY/":("##/##/##",'','DF','^' + months + '/' + days + '/\d\d',[], False, "US Date\n(MMDDYY)"),
672 "USDATEMMDDYYYY-":("##-##-####",'','DF','^' + months + '-' + days + '-' +'\d{4}',[], False, "MM-DD-YYYY"),
673
674 "EUDATEYYYYMMDD/":("####/##/##",'','DF','^' + '\d{4}'+ '/' + months + '/' + days,[], False, "YYYY/MM/DD"),
675 "EUDATEYYYYMMDD.":("####.##.##",'','DF','^' + '\d{4}'+ '.' + months + '.' + days,[], False, "YYYY.MM.DD"),
676 "EUDATEDDMMYYYY/":("##/##/####",'','DF','^' + days + '/' + months + '/' + '\d{4}',[], False, "DD/MM/YYYY"),
677 "EUDATEDDMMYYYY.":("##.##.####",'','DF','^' + days + '.' + months + '.' + '\d{4}',[], False, "DD.MM.YYYY"),
678 "EUDATEDDMMMYYYY.":("##.CCC.####",'','DF','^' + days + '.' + charmonths + '.' + '\d{4}',[], False, "DD.Month.YYYY"),
679 "EUDATEDDMMMYYYY/":("##/CCC/####",'','DF','^' + days + '/' + charmonths + '/' + '\d{4}',[], False, "DD/Month/YYYY"),
680
681 "EUDATETIMEYYYYMMDD/HHMMSS":("####/##/## ##:##:## AM",am_pm_exclude,'DF!','^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "YYYY/MM/DD HH:MM:SS"),
682 "EUDATETIMEYYYYMMDD.HHMMSS":("####.##.## ##:##:## AM",am_pm_exclude,'DF!','^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "YYYY.MM.DD HH:MM:SS"),
683 "EUDATETIMEDDMMYYYY/HHMMSS":("##/##/#### ##:##:## AM",am_pm_exclude,'DF!','^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "DD/MM/YYYY HH:MM:SS"),
684 "EUDATETIMEDDMMYYYY.HHMMSS":("##.##.#### ##:##:## AM",am_pm_exclude,'DF!','^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "DD.MM.YYYY HH:MM:SS"),
685
686 "EUDATETIMEYYYYMMDD/HHMM":("####/##/## ##:## AM",am_pm_exclude,'DF!','^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M',[], False, "YYYY/MM/DD HH:MM"),
687 "EUDATETIMEYYYYMMDD.HHMM":("####.##.## ##:## AM",am_pm_exclude,'DF!','^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M',[], False, "YYYY.MM.DD HH:MM"),
688 "EUDATETIMEDDMMYYYY/HHMM":("##/##/#### ##:## AM",am_pm_exclude,'DF!','^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[], False, "DD/MM/YYYY HH:MM"),
689 "EUDATETIMEDDMMYYYY.HHMM":("##.##.#### ##:## AM",am_pm_exclude,'DF!','^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',[], False, "DD.MM.YYYY HH:MM"),
690
691 "EUDATEMILTIMEYYYYMMDD/HHMMSS":("####/##/## ##:##:##",'','DF','^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds,[], False, "YYYY/MM/DD Mil. Time"),
692 "EUDATEMILTIMEYYYYMMDD.HHMMSS":("####.##.## ##:##:##",'','DF','^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds,[], False, "YYYY.MM.DD Mil. Time"),
693 "EUDATEMILTIMEDDMMYYYY/HHMMSS":("##/##/#### ##:##:##",'','DF','^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[], False, "DD/MM/YYYY Mil. Time"),
694 "EUDATEMILTIMEDDMMYYYY.HHMMSS":("##.##.#### ##:##:##",'','DF','^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,[], False, "DD.MM.YYYY Mil. Time"),
695 "EUDATEMILTIMEYYYYMMDD/HHMM":("####/##/## ##:##",'','DF','^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes,[], False, "YYYY/MM/DD Mil. Time\n(w/o seconds)"),
696 "EUDATEMILTIMEYYYYMMDD.HHMM":("####.##.## ##:##",'','DF','^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes,[], False, "YYYY.MM.DD Mil. Time\n(w/o seconds)"),
697 "EUDATEMILTIMEDDMMYYYY/HHMM":("##/##/#### ##:##",'','DF','^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes,[], False, "DD/MM/YYYY Mil. Time\n(w/o seconds)"),
698 "EUDATEMILTIMEDDMMYYYY.HHMM":("##.##.#### ##:##",'','DF','^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes,[], False, "DD.MM.YYYY Mil. Time\n(w/o seconds)"),
699
700 "TIMEHHMMSS":("##:##:## AM", am_pm_exclude, 'TF!', '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',[], False, "HH:MM:SS (A|P)M\n(see wxTimeCtrl)"),
701 "TIMEHHMM":("##:## AM", am_pm_exclude, 'TF!', '^' + hours + ':' + minutes + ' (A|P)M',[], False, "HH:MM (A|P)M\n(see wxTimeCtrl)"),
702 "MILTIMEHHMMSS":("##:##:##", "", 'TF', '^' + milhours + ':' + minutes + ':' + seconds,[], False, "Military HH:MM:SS\n(see wxTimeCtrl)"),
703 "MILTIMEHHMM":("##:##", "", 'TF', '^' + milhours + ':' + minutes,[], False, "Military HH:MM\n(see wxTimeCtrl)"),
704 "USSOCIALSEC":("###-##-####","",'F',"\d{3}-\d{2}-\d{4}",[], False, "Social Sec#"),
705 "CREDITCARD":("####-####-####-####","",'F',"\d{4}-\d{4}-\d{4}-\d{4}",[], False, "Credit Card"),
706 "EXPDATEMMYY":("##/##", "", "F", "^" + months + "/\d\d",[], False, "Expiration MM/YY"),
707 "USZIP":("#####","",'F',"^\d{5}",[], False, "US 5-digit zip code"),
708 "USZIPPLUS4":("#####-####","",'F',"\d{5}-(\s{4}|\d{4})",[], False, "US zip+4 code"),
709 "PERCENT":("0.##","",'F',"^0.\d\d",[], False, "Percentage"),
710 "AGE":("###","","F","^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]",[], False, "Age"),
711 "EMAIL":("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"," \\/*&%$#!+='\"","F",
712 "[a-zA-Z]\w*(\.\w+)*@\w+\.([a-zA-Z]\w*\.)*(com|org|net|edu|mil|gov|(co\.)?\w\w) *$",[], False, "Email address"),
713 "IPADDR":("###.###.###.###", "", 'F_Sr<',
714 "( \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}",[], False, "IP Address\n(see wxIpAddrCtrl)")
715 }
716
717 # build demo-friendly dictionary of descriptions of autoformats
718 autoformats = []
719 for key, value in masktags.items():
720 autoformats.append((key, value[6]))
721 autoformats.sort()
722
723 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
724
725 class Field:
726 valid_params = {
727 'index': None, ## which field of mask; set by parent control.
728 'mask': "", ## mask chars for this field
729 'extent': (), ## (edit start, edit_end) of field; set by parent control.
730 'formatcodes': "", ## codes indicating formatting options for the control
731 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given
732 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits
733 'decimalChar': '.', ## used with numeric fields; indicates what char separates ordinal from fraction
734 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard
735 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar
736 'excludeChars': "", ## optional string of chars to exclude even if main mask type does
737 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't
738 'validRegex': "", ## optional regular expression to use to validate the control
739 'validRange': (), ## Optional hi-low range for numerics
740 'choices': [], ## Optional list for character expressions
741 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list
742 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl
743 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search
744 'validRequired': False, ## Set to True to disallow input that results in an invalid value
745 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID
746 }
747
748
749 def __init__(self, **kwargs):
750 """
751 This is the "constructor" for setting up parameters for fields.
752 a field_index of -1 is used to indicate "the entire control."
753 """
754 ## dbg('Field::Field', indent=1)
755 # Validate legitimate set of parameters:
756 for key in kwargs.keys():
757 if key not in Field.valid_params.keys():
758 raise TypeError('invalid parameter "%s"' % (key))
759
760 # Set defaults for each parameter for this instance, and fully
761 # populate initial parameter list for configuration:
762 for key, value in Field.valid_params.items():
763 setattr(self, '_' + key, copy.copy(value))
764 if not kwargs.has_key(key):
765 kwargs[key] = copy.copy(value)
766
767 self._SetParameters(**kwargs)
768
769 ## dbg(indent=0)
770
771
772 def _SetParameters(self, **kwargs):
773 """
774 This function can be used to set individual or multiple parameters for
775 a masked edit field parameter after construction.
776 """
777 dbg(suspend=1)
778 dbg('maskededit.Field::_SetParameters', indent=1)
779 # Validate keyword arguments:
780 for key in kwargs.keys():
781 if key not in Field.valid_params.keys():
782 raise AttributeError('invalid keyword argument "%s"' % key)
783
784 if self._index is not None: dbg('field index:', self._index)
785 dbg('parameters:', indent=1)
786 for key, value in kwargs.items():
787 dbg('%s:' % key, value)
788 dbg(indent=0)
789
790 # First, Assign all parameters specified:
791 for key in Field.valid_params.keys():
792 if kwargs.has_key(key):
793 setattr(self, '_' + key, kwargs[key] )
794
795 # Now go do validation, semantic and inter-dependency parameter processing:
796 if kwargs.has_key('choiceRequired'): # (set/changed)
797 if self._choiceRequired:
798 self._choices = [choice.strip() for choice in self._choices]
799
800 if kwargs.has_key('compareNoCase'): # (set/changed)
801 if self._compareNoCase and self._choices:
802 self._choices = [item.lower() for item in self._choices]
803 dbg('modified choices:', self._choices)
804
805 if kwargs.has_key('formatcodes'): # (set/changed)
806 self._forceupper = '!' in self._formatcodes
807 self._forcelower = '^' in self._formatcodes
808 self._groupdigits = ',' in self._formatcodes
809 self._okSpaces = '_' in self._formatcodes
810 self._padZero = '0' in self._formatcodes
811 self._autofit = 'F' in self._formatcodes
812 self._insertRight = 'r' in self._formatcodes
813 self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes
814 self._moveOnFieldFull = not '<' in self._formatcodes
815 self._selectOnFieldEntry = 'S' in self._formatcodes
816
817 if self._groupdigits:
818 if kwargs.has_key('groupChar'):
819 self._groupChar = kwargs['groupChar']
820 if kwargs.has_key('decimalChar'):
821 self._decimalChar = kwargs['decimalChar']
822 if kwargs.has_key('shiftDecimalChar'):
823 self._shiftDecimalChar = kwargs['shiftDecimalChar']
824
825 if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'):
826 self._regexMask = 'V' in self._formatcodes and self._validRegex
827
828 if kwargs.has_key('validRegex'): # (set/changed)
829 if self._validRegex:
830 try:
831 if self._compareNoCase:
832 self._filter = re.compile(self._validRegex, re.IGNORECASE)
833 else:
834 self._filter = re.compile(self._validRegex)
835 except:
836 raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
837 else:
838 self._filter = None
839
840 if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed)
841 self._isInt = isInteger(self._mask)
842 dbg('isInt?', self._isInt)
843
844 if kwargs.has_key('validRange'): # (set/changed)
845 self._hasRange = False
846 self._rangeHigh = 0
847 self._rangeLow = 0
848 if self._validRange:
849 if type(self._validRange) != type(()) or len( self._validRange )!= 2 or self._validRange[0] >= self._validRange[1]:
850 raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a < b'
851 % (str(self._index), repr(self._validRange)) )
852
853 self._hasRange = True
854 self._rangeLow = self._validRange[0]
855 self._rangeHigh = self._validRange[1]
856
857 if kwargs.has_key('choices'): # (set/changed)
858 self._hasList = False
859 if type(self._choices) not in (type(()), type([])):
860 raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
861 elif len( self._choices) > 0:
862 for choice in self._choices:
863 if type(choice) != type(''):
864 raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
865
866 # Verify each choice specified is valid:
867 for choice in self._choices:
868 if not self.IsValid(choice):
869 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
870 self._hasList = True
871
872 # reset field validity assumption:
873 self._valid = True
874 dbg(indent=0, suspend=0)
875
876
877 def _GetParameter(self, paramname):
878 """
879 Routine for retrieving the value of any given parameter
880 """
881 if Field.valid_params.has_key(paramname):
882 return getattr(self, '_' + paramname)
883 else:
884 TypeError('Field._GetParameter: invalid parameter "%s"' % key)
885
886
887 def IsEmpty(self, slice):
888 """
889 Indicates whether the specified slice is considered empty for the
890 field.
891 """
892 dbg('Field::IsEmpty("%s")' % slice, indent=1)
893 if not hasattr(self, '_template'):
894 raise AttributeError('_template')
895
896 dbg('self._template: "%s"' % self._template)
897 dbg('self._defaultValue: "%s"' % str(self._defaultValue))
898 if slice == self._template and not self._defaultValue:
899 dbg(indent=0)
900 return True
901
902 elif slice == self._template:
903 empty = True
904 for pos in range(len(self._template)):
905 ## dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos])
906 if slice[pos] not in (' ', self._fillChar):
907 empty = False
908 break
909 dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0)
910 return empty
911 else:
912 dbg("IsEmpty? 0 (slice doesn't match template)", indent=0)
913 return False
914
915
916 def IsValid(self, slice):
917 """
918 Indicates whether the specified slice is considered a valid value for the
919 field.
920 """
921 dbg(suspend=1)
922 dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1)
923 valid = True # assume true to start
924
925 if self._emptyInvalid and self.IsEmpty(slice):
926 valid = False
927
928 elif self._hasList and self._choiceRequired:
929 dbg("(member of list required)")
930 # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices):
931 compareStr = slice.strip()
932 if self._compareNoCase:
933 compareStr = compareStr.lower()
934 valid = (compareStr in self._choices)
935
936 elif self._hasRange and not self.IsEmpty(slice):
937 dbg('validating against range')
938 try:
939 valid = self._rangeLow <= int(slice) <= self._rangeHigh
940 except:
941 valid = False
942
943 elif self._validRegex and self._filter:
944 dbg('validating against regex')
945 valid = (re.match( self._filter, slice) is not None)
946
947 if valid and self._validFunc:
948 dbg('validating against supplied function')
949 valid = self._validFunc(slice)
950 dbg('valid?', valid, indent=0, suspend=0)
951 return valid
952
953
954 def _AdjustField(self, slice):
955 """ 'Fixes' an integer field. Right or left-justifies, as required."""
956 dbg('Field::_AdjustField("%s")' % slice, indent=1)
957 length = len(self._mask)
958 if self._isInt:
959 signpos = slice.find('-')
960 intStr = slice.replace( '-', '' ) # drop sign, if any
961 intStr = intStr.replace(' ', '') # drop extra spaces
962 intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars
963 intStr = string.replace(intStr,"-","") # drop sign, if any
964 intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots
965 if self._groupdigits:
966 new = ''
967 cnt = 1
968 for i in range(len(intStr)-1, -1, -1):
969 new = intStr[i] + new
970 if (cnt) % 3 == 0:
971 new = self._groupChar + new
972 cnt += 1
973 if new and new[0] == self._groupChar:
974 new = new[1:]
975 if len(new) <= length:
976 # expanded string will still fit and leave room for sign:
977 intStr = new
978 # else... leave it without the commas...
979
980 dbg('padzero?', self._padZero)
981 dbg('len(intStr):', len(intStr), 'field length:', length)
982 if self._padZero and len(intStr) < length:
983 intStr = '0' * (length - len(intStr)) + intStr
984 if signpos != -1:
985 intStr = '-' + intStr[1:]
986 elif signpos != -1:
987 intStr = '-' + intStr
988 slice = intStr
989
990 slice = slice.strip() # drop extra spaces
991
992 if self._alignRight: ## Only if right-alignment is enabled
993 slice = slice.rjust( length )
994 else:
995 slice = slice.ljust( length )
996 dbg('adjusted slice: "%s"' % slice, indent=0)
997 return slice
998
999
1000 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
1001
1002 class wxMaskedEditMixin:
1003 """
1004 This class allows us to abstract the masked edit functionality that could
1005 be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.)
1006 """
1007 valid_ctrl_params = {
1008 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control
1009 'autoformat': "", ## optional auto-format code to set format from masktags dictionary
1010 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask
1011 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year
1012 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete
1013 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control
1014 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently,
1015 ## from field validation constraints
1016 'emptyBackgroundColor': "White",
1017 'validBackgroundColor': "White",
1018 'invalidBackgroundColor': "Yellow",
1019 'foregroundColor': "Black",
1020 'signedForegroundColor': "Red",
1021 'demo': False}
1022
1023
1024 def __init__(self, name = 'wxMaskedEdit', **kwargs):
1025 """
1026 This is the "constructor" for setting up the mixin variable parameters for the composite class.
1027 """
1028
1029 self.name = name
1030
1031 # set up flag for doing optional things to base control if possible
1032 if not hasattr(self, 'controlInitialized'):
1033 self.controlInitialized = False
1034
1035 # Set internal state var for keeping track of whether or not a character
1036 # action results in a modification of the control, since .SetValue()
1037 # doesn't modify the base control's internal state:
1038 self.modified = False
1039
1040 # Validate legitimate set of parameters:
1041 for key in kwargs.keys():
1042 if key not in wxMaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
1043 raise TypeError('%s: invalid parameter "%s"' % (name, key))
1044
1045 ## Set up dictionary that can be used by subclasses to override or add to default
1046 ## behavior for individual characters. Derived subclasses needing to change
1047 ## default behavior for keys can either redefine the default functions for the
1048 ## common keys or add functions for specific keys to this list. Each function
1049 ## added should take the key event as argument, and return False if the key
1050 ## requires no further processing.
1051 ##
1052 ## Initially populated with navigation and function control keys:
1053 self._keyhandlers = {
1054 # default navigation keys and handlers:
1055 WXK_BACK: self._OnErase,
1056 WXK_LEFT: self._OnArrow,
1057 WXK_RIGHT: self._OnArrow,
1058 WXK_UP: self._OnAutoCompleteField,
1059 WXK_DOWN: self._OnAutoCompleteField,
1060 WXK_TAB: self._OnChangeField,
1061 WXK_HOME: self._OnHome,
1062 WXK_END: self._OnEnd,
1063 WXK_RETURN: self._OnReturn,
1064 WXK_PRIOR: self._OnAutoCompleteField,
1065 WXK_NEXT: self._OnAutoCompleteField,
1066
1067 # default function control keys and handlers:
1068 WXK_DELETE: self._OnErase,
1069 WXK_CTRL_A: self._OnCtrl_A,
1070 WXK_CTRL_C: self._baseCtrlEventHandler,
1071 WXK_CTRL_S: self._OnCtrl_S,
1072 WXK_CTRL_V: self._OnCtrl_V,
1073 WXK_CTRL_X: self._OnCtrl_X,
1074 }
1075
1076 ## bind standard navigational and control keycodes to this instance,
1077 ## so that they can be augmented and/or changed in derived classes:
1078 self._nav = list(nav)
1079 self._control = list(control)
1080
1081 ## Dynamically evaluate and store string constants for mask chars
1082 ## so that locale settings can be made after this module is imported
1083 ## and the controls created after that is done can allow the
1084 ## appropriate characters:
1085 self.maskchardict = {
1086 "#": string.digits,
1087 "A": string.uppercase,
1088 "a": string.lowercase,
1089 "X": string.letters + string.punctuation + string.digits,
1090 "C": string.letters,
1091 "N": string.letters + string.digits
1092 }
1093
1094 ## self._ignoreChange is used by wxMaskedComboBox, because
1095 ## of the hack necessary to determine the selection; it causes
1096 ## EVT_TEXT messages from the combobox to be ignored if set.
1097 self._ignoreChange = False
1098 self._oldvalue = None
1099
1100 self._valid = True
1101
1102 # Set defaults for each parameter for this instance, and fully
1103 # populate initial parameter list for configuration:
1104 for key, value in wxMaskedEditMixin.valid_ctrl_params.items():
1105 setattr(self, '_' + key, copy.copy(value))
1106 if not kwargs.has_key(key):
1107 ## dbg('%s: "%s"' % (key, repr(value)))
1108 kwargs[key] = copy.copy(value)
1109
1110 # Create a "field" that holds global parameters for control constraints
1111 self._ctrl_constraints = self._fields[-1] = Field(index=-1)
1112 self.SetCtrlParameters(**kwargs)
1113
1114
1115
1116 def SetCtrlParameters(self, **kwargs):
1117 """
1118 This public function can be used to set individual or multiple masked edit
1119 parameters after construction.
1120 """
1121 dbg('wxMaskedEditMixin::SetCtrlParameters', indent=1)
1122 dbg('kwargs:', indent=1)
1123 for key, value in kwargs.items():
1124 dbg(key, '=', value)
1125 dbg(indent=0)
1126
1127 # Validate keyword arguments:
1128 constraint_kwargs = {}
1129 ctrl_kwargs = {}
1130 for key, value in kwargs.items():
1131 if key not in wxMaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
1132 raise TypeError('%s: invalid keyword argument "%s"' % (self.name, key))
1133 elif key in Field.valid_params.keys():
1134 constraint_kwargs[key] = value
1135 else:
1136 ctrl_kwargs[key] = value
1137
1138 mask = None
1139 reset_args = {}
1140
1141 if ctrl_kwargs.has_key('autoformat'):
1142 autoformat = ctrl_kwargs['autoformat']
1143 else:
1144 autoformat = None
1145
1146 if autoformat != self._autoformat and autoformat in masktags.keys():
1147 dbg('autoformat:', autoformat)
1148 self._autoformat = autoformat
1149 mask = masktags[self._autoformat][0]
1150 constraint_kwargs['excludeChars'] = masktags[self._autoformat][1]
1151 constraint_kwargs['formatcodes'] = masktags[self._autoformat][2]
1152 constraint_kwargs['validRegex'] = masktags[self._autoformat][3]
1153 constraint_kwargs['choices'] = masktags[self._autoformat][4]
1154 if masktags[self._autoformat][4]:
1155 constraint_kwargs['choiceRequired'] = masktags[self._autoformat][5]
1156
1157 else:
1158 dbg('autoformat not selected')
1159 if kwargs.has_key('mask'):
1160 mask = kwargs['mask']
1161 dbg('mask:', mask)
1162
1163 ## Assign style flags
1164 if mask is None:
1165 dbg('preserving previous mask')
1166 mask = self._previous_mask # preserve previous mask
1167 else:
1168 dbg('mask (re)set')
1169 reset_args['reset_mask'] = mask
1170 constraint_kwargs['mask'] = mask
1171
1172 # wipe out previous fields; preserve new control-level constraints
1173 self._fields = {-1: self._ctrl_constraints}
1174
1175
1176 if ctrl_kwargs.has_key('fields'):
1177 # do field parameter type validation, and conversion to internal dictionary
1178 # as appropriate:
1179 fields = ctrl_kwargs['fields']
1180 if type(fields) in (types.ListType, types.TupleType):
1181 for i in range(len(fields)):
1182 field = fields[i]
1183 if not isinstance(field, Field):
1184 dbg(indent=0)
1185 raise AttributeError('invalid type for field parameter: %s' % repr(field))
1186 self._fields[i] = field
1187
1188 elif type(fields) == types.DictionaryType:
1189 for index, field in fields.items():
1190 if not isinstance(field, Field):
1191 dbg(indent=0)
1192 raise AttributeError('invalid type for field parameter: %s' % repr(field))
1193 self._fields[index] = field
1194 else:
1195 dbg(indent=0)
1196 raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
1197
1198 # Assign constraint parameters for entire control:
1199 ## dbg('control constraints:', indent=1)
1200 ## for key, value in constraint_kwargs.items():
1201 ## dbg('%s:' % key, value)
1202 ## dbg(indent=0)
1203
1204 # determine if changing parameters that should affect the entire control:
1205 for key in wxMaskedEditMixin.valid_ctrl_params.keys():
1206 if key in ( 'mask', 'fields' ): continue # (processed separately)
1207 if ctrl_kwargs.has_key(key):
1208 setattr(self, '_' + key, ctrl_kwargs[key])
1209
1210
1211 dbg('self._retainFieldValidation:', self._retainFieldValidation)
1212 if not self._retainFieldValidation:
1213 # Build dictionary of any changing parameters which should be propagated to the
1214 # component fields:
1215 for arg in ('fillChar', 'groupChar', 'compareNoCase', 'defaultValue', 'validRequired'):
1216 dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg))
1217 dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg))
1218 reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg)
1219 dbg('reset_args[%s]?' % arg, reset_args[arg])
1220
1221 # Set the control-level constraints:
1222 self._ctrl_constraints._SetParameters(**constraint_kwargs)
1223
1224 # This routine does the bulk of the interdependent parameter processing, determining
1225 # the field extents of the mask if changed, resetting parameters as appropriate,
1226 # determining the overall template value for the control, etc.
1227 self._configure(mask, **reset_args)
1228
1229 self._autofit = self._ctrl_constraints._autofit
1230 self._isNeg = False
1231
1232 self._isDate = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask)
1233 self._isTime = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask)
1234 if self._isDate:
1235 # Set _dateExtent, used in date validation to locate date in string;
1236 # always set as though year will be 4 digits, even if mask only has
1237 # 2 digits, so we can always properly process the intended year for
1238 # date validation (leap years, etc.)
1239 if self._mask.find('CCC') != -1: self._dateExtent = 11
1240 else: self._dateExtent = 10
1241
1242 self._4digityear = len(self._mask) > 8 and self._mask[9] == '#'
1243
1244 if self._isDate and self._autoformat:
1245 # Auto-decide datestyle:
1246 if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY'
1247 elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD'
1248 elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD'
1249 elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY'
1250 elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY'
1251
1252
1253 if self.controlInitialized:
1254 # Then the base control is available for configuration;
1255 # take action on base control based on new settings, as appropriate.
1256 if kwargs.has_key('useFixedWidthFont'):
1257 # Set control font - fixed width by default
1258 self._setFont()
1259
1260 if reset_args.has_key('reset_mask') or not self._GetValue().strip():
1261 self._SetInitialValue()
1262
1263 if self._autofit:
1264 self.SetClientSize(self._calcSize())
1265
1266 # Set value/type-specific formatting
1267 self._applyFormatting()
1268 dbg(indent=0)
1269
1270 def SetMaskParameters(self, **kwargs):
1271 """ old name for this function """
1272 return self.SetCtrlParameters(**kwargs)
1273
1274
1275 def GetCtrlParameter(self, paramname):
1276 """
1277 Routine for retrieving the value of any given parameter
1278 """
1279 if wxMaskedEditMixin.valid_ctrl_params.has_key(paramname):
1280 return getattr(self, '_' + paramname)
1281 elif Field.valid_params.has_key(paramname):
1282 return self._ctrl_constraints._GetParameter(paramname)
1283 else:
1284 TypeError('%s.GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname))
1285
1286 def GetMaskParameter(self, paramname):
1287 """ old name for this function """
1288 return self.GetCtrlParameter(paramname)
1289
1290 def SetFieldParameters(self, field_index, **kwargs):
1291 """
1292 Routine provided to modify the parameters of a given field.
1293 Because changes to fields can affect the overall control,
1294 direct access to the fields is prevented, and the control
1295 is always "reconfigured" after setting a field parameter.
1296 """
1297 if field_index not in self._field_indices:
1298 raise IndexError('%s: %s is not a valid field for this control.' % (self.name, str(field_index)))
1299 # set parameters as requested:
1300 self._fields[field_index]._SetParameters(**kwargs)
1301
1302 # Possibly reprogram control template due to resulting changes, and ensure
1303 # control-level params are still propagated to fields:
1304 self._configure(self._previous_mask)
1305
1306 if self.controlInitialized:
1307 if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'):
1308 self._SetInitialValue()
1309
1310 if self._autofit:
1311 self.SetClientSize(self._calcSize())
1312
1313 # Set value/type-specific formatting
1314 self._applyFormatting()
1315
1316
1317 def GetFieldParameter(self, field_index, paramname):
1318 """
1319 Routine provided for getting a parameter of an individual field.
1320 """
1321 if field_index not in self._field_indices:
1322 raise IndexError('%s: %s is not a valid field for this control.' % (self.name, str(field_index)))
1323 elif Field.valid_params.has_key(paramname):
1324 return self._fields[field_index]._GetParameter(paramname)
1325 else:
1326 TypeError('%s.GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
1327
1328
1329 def _SetKeycodeHandler(self, keycode, func):
1330 """
1331 This function adds and/or replaces key event handling functions
1332 used by the control. <func> should take the event as argument
1333 and return False if no further action on the key is necessary.
1334 """
1335 self._keyhandlers[keycode] = func
1336
1337
1338 def _SetKeyHandler(self, char, func):
1339 """
1340 This function adds and/or replaces key event handling functions
1341 for ascii characters. <func> should take the event as argument
1342 and return False if no further action on the key is necessary.
1343 """
1344 self._SetKeycodeHandler(ord(char), func)
1345
1346
1347 def _AddNavKeycode(self, keycode, handler=None):
1348 """
1349 This function allows a derived subclass to augment the list of
1350 keycodes that are considered "navigational" keys.
1351 """
1352 self._nav.append(keycode)
1353 if handler:
1354 self._keyhandlers[keycode] = handler
1355
1356
1357 def _AddNavKey(self, char, handler=None):
1358 """
1359 This function is a convenience function so you don't have to
1360 remember to call ord() for ascii chars to be used for navigation.
1361 """
1362 self._AddNavKeycode(ord(char), handler)
1363
1364
1365 def _GetNavKeycodes(self):
1366 """
1367 This function retrieves the current list of navigational keycodes for
1368 the control.
1369 """
1370 return self._nav
1371
1372
1373 def _SetNavKeycodes(self, keycode_func_tuples):
1374 """
1375 This function allows you to replace the current list of keycode processed
1376 as navigation keys, and bind associated optional keyhandlers.
1377 """
1378 self._nav = []
1379 for keycode, func in keycode_func_tuples:
1380 self._nav.append(keycode)
1381 if func:
1382 self._keyhandlers[keycode] = func
1383
1384
1385 def _processMask(self, mask):
1386 """
1387 This subroutine expands {n} syntax in mask strings, and looks for escaped
1388 special characters and returns the expanded mask, and an dictionary
1389 of booleans indicating whether or not a given position in the mask is
1390 a mask character or not.
1391 """
1392 dbg('_processMask: mask', mask, indent=1)
1393 # regular expression for parsing c{n} syntax:
1394 rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}')
1395 s = mask
1396 match = rex.search(s)
1397 while match: # found an(other) occurrence
1398 maskchr = s[match.start(1):match.end(1)] # char to be repeated
1399 repcount = int(s[match.start(2):match.end(2)]) # the number of times
1400 replacement = string.join( maskchr * repcount, "") # the resulting substr
1401 s = s[:match.start(1)] + replacement + s[match.end(2)+1:] #account for trailing '}'
1402 match = rex.search(s) # look for another such entry in mask
1403
1404 self._decimalChar = self._ctrl_constraints._decimalChar
1405 self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar
1406
1407 self._isDec = isDecimal(s, self._decimalChar) and not self._ctrl_constraints._validRegex
1408 self._isInt = isInteger(s) and not self._ctrl_constraints._validRegex
1409 self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isDec or self._isInt)
1410 dbg('isDecimal(%s, %c)?' % (s, self._decimalChar), isDecimal(s, self._decimalChar),
1411 'ctrl regex:', self._ctrl_constraints._validRegex)
1412
1413 if self._signOk and s[0] != ' ':
1414 s = ' ' + s
1415 if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ':
1416 self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue
1417
1418 # Now, go build up a dictionary of booleans, indexed by position,
1419 # indicating whether or not a given position is masked or not
1420 ismasked = {}
1421 i = 0
1422 while i < len(s):
1423 if s[i] == '\\': # if escaped character:
1424 ismasked[i] = False # mark position as not a mask char
1425 if i+1 < len(s): # if another char follows...
1426 s = s[:i] + s[i+1:] # elide the '\'
1427 if i+2 < len(s) and s[i+1] == '\\':
1428 # if next char also a '\', char is a literal '\'
1429 s = s[:i] + s[i+1:] # elide the 2nd '\' as well
1430 else: # else if special char, mark position accordingly
1431 ismasked[i] = s[i] in maskchars
1432 ## dbg('ismasked[%d]:' % i, ismasked[i], s)
1433 i += 1 # increment to next char
1434 ## dbg('ismasked:', ismasked)
1435 dbg(indent=0)
1436 return s, ismasked
1437
1438
1439 def _calcFieldExtents(self):
1440 """
1441 Subroutine responsible for establishing/configuring field instances with
1442 indices and editable extents appropriate to the specified mask, and building
1443 the lookup table mapping each position to the corresponding field.
1444 """
1445 if self._mask:
1446
1447 ## Create dictionary of positions,characters in mask
1448 self.maskdict = {}
1449 for charnum in range( len( self._mask)):
1450 self.maskdict[charnum] = self._mask[charnum:charnum+1]
1451
1452 # For the current mask, create an ordered list of field extents
1453 # and a dictionary of positions that map to field indices:
1454 self._lookupField = {}
1455
1456 if self._signOk: start = 1
1457 else: start = 0
1458
1459 if self._isDec:
1460 # Skip field "discovery", and just construct a 2-field control with appropriate
1461 # constraints for a floating-point entry.
1462
1463 # .setdefault always constructs 2nd argument even if not needed, so we do this
1464 # the old-fashioned way...
1465 if not self._fields.has_key(0):
1466 self._fields[0] = Field()
1467 if not self._fields.has_key(1):
1468 self._fields[1] = Field()
1469
1470 self._decimalpos = string.find( self._mask, self._decimalChar)
1471 dbg('decimal pos =', self._decimalpos)
1472
1473 formatcodes = self._fields[0]._GetParameter('formatcodes')
1474 if 'R' not in formatcodes: formatcodes += 'R'
1475 self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos),
1476 mask=self._mask[start:self._decimalpos], formatcodes=formatcodes)
1477
1478 self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, len(self._mask)),
1479 mask=self._mask[self._decimalpos+1:len(self._mask)])
1480
1481 for i in range(self._decimalpos+1):
1482 self._lookupField[i] = 0
1483
1484 for i in range(self._decimalpos+1, len(self._mask)+1):
1485 self._lookupField[i] = 1
1486
1487 elif self._isInt:
1488 # Skip field "discovery", and just construct a 1-field control with appropriate
1489 # constraints for a integer entry.
1490 if not self._fields.has_key(0):
1491 self._fields[0] = Field(index=0)
1492 self._fields[0]._SetParameters(extent=(start, len(self._mask)),
1493 mask=self._mask[start:len(self._mask)])
1494
1495 for i in range(len(self._mask)+1):
1496 self._lookupField[i] = 0
1497 else:
1498 # generic control; parse mask to figure out where the fields are:
1499 field_index = 0
1500 pos = 0
1501 i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point:
1502 if i < len(self._mask): # no editable chars!
1503 for j in range(pos, i+1):
1504 self._lookupField[j] = field_index
1505 pos = i # figure out field for 1st editable space:
1506
1507 while i <= len(self._mask):
1508 ## dbg('searching: outer field loop: i = ', i)
1509 if self._isMaskChar(i):
1510 ## dbg('1st char is mask char; recording edit_start=', i)
1511 edit_start = i
1512 # Skip to end of editable part of current field:
1513 while i < len(self._mask) and self._isMaskChar(i):
1514 self._lookupField[i] = field_index
1515 i += 1
1516 ## dbg('edit_end =', i)
1517 edit_end = i
1518 self._lookupField[i] = field_index
1519 ## dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index))
1520 if not self._fields.has_key(field_index):
1521 self._fields[field_index] = Field()
1522 self._fields[field_index]._SetParameters(
1523 index=field_index,
1524 extent=(edit_start, edit_end),
1525 mask=self._mask[edit_start:edit_end])
1526 pos = i
1527 i = self._findNextEntry(pos, adjustInsert=False) # go to next field:
1528 if i > pos:
1529 for j in range(pos, i+1):
1530 self._lookupField[j] = field_index
1531 if i >= len(self._mask):
1532 break # if past end, we're done
1533 else:
1534 field_index += 1
1535 ## dbg('next field:', field_index)
1536
1537 indices = self._fields.keys()
1538 indices.sort()
1539 self._field_indices = indices[1:]
1540 ## dbg('lookupField map:', indent=1)
1541 ## for i in range(len(self._mask)):
1542 ## dbg('pos %d:' % i, self._lookupField[i])
1543 ## dbg(indent=0)
1544
1545 # Verify that all field indices specified are valid for mask:
1546 for index in self._fields.keys():
1547 if index not in [-1] + self._lookupField.values():
1548 dbg(indent=0)
1549 raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
1550
1551
1552 def _calcTemplate(self, reset_fillchar, reset_default):
1553 """
1554 Subroutine for processing current fillchars and default values for
1555 whole control and individual fields, constructing the resulting
1556 overall template, and adjusting the current value as necessary.
1557 """
1558 default_set = False
1559 if self._ctrl_constraints._defaultValue:
1560 default_set = True
1561 else:
1562 for field in self._fields.values():
1563 if field._defaultValue and not reset_default:
1564 default_set = True
1565 dbg('default set?', default_set)
1566
1567 # Determine overall new template for control, and keep track of previous
1568 # values, so that current control value can be modified as appropriate:
1569 if self.controlInitialized: curvalue = list(self._GetValue())
1570 else: curvalue = None
1571
1572 if hasattr(self, '_fillChar'): old_fillchars = self._fillChar
1573 else: old_fillchars = None
1574
1575 if hasattr(self, '_template'): old_template = self._template
1576 else: old_template = None
1577
1578 self._template = ""
1579
1580 self._fillChar = {}
1581 reset_value = False
1582
1583 for field in self._fields.values():
1584 field._template = ""
1585
1586 for pos in range(len(self._mask)):
1587 ## dbg('pos:', pos)
1588 field = self._FindField(pos)
1589 ## dbg('field:', field._index)
1590 start, end = field._extent
1591
1592 if pos == 0 and self._signOk:
1593 self._template = ' ' # always make 1st 1st position blank, regardless of fillchar
1594 elif self._isMaskChar(pos):
1595 if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar:
1596 fillChar = field._fillChar
1597 else:
1598 fillChar = self._ctrl_constraints._fillChar
1599 self._fillChar[pos] = fillChar
1600
1601 # Replace any current old fillchar with new one in current value;
1602 # if action required, set reset_value flag so we can take that action
1603 # after we're all done
1604 if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue:
1605 if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar:
1606 reset_value = True
1607 curvalue[pos] = fillChar
1608
1609 if not field._defaultValue and not self._ctrl_constraints._defaultValue:
1610 ## dbg('no default value')
1611 self._template += fillChar
1612 field._template += fillChar
1613
1614 elif field._defaultValue and not reset_default:
1615 ## dbg('len(field._defaultValue):', len(field._defaultValue))
1616 ## dbg('pos-start:', pos-start)
1617 if len(field._defaultValue) > pos-start:
1618 ## dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start])
1619 self._template += field._defaultValue[pos-start]
1620 field._template += field._defaultValue[pos-start]
1621 else:
1622 ## dbg('field default not long enough; using fillChar')
1623 self._template += fillChar
1624 field._template += fillChar
1625 else:
1626 if len(self._ctrl_constraints._defaultValue) > pos:
1627 ## dbg('using control default')
1628 self._template += self._ctrl_constraints._defaultValue[pos]
1629 field._template += self._ctrl_constraints._defaultValue[pos]
1630 else:
1631 ## dbg('ctrl default not long enough; using fillChar')
1632 self._template += fillChar
1633 field._template += fillChar
1634 ## dbg('field[%d]._template now "%s"' % (field._index, field._template))
1635 ## dbg('self._template now "%s"' % self._template)
1636 else:
1637 self._template += self._mask[pos]
1638
1639 self._fields[-1]._template = self._template # (for consistency)
1640
1641 if curvalue: # had an old value, put new one back together
1642 newvalue = string.join(curvalue, "")
1643 else:
1644 newvalue = None
1645
1646 if default_set:
1647 self._defaultValue = self._template
1648 dbg('self._defaultValue:', self._defaultValue)
1649 if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
1650 dbg(indent=0)
1651 raise ValueError('%s: default value of "%s" is not a valid value' % (self.name, self._defaultValue))
1652
1653 # if no fillchar change, but old value == old template, replace it:
1654 if newvalue == old_template:
1655 newvalue = self._template
1656 reset_value = true
1657 else:
1658 self._defaultValue = None
1659
1660 if reset_value:
1661 pos = self._GetInsertionPoint()
1662 sel_start, sel_to = self._GetSelection()
1663 self._SetValue(newvalue)
1664 self._SetInsertionPoint(pos)
1665 self._SetSelection(sel_start, sel_to)
1666
1667
1668 def _propagateConstraints(self, **reset_args):
1669 """
1670 Subroutine for propagating changes to control-level constraints and
1671 formatting to the individual fields as appropriate.
1672 """
1673 parent_codes = self._ctrl_constraints._formatcodes
1674 for i in self._field_indices:
1675 field = self._fields[i]
1676 inherit_args = {}
1677
1678 field_codes = current_codes = field._GetParameter('formatcodes')
1679 for c in parent_codes:
1680 if c not in field_codes: field_codes += c
1681 if field_codes != current_codes:
1682 inherit_args['formatcodes'] = field_codes
1683
1684 include_chars = current_includes = field._GetParameter('includeChars')
1685 for c in include_chars:
1686 if not c in include_chars: include_chars += c
1687 if include_chars != current_includes:
1688 inherit_args['includeChars'] = include_chars
1689
1690 exclude_chars = current_excludes = field._GetParameter('excludeChars')
1691 for c in exclude_chars:
1692 if not c in exclude_chars: exclude_chars += c
1693 if exclude_chars != current_excludes:
1694 inherit_args['excludeChars'] = exclude_chars
1695
1696 if reset_args.has_key('defaultValue') and reset_args['defaultValue']:
1697 inherit_args['defaultValue'] = "" # (reset for field)
1698
1699 for param in ['fillChar', 'groupChar', 'compareNoCase', 'emptyInvalid', 'validRequired']:
1700 if reset_args.has_key(param) and reset_args[param]:
1701 inherit_args[param] = self.GetCtrlParameter(param)
1702
1703 if inherit_args:
1704 field._SetParameters(**inherit_args)
1705
1706
1707 def _validateChoices(self):
1708 """
1709 Subroutine that validates that all choices for given fields are at
1710 least of the necessary length, and that they all would be valid pastes
1711 if pasted into their respective fields.
1712 """
1713 for field in self._fields.values():
1714 if field._choices:
1715 index = field._index
1716 ## dbg('checking for choices for field', field._index)
1717 start, end = field._extent
1718 field_length = end - start
1719 ## dbg('start, end, length:', start, end, field_length)
1720
1721 for choice in field._choices:
1722 valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
1723 if not valid_paste:
1724 dbg(indent=0)
1725 raise ValueError('%s: "%s" could not be entered into field %d' % (self.name, choice, index))
1726 elif replace_to > end:
1727 dbg(indent=0)
1728 raise ValueError('%s: "%s" will not fit into field %d' (self.name, choice, index))
1729 ## dbg(choice, 'valid in field', index)
1730
1731
1732 def _configure(self, mask, **reset_args):
1733 """
1734 This function sets flags for automatic styling options. It is
1735 called whenever a control or field-level parameter is set/changed.
1736
1737 This routine does the bulk of the interdependent parameter processing, determining
1738 the field extents of the mask if changed, resetting parameters as appropriate,
1739 determining the overall template value for the control, etc.
1740
1741 reset_args is supplied if called from control's .SetCtrlParameters()
1742 routine, and indicates which if any parameters which can be
1743 overridden by individual fields have been reset by request for the
1744 whole control.
1745 """
1746 dbg('wxMaskedEditMixin::_configure("%s")' % mask, indent=1)
1747
1748 # Preprocess specified mask to expand {n} syntax, handle escaped
1749 # mask characters, etc and build the resulting positionally keyed
1750 # dictionary for which positions are mask vs. template characters:
1751 self._mask, self.ismasked = self._processMask(mask)
1752 dbg('processed mask:', self._mask)
1753
1754 # Preserve original mask specified, for subsequent reprocessing
1755 # if parameters change.
1756 self._previous_mask = mask
1757
1758
1759 # Set extent of field -1 to width of entire control:
1760 self._ctrl_constraints._SetParameters(extent=(0,len(self._mask)))
1761
1762 # Go parse mask to determine where each field is, construct field
1763 # instances as necessary, configure them with those extents, and
1764 # build lookup table mapping each position for control to its corresponding
1765 # field.
1766 self._calcFieldExtents()
1767
1768 # Go process defaultValues and fillchars to construct the overall
1769 # template, and adjust the current value as necessary:
1770 reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar']
1771 reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue']
1772
1773 self._calcTemplate(reset_fillchar, reset_default)
1774
1775 # Propagate control-level formatting and character constraints to each
1776 # field if they don't already have them:
1777 self._propagateConstraints(**reset_args)
1778
1779
1780
1781 if self._isDec and self._fields[0]._groupChar == self._decimalChar:
1782 raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' %
1783 (self._fields[0]._groupChar, self._decimalChar) )
1784
1785 # Validate that all choices for given fields are at least of the
1786 # necessary length, and that they all would be valid pastes if pasted
1787 # into their respective fields:
1788 self._validateChoices()
1789
1790 dbg('fields:', indent=1)
1791 for i in [-1] + self._field_indices:
1792 dbg('field %d:' % i, self._fields[i].__dict__)
1793 dbg(indent=0)
1794
1795 # Set up special parameters for numeric control, if appropriate:
1796 if self._signOk:
1797 self._signpos = 0 # assume it starts here, but it will move around on floats
1798 self._SetKeyHandler('-', self._OnChangeSign)
1799 self._SetKeyHandler('+', self._OnChangeSign)
1800 self._SetKeyHandler(' ', self._OnChangeSign)
1801
1802 if self._isDec or self._isInt:
1803 dbg('Registering numeric navigation and control handlers')
1804
1805 # Replace up/down arrow default handling:
1806 # make down act like tab, up act like shift-tab:
1807 self._SetKeycodeHandler(WXK_DOWN, self._OnChangeField)
1808 self._SetKeycodeHandler(WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField)
1809
1810 # On ., truncate contents right of cursor to decimal point (if any)
1811 # leaves cusor after decimal point if dec, otherwise at 0.
1812 self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint)
1813 self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards)
1814
1815 # Allow selective insert of groupchar in numbers:
1816 self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar)
1817
1818 dbg(indent=0)
1819
1820
1821 def _SetInitialValue(self, value=""):
1822 """
1823 fills the control with the generated or supplied default value.
1824 It will also set/reset the font if necessary and apply
1825 formatting to the control at this time.
1826 """
1827 dbg('wxMaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
1828 if not value:
1829 self._SetValue( self._template )
1830 else:
1831 # Apply validation as appropriate to passed value
1832 self.SetValue(value)
1833
1834 # Set value/type-specific formatting
1835 self._applyFormatting()
1836 dbg(indent=0)
1837
1838
1839 def _calcSize(self, size=None):
1840 """ Calculate automatic size if allowed; must be called after the base control is instantiated"""
1841 ## dbg('wxMaskedEditMixin::_calcSize', indent=1)
1842 cont = (size is None or size == wxDefaultSize)
1843
1844 if cont and self._autofit:
1845 sizing_text = 'M' * len(self._mask)
1846 if wxPlatform != "__WXMSW__": # give it a little extra space
1847 sizing_text += 'M'
1848 if wxPlatform == "__WXMAC__": # give it even a little more...
1849 sizing_text += 'M'
1850 ## dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
1851 w, h = self.GetTextExtent(sizing_text)
1852 size = (w+4, self.GetClientSize().height)
1853 ## dbg('size:', size, indent=0)
1854 return size
1855
1856
1857 def _setFont(self):
1858 """ Set the control's font typeface -- pass the font name as str."""
1859 ## dbg('wxMaskedEditMixin::_setFont', indent=1)
1860 if not self._useFixedWidthFont:
1861 self._font = wxSystemSettings_GetFont(wxSYS_DEFAULT_GUI_FONT)
1862 else:
1863 font = self.GetFont() # get size, weight, etc from current font
1864
1865 # Set to teletype font (guaranteed to be mappable to all wxWindows
1866 # platforms:
1867 self._font = wxFont( font.GetPointSize(), wxTELETYPE, font.GetStyle(),
1868 font.GetWeight(), font.GetUnderlined())
1869 ## dbg('font string: "%s"' % font.GetNativeFontInfo().ToString())
1870
1871 self.SetFont(self._font)
1872 ## dbg(indent=0)
1873
1874
1875 def _OnTextChange(self, event):
1876 """
1877 Handler for EVT_TEXT event.
1878 self._Change() is provided for subclasses, and may return False to
1879 skip this method logic. This function returns True if the event
1880 detected was a legitimate event, or False if it was a "bogus"
1881 EVT_TEXT event. (NOTE: There is currently an issue with calling
1882 .SetValue from within the EVT_CHAR handler that causes duplicate
1883 EVT_TEXT events for the same change.)
1884 """
1885 newvalue = self._GetValue()
1886 dbg('wxMaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1)
1887 bValid = False
1888 if self._ignoreChange: # ie. if an "intermediate text change event"
1889 dbg(indent=0)
1890 return bValid
1891
1892 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue
1893 ## call is generating two (2) EVT_TEXT events.
1894 ## This is the only mechanism I can find to mask this problem:
1895 if newvalue == self._oldvalue:
1896 dbg('ignoring bogus text change event', indent=0)
1897 else:
1898 dbg('oldvalue: "%s", newvalue: "%s"' % (self._oldvalue, newvalue))
1899 if self._Change():
1900 if self._signOk and self._isNeg and newvalue.find('-') == -1:
1901 self._isNeg = False
1902 text, self._signpos = self._getSignedValue()
1903 self._CheckValid() # Recolor control as appropriate
1904 event.Skip()
1905 bValid = True
1906 self._oldvalue = newvalue # Save last seen value for next iteration
1907 dbg(indent=0)
1908 return bValid
1909
1910
1911 def _OnKeyDown(self, event):
1912 """
1913 This function allows the control to capture Ctrl-events like Ctrl-tab,
1914 that are not normally seen by the "cooked" EVT_CHAR routine.
1915 """
1916 # Get keypress value, adjusted by control options (e.g. convert to upper etc)
1917 key = event.GetKeyCode()
1918 if key in self._nav and event.ControlDown():
1919 # then this is the only place we will likely see these events;
1920 # process them now:
1921 dbg('wxMaskedEditMixin::OnKeyDown: calling _OnChar')
1922 self._OnChar(event)
1923 return
1924 # else allow regular EVT_CHAR key processing
1925 event.Skip()
1926
1927
1928 def _OnChar(self, event):
1929 """
1930 This is the engine of wxMaskedEdit controls. It examines each keystroke,
1931 decides if it's allowed, where it should go or what action to take.
1932 """
1933 dbg('wxMaskedEditMixin::_OnChar', indent=1)
1934
1935 # Get keypress value, adjusted by control options (e.g. convert to upper etc)
1936 key = event.GetKeyCode()
1937 orig_pos = self._GetInsertionPoint()
1938 orig_value = self._GetValue()
1939 dbg('keycode = ', key)
1940 dbg('current pos = ', orig_pos)
1941 dbg('current selection = ', self._GetSelection())
1942
1943 if not self._Keypress(key):
1944 dbg(indent=0)
1945 return
1946
1947 # If no format string for this control, or the control is marked as "read-only",
1948 # skip the rest of the special processing, and just "do the standard thing:"
1949 if not self._mask or not self._IsEditable():
1950 event.Skip()
1951 dbg(indent=0)
1952 return
1953
1954 # Process navigation and control keys first, with
1955 # position/selection unadulterated:
1956 if key in self._nav + self._control:
1957 if self._keyhandlers.has_key(key):
1958 keep_processing = self._keyhandlers[key](event)
1959 if self._GetValue() != orig_value:
1960 self.modified = True
1961 if not keep_processing:
1962 dbg(indent=0)
1963 return
1964 self._applyFormatting()
1965 dbg(indent=0)
1966 return
1967
1968 # Else... adjust the position as necessary for next input key,
1969 # and determine resulting selection:
1970 pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed
1971 sel_start, sel_to = self._GetSelection() ## check for a range of selected text
1972 dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to)
1973
1974 keep_processing = True
1975 # Capture user past end of format field
1976 if pos > len(self.maskdict):
1977 dbg("field length exceeded:",pos)
1978 keep_processing = False
1979
1980 if keep_processing:
1981 if self._isMaskChar(pos): ## Get string of allowed characters for validation
1982 okchars = self._getAllowedChars(pos)
1983 else:
1984 dbg('Not a valid position: pos = ', pos,"chars=",maskchars)
1985 okchars = ""
1986
1987 key = self._adjustKey(pos, key) # apply formatting constraints to key:
1988
1989 if self._keyhandlers.has_key(key):
1990 # there's an override for default behavior; use override function instead
1991 dbg('using supplied key handler:', self._keyhandlers[key])
1992 keep_processing = self._keyhandlers[key](event)
1993 if self._GetValue() != orig_value:
1994 self.modified = True
1995 if not keep_processing:
1996 dbg(indent=0)
1997 return
1998 # else skip default processing, but do final formatting
1999 if key < WXK_SPACE or key > 255:
2000 dbg('key < WXK_SPACE or key > 255')
2001 event.Skip() # non alphanumeric
2002 keep_processing = False
2003 else:
2004 field = self._FindField(pos)
2005 dbg("key ='%s'" % chr(key))
2006 if chr(key) == ' ':
2007 dbg('okSpaces?', field._okSpaces)
2008
2009 if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars:
2010 keep_processing = False
2011 if (not wxValidator_IsSilent()) and orig_pos == pos:
2012 wxBell()
2013
2014 if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ):
2015 dbg("key allowed by mask")
2016 # insert key into candidate new value, but don't change control yet:
2017 oldstr = self._GetValue()
2018 newstr, newpos = self._insertKey(chr(key), pos, sel_start, sel_to, self._GetValue())
2019 dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr)
2020 if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
2021 dbg('not valid; checking to see if adjusted string is:')
2022 keep_processing = False
2023 if self._isDec and newstr != self._template:
2024 newstr = self._adjustDec(newstr)
2025 dbg('adjusted str:', newstr)
2026 if self.IsValid(newstr):
2027 dbg("it is!")
2028 keep_processing = True
2029 wxCallAfter(self._SetInsertionPoint, self._decimalpos)
2030 if not keep_processing:
2031 dbg("key disallowed by validation")
2032 if not wxValidator_IsSilent() and orig_pos == pos:
2033 wxBell()
2034
2035 if keep_processing:
2036 unadjusted = newstr
2037
2038 # special case: adjust date value as necessary:
2039 if self._isDate and newstr != self._template:
2040 newstr = self._adjustDate(newstr)
2041 dbg('adjusted newstr:', newstr)
2042
2043 if newstr != orig_value:
2044 self.modified = True
2045
2046 wxCallAfter(self._SetValue, newstr)
2047
2048 # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits:
2049 if not self.IsDefault() and self._isDate and self._4digityear:
2050 year2dig = self._dateExtent - 2
2051 if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]:
2052 newpos = pos+2
2053
2054 wxCallAfter(self._SetInsertionPoint, newpos)
2055 newfield = self._FindField(newpos)
2056 if newfield != field and newfield._selectOnFieldEntry:
2057 wxCallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
2058 keep_processing = false
2059 else:
2060 dbg('char not allowed; orig_pos == pos?', orig_pos == pos)
2061 keep_processing = False
2062 if (not wxValidator_IsSilent()) and orig_pos == pos:
2063 wxBell()
2064
2065 self._applyFormatting()
2066
2067 # Move to next insertion point
2068 if keep_processing and key not in self._nav:
2069 pos = self._GetInsertionPoint()
2070 next_entry = self._findNextEntry( pos )
2071 if pos != next_entry:
2072 dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals())
2073 wxCallAfter(self._SetInsertionPoint, next_entry )
2074
2075 if self._isTemplateChar(pos):
2076 self._AdjustField(pos)
2077 dbg(indent=0)
2078
2079
2080 def _FindFieldExtent(self, pos=None, getslice=False, value=None):
2081 """ returns editable extent of field corresponding to
2082 position pos, and, optionally, the contents of that field
2083 in the control or the value specified.
2084 Template chars are bound to the preceding field.
2085 For masks beginning with template chars, these chars are ignored
2086 when calculating the current field.
2087
2088 Eg: with template (###) ###-####,
2089 >>> self._FindFieldExtent(pos=0)
2090 1, 4
2091 >>> self._FindFieldExtent(pos=1)
2092 1, 4
2093 >>> self._FindFieldExtent(pos=5)
2094 1, 4
2095 >>> self._FindFieldExtent(pos=6)
2096 6, 9
2097 >>> self._FindFieldExtent(pos=10)
2098 10, 14
2099 etc.
2100 """
2101 dbg('wxMaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (
2102 str(pos), str(getslice)) ,indent=1)
2103
2104 field = self._FindField(pos)
2105 if not field:
2106 if getslice:
2107 return None, None, ""
2108 else:
2109 return None, None
2110 edit_start, edit_end = field._extent
2111 if getslice:
2112 if value is None: value = self._GetValue()
2113 slice = value[edit_start:edit_end]
2114 dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice)
2115 dbg(indent=0)
2116 return edit_start, edit_end, slice
2117 else:
2118 dbg('edit_start:', edit_start, 'edit_end:', edit_end)
2119 dbg(indent=0)
2120 return edit_start, edit_end
2121
2122
2123 def _FindField(self, pos=None):
2124 """
2125 Returns the field instance in which pos resides.
2126 Template chars are bound to the preceding field.
2127 For masks beginning with template chars, these chars are ignored
2128 when calculating the current field.
2129
2130 """
2131 ## dbg('wxMaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
2132 if pos is None: pos = self._GetInsertionPoint()
2133 elif pos < 0 or pos > len(self._mask):
2134 raise IndexError('position %s out of range of control' % str(pos))
2135
2136 if len(self._fields) == 0:
2137 dbg(indent=0)
2138 return None
2139
2140 # else...
2141 ## dbg(indent=0)
2142 return self._fields[self._lookupField[pos]]
2143
2144
2145 def ClearValue(self):
2146 """ Blanks the current control value by replacing it with the default value."""
2147 dbg("wxMaskedEditMixin::ClearValue - value reset to default value (template)")
2148 self._SetValue( self._template )
2149 self._SetInsertionPoint(0)
2150 self.Refresh()
2151
2152
2153 def _baseCtrlEventHandler(self, event):
2154 """
2155 This function is used whenever a key should be handled by the base control.
2156 """
2157 event.Skip()
2158 return False
2159
2160
2161 def _OnUpNumeric(self, event):
2162 """
2163 Makes up-arrow act like shift-tab should; ie. take you to start of
2164 previous field.
2165 """
2166 dbg('wxMaskedEditMixin::_OnUpNumeric', indent=1)
2167 event.m_shiftDown = 1
2168 dbg('event.ShiftDown()?', event.ShiftDown())
2169 self._OnChangeField(event)
2170 dbg(indent=0)
2171
2172
2173 def _OnArrow(self, event):
2174 """
2175 Used in response to left/right navigation keys; makes these actions skip
2176 over mask template chars.
2177 """
2178 dbg("wxMaskedEditMixin::_OnArrow", indent=1)
2179 pos = self._GetInsertionPoint()
2180 keycode = event.GetKeyCode()
2181 sel_start, sel_to = self._GetSelection()
2182 entry_end = self._goEnd(getPosOnly=True)
2183 if keycode in (WXK_RIGHT, WXK_DOWN):
2184 if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
2185 or ( self._isTemplateChar(pos) and pos >= entry_end) ):
2186 dbg(indent=0)
2187 return False
2188 elif self._isTemplateChar(pos):
2189 self._AdjustField(pos)
2190 elif keycode in (WXK_LEFT,WXK_UP) and pos > 0 and self._isTemplateChar(pos-1):
2191 dbg('adjusting field')
2192 self._AdjustField(pos)
2193
2194 # treat as shifted up/down arrows as tab/reverse tab:
2195 if event.ShiftDown() and keycode in (WXK_UP, WXK_DOWN):
2196 # remove "shifting" and treat as (forward) tab:
2197 event.m_shiftDown = False
2198 keep_processing = self._OnChangeField(event)
2199
2200 elif self._FindField(pos)._selectOnFieldEntry:
2201 if( keycode in (WXK_UP, WXK_LEFT)
2202 and sel_start != 0
2203 and self._isTemplateChar(sel_start-1)):
2204
2205 # call _OnChangeField to handle "ctrl-shifted event"
2206 # (which moves to previous field and selects it.)
2207 event.m_shiftDown = True
2208 event.m_ControlDown = True
2209 keep_processing = self._OnChangeField(event)
2210 elif( keycode in (WXK_DOWN, WXK_RIGHT)
2211 and sel_to != len(self._mask)
2212 and self._isTemplateChar(sel_to)):
2213
2214 # when changing field to the right, ensure don't accidentally go left instead
2215 event.m_shiftDown = False
2216 keep_processing = self._OnChangeField(event)
2217 else:
2218 # treat arrows as normal, allowing selection
2219 # as appropriate:
2220 event.Skip()
2221 else:
2222 if( (sel_to == self._fields[0]._extent[0] and keycode == WXK_LEFT)
2223 or (sel_to == len(self._mask) and keycode == WXK_RIGHT) ):
2224 if not wxValidator_IsSilent():
2225 wxBell()
2226 else:
2227 # treat arrows as normal, allowing selection
2228 # as appropriate:
2229 dbg('skipping event')
2230 event.Skip()
2231
2232 keep_processing = False
2233 dbg(indent=0)
2234 return keep_processing
2235
2236
2237 def _OnCtrl_S(self, event):
2238 """ Default Ctrl-S handler; prints value information if demo enabled. """
2239 dbg("wxMaskedEditMixin::_OnCtrl_S")
2240 if self._demo:
2241 print 'wxMaskedEditMixin.GetValue() = "%s"\nwxMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue())
2242 print "Valid? => " + str(self.IsValid())
2243 print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True))
2244 return False
2245
2246
2247 def _OnCtrl_X(self, event):
2248 """ Handles ctrl-x keypress in control. Should return False to skip other processing. """
2249 dbg("wxMaskedEditMixin::_OnCtrl_X", indent=1)
2250 self._Cut()
2251 dbg(indent=0)
2252 return False
2253
2254
2255 def _OnCtrl_V(self, event):
2256 """ Handles ctrl-V keypress in control. Should return False to skip other processing. """
2257 dbg("wxMaskedEditMixin::_OnCtrl_V", indent=1)
2258 self._Paste()
2259 dbg(indent=0)
2260 return False
2261
2262
2263 def _OnCtrl_A(self,event):
2264 """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
2265 end = self._goEnd(getPosOnly=True)
2266 if event.ShiftDown():
2267 wxCallAfter(self._SetInsertionPoint, 0)
2268 wxCallAfter(self._SetSelection, 0, len(self._mask))
2269 else:
2270 wxCallAfter(self._SetInsertionPoint, 0)
2271 wxCallAfter(self._SetSelection, 0, end)
2272 return False
2273
2274
2275 def _OnErase(self,event):
2276 """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
2277 dbg("wxMaskedEditMixin::_OnErase", indent=1)
2278 sel_start, sel_to = self._GetSelection() ## check for a range of selected text
2279 key = event.GetKeyCode()
2280 if( ((sel_to == 0 or sel_to == self._fields[0]._extent[0]) and key == WXK_BACK)
2281 or (sel_to == len(self._mask) and key == WXK_DELETE)):
2282 if not wxValidator_IsSilent():
2283 wxBell()
2284 dbg(indent=0)
2285 return False
2286
2287 field = self._FindField(sel_to)
2288 start, end = field._extent
2289 value = self._GetValue()
2290
2291 if( field._insertRight
2292 and key == WXK_BACK
2293 and sel_start >= start and sel_to == end # within field
2294 and value[start:end] != self._template[start:end]): # and field not empty
2295 dbg('delete left')
2296 dbg('sel_start, start:', sel_start, start)
2297 # special case: backspace at the end of a right insert field shifts contents right to cursor
2298
2299 if sel_start == end: # select "last char in field"
2300 sel_start -= 1
2301
2302 newfield = value[start:sel_start]
2303 dbg('cut newfield: "%s"' % newfield)
2304 left = ""
2305 for i in range(start, end - len(newfield)):
2306 if field._padZero:
2307 left += '0'
2308 elif self._signOk and self._isNeg and newfield.find('-') == -1 and i == 1:
2309 left += '-'
2310 else:
2311 left += self._template[i] # this can produce strange results in combination with default values...
2312 newfield = left + newfield
2313 dbg('filled newfield: "%s"' % newfield)
2314 newstr = value[:start] + newfield + value[end:]
2315 if self._signOk and self._isNeg and newstr[0] == '-':
2316 newstr = ' ' + newstr[1:]
2317 pos = end
2318 else:
2319 if sel_start == sel_to:
2320 dbg("current sel_start, sel_to:", sel_start, sel_to)
2321 if key == WXK_BACK:
2322 sel_start, sel_to = sel_to-1, sel_to-1
2323 dbg("new sel_start, sel_to:", sel_start, sel_to)
2324 if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''):
2325 # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0:
2326 newchar = '0'
2327 else:
2328 newchar = self._template[sel_to] ## get an original template character to "clear" the current char
2329 dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start]))
2330
2331 if self._isTemplateChar(sel_to):
2332 newstr = value
2333 newpos = sel_to
2334 else:
2335 newstr, newpos = self._insertKey(newchar, sel_to, sel_start, sel_to, value)
2336
2337 else:
2338 newstr = value
2339 newpos = sel_start
2340 for i in range(sel_start, sel_to):
2341 pos = i
2342 newchar = self._template[pos] ## get an original template character to "clear" the current char
2343
2344 if not self._isTemplateChar(pos):
2345 newstr, newpos = self._insertKey(newchar, pos, sel_start, sel_to, newstr)
2346
2347 pos = sel_start # put cursor back at beginning of selection
2348 dbg('newstr:', newstr)
2349
2350 # if erasure results in an invalid field, disallow it:
2351 dbg('field._validRequired?', field._validRequired)
2352 dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end]))
2353 if field._validRequired and not field.IsValid(newstr[start:end]):
2354 if not wxValidator_IsSilent():
2355 wxBell()
2356 dbg(indent=0)
2357 return False
2358
2359 # if erasure results in an invalid value, disallow it:
2360 if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
2361 if not wxValidator_IsSilent():
2362 wxBell()
2363 dbg(indent=0)
2364 return False
2365
2366 dbg('setting value (later) to', newstr)
2367 wxCallAfter(self._SetValue, newstr)
2368 dbg('setting insertion point (later) to', pos)
2369 wxCallAfter(self._SetInsertionPoint, pos)
2370 dbg(indent=0)
2371 return False
2372
2373
2374 def _OnEnd(self,event):
2375 """ Handles End keypress in control. Should return False to skip other processing. """
2376 dbg("wxMaskedEditMixin::_OnEnd", indent=1)
2377 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
2378 end = self._goEnd(getPosOnly=True)
2379
2380 if event.ShiftDown():
2381 dbg("shift-end; select to end of non-whitespace")
2382 wxCallAfter(self._SetInsertionPoint, pos)
2383 wxCallAfter(self._SetSelection, pos, end)
2384 elif event.ControlDown():
2385 dbg("control-end; select to end of control")
2386 wxCallAfter(self._SetInsertionPoint, pos)
2387 wxCallAfter(self._SetSelection, pos, len(self._mask))
2388 else:
2389 onChar = self._goEnd()
2390 dbg(indent=0)
2391 return False
2392
2393
2394 def _OnReturn(self, event):
2395 """
2396 Changes the event to look like a tab event, so we can then call
2397 event.Skip() on it, and have the parent form "do the right thing."
2398 """
2399 event.m_keyCode = WXK_TAB
2400 event.Skip()
2401
2402
2403 def _OnHome(self,event):
2404 """ Handles Home keypress in control. Should return False to skip other processing."""
2405 dbg("wxMaskedEditMixin::_OnHome", indent=1)
2406 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
2407 sel_start, sel_to = self._GetSelection()
2408
2409 if event.ShiftDown():
2410 dbg("shift-home; select to beginning of non-whitespace")
2411 if sel_to > pos:
2412 pos = sel_to
2413 wxCallAfter(self._SetInsertionPoint, pos)
2414 wxCallAfter(self._SetSelection, 0, pos)
2415 else:
2416 self._goHome()
2417 dbg(indent=0)
2418 return False
2419
2420
2421 def _OnChangeField(self, event):
2422 """
2423 Primarily handles TAB events, but can be used for any key that
2424 designer wants to change fields within a masked edit control.
2425 NOTE: at the moment, although coded to handle shift-TAB and
2426 control-shift-TAB, these events are not sent to the controls
2427 by the framework.
2428 """
2429 dbg('wxMaskedEditMixin::_OnChangeField', indent = 1)
2430 # determine end of current field:
2431 pos = self._GetInsertionPoint()
2432 dbg('current pos:', pos)
2433
2434
2435 masklength = len(self._mask)
2436 if masklength < 0: # no fields; process tab normally
2437 self._AdjustField(pos)
2438 if event.GetKeyCode() == WXK_TAB:
2439 dbg('tab to next ctrl')
2440 event.Skip()
2441 #else: do nothing
2442 dbg(indent=0)
2443 return False
2444
2445 field = self._FindField(pos)
2446
2447 if event.ShiftDown():
2448 # "Go backward"
2449
2450 # NOTE: doesn't yet work with SHIFT-tab under wx; the control
2451 # never sees this event! (But I've coded for it should it ever work,
2452 # and it *does* work for '.' in wxIpAddrCtrl.)
2453
2454 index = field._index
2455 begin_field = field._extent[0]
2456 if event.ControlDown():
2457 dbg('select to beginning of field:', begin_field, pos)
2458 wxCallAfter(self._SetInsertionPoint, begin_field)
2459 wxCallAfter(self._SetSelection, begin_field, pos)
2460 dbg(indent=0)
2461 return False
2462
2463 elif index == 0:
2464 # We're already in the 1st field; process shift-tab normally:
2465 self._AdjustField(pos)
2466 if event.GetKeyCode() == WXK_TAB:
2467 dbg('tab to previous ctrl')
2468 event.Skip()
2469 else:
2470 dbg('position at beginning')
2471 wxCallAfter(self._SetInsertionPoint, begin_field)
2472 dbg(indent=0)
2473 return False
2474 else:
2475 # find beginning of previous field:
2476 begin_prev = self._FindField(begin_field-1)._extent[0]
2477 self._AdjustField(pos)
2478 dbg('repositioning to', begin_prev)
2479 wxCallAfter(self._SetInsertionPoint, begin_prev)
2480 if self._FindField(begin_prev)._selectOnFieldEntry:
2481 edit_start, edit_end = self._FindFieldExtent(begin_prev)
2482 wxCallAfter(self._SetSelection, edit_start, edit_end)
2483 dbg(indent=0)
2484 return False
2485
2486 else:
2487 # "Go forward"
2488
2489 end_field = field._extent[1]
2490 if event.ControlDown():
2491 dbg('select to end of field:', pos, end_field)
2492 wxCallAfter(self._SetInsertionPoint, pos)
2493 wxCallAfter(self._SetSelection, pos, end_field)
2494 dbg(indent=0)
2495 return False
2496 else:
2497 dbg('end of current field:', end_field)
2498 dbg('go to next field')
2499 if end_field == self._fields[self._field_indices[-1]]._extent[1]:
2500 self._AdjustField(pos)
2501 if event.GetKeyCode() == WXK_TAB:
2502 dbg('tab to next ctrl')
2503 event.Skip()
2504 else:
2505 dbg('position at end')
2506 wxCallAfter(self._SetInsertionPoint, end_field)
2507 dbg(indent=0)
2508 return False
2509 else:
2510 # we have to find the start of the next field
2511 next_pos = self._findNextEntry(end_field)
2512 if next_pos == end_field:
2513 dbg('already in last field')
2514 self._AdjustField(pos)
2515 if event.GetKeyCode() == WXK_TAB:
2516 dbg('tab to next ctrl')
2517 event.Skip()
2518 #else: do nothing
2519 dbg(indent=0)
2520 return False
2521 else:
2522 self._AdjustField( pos )
2523
2524 # move cursor to appropriate point in the next field and select as necessary:
2525 field = self._FindField(next_pos)
2526 edit_start, edit_end = field._extent
2527 if field._selectOnFieldEntry:
2528 dbg('move to ', next_pos)
2529 wxCallAfter(self._SetInsertionPoint, next_pos)
2530 edit_start, edit_end = self._FindFieldExtent(next_pos)
2531 dbg('select', edit_start, edit_end)
2532 wxCallAfter(self._SetSelection, edit_start, edit_end)
2533 else:
2534 if field._insertRight:
2535 next_pos = field._extent[1]
2536 dbg('move to ', next_pos)
2537 wxCallAfter(self._SetInsertionPoint, next_pos)
2538 dbg(indent=0)
2539 return False
2540
2541
2542 def _OnDecimalPoint(self, event):
2543 dbg('wxMaskedEditMixin::_OnDecimalPoint', indent=1)
2544
2545 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
2546
2547 if self._isDec: ## handle decimal value, move to decimal place
2548 dbg('key == Decimal tab; decimal pos:', self._decimalpos)
2549 value = self._GetValue()
2550 if pos < self._decimalpos:
2551 clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:]
2552 dbg('value: "%s"' % self._GetValue(), 'clipped_text:', clipped_text)
2553 newstr = self._adjustDec(clipped_text)
2554 else:
2555 newstr = self._adjustDec(value)
2556 wxCallAfter(self._SetValue, newstr)
2557 wxCallAfter(self._SetInsertionPoint, self._decimalpos+1)
2558 keep_processing = False
2559
2560 if self._isInt: ## handle integer value, truncate from current position
2561 dbg('key == Integer decimal event')
2562 value = self._GetValue()
2563 clipped_text = value[0:pos]
2564 dbg('value: "%s"' % self._GetValue(), 'clipped_text:', clipped_text)
2565 newstr = self._adjustInt(clipped_text)
2566 dbg('newstr: "%s"' % newstr)
2567 wxCallAfter(self._SetValue, newstr)
2568 wxCallAfter(self._SetInsertionPoint, len(newstr.rstrip()))
2569 keep_processing = False
2570 dbg(indent=0)
2571
2572
2573 def _OnChangeSign(self, event):
2574 dbg('wxMaskedEditMixin::_OnChangeSign', indent=1)
2575 key = event.GetKeyCode()
2576 pos = self._adjustPos(self._GetInsertionPoint(), key)
2577 if chr(key) in ("-","+") or (chr(key) == " " and pos == self._signpos):
2578 cursign = self._isNeg
2579 dbg('cursign:', cursign)
2580 if chr(key) == "-":
2581 self._isNeg = (not self._isNeg) ## flip value
2582 else:
2583 self._isNeg = False
2584 dbg('isNeg?', self._isNeg)
2585
2586 text, signpos = self._getSignedValue()
2587 if text is not None:
2588 self._signpos = signpos
2589 dbg('self._signpos now:', self._signpos)
2590 else:
2591 text = self._GetValue()
2592 field = self._fields[0]
2593 if field._alignRight and field._fillChar == ' ':
2594 self._signpos = text.find('-')
2595 if self._signpos == -1:
2596 if len(text.lstrip()) < len(text):
2597 self._signpos = len(text) - len(text.lstrip()) - 1
2598 else:
2599 self._signpos = 0
2600 else:
2601 self._signpos = 0
2602 dbg('self._signpos now:', self._signpos)
2603 if self._isNeg:
2604 text = text[:self._signpos] + '-' + text[self._signpos+1:]
2605 else:
2606 text = text[:self._signpos] + ' ' + text[self._signpos+1:]
2607
2608 wxCallAfter(self._SetValue, text)
2609 wxCallAfter(self._applyFormatting)
2610 if pos == self._signpos:
2611 wxCallAfter(self._SetInsertionPoint, self._signpos+1)
2612 else:
2613 wxCallAfter(self._SetInsertionPoint, pos)
2614
2615 keep_processing = False
2616 else:
2617 keep_processing = True
2618 dbg(indent=0)
2619 return keep_processing
2620
2621
2622 def _OnGroupChar(self, event):
2623 """
2624 This handler is only registered if the mask is a numeric mask.
2625 It allows the insertion of ',' or '.' if appropriate.
2626 """
2627 dbg('wxMaskedEditMixin::_OnGroupChar', indent=1)
2628 keep_processing = True
2629 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
2630 sel_start, sel_to = self._GetSelection()
2631 groupchar = self._fields[0]._groupChar
2632 if not self._isCharAllowed(groupchar, pos, checkRegex=True):
2633 keep_processing = False
2634 if not wxValidator_IsSilent():
2635 wxBell()
2636
2637 if keep_processing:
2638 newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() )
2639 dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr)
2640 if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
2641 keep_processing = False
2642 if not wxValidator_IsSilent():
2643 wxBell()
2644
2645 if keep_processing:
2646 wxCallAfter(self._SetValue, newstr)
2647 wxCallAfter(self._SetInsertionPoint, newpos)
2648 keep_processing = False
2649 dbg(indent=0)
2650 return keep_processing
2651
2652
2653 def _findNextEntry(self,pos, adjustInsert=True):
2654 """ Find the insertion point for the next valid entry character position."""
2655 if self._isTemplateChar(pos): # if changing fields, pay attn to flag
2656 adjustInsert = adjustInsert
2657 else: # else within a field; flag not relevant
2658 adjustInsert = False
2659
2660 while self._isTemplateChar(pos) and pos < len(self._mask):
2661 pos += 1
2662
2663 # if changing fields, and we've been told to adjust insert point,
2664 # look at new field; if empty and right-insert field,
2665 # adjust to right edge:
2666 if adjustInsert and pos < len(self._mask):
2667 field = self._FindField(pos)
2668 start, end = field._extent
2669 slice = self._GetValue()[start:end]
2670 if field._insertRight and field.IsEmpty(slice):
2671 pos = end
2672 return pos
2673
2674
2675 def _findNextTemplateChar(self, pos):
2676 """ Find the position of the next non-editable character in the mask."""
2677 while not self._isTemplateChar(pos) and pos < len(self._mask):
2678 pos += 1
2679 return pos
2680
2681
2682 def _OnAutoCompleteField(self, event):
2683 dbg('wxMaskedEditMixin::_OnAutoCompleteField', indent =1)
2684 pos = self._GetInsertionPoint()
2685 field = self._FindField(pos)
2686 edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
2687 match_index = None
2688 keycode = event.GetKeyCode()
2689 text = slice.replace(field._fillChar, '')
2690 text = text.strip()
2691 keep_processing = True # (assume True to start)
2692 dbg('field._hasList?', field._hasList)
2693 if field._hasList:
2694 dbg('choices:', field._choices)
2695 choices, choice_required = field._choices, field._choiceRequired
2696 if keycode in (WXK_PRIOR, WXK_UP):
2697 direction = -1
2698 else:
2699 direction = 1
2700 match_index = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase)
2701 if( match_index is None
2702 and (keycode in self._autoCompleteKeycodes + [WXK_PRIOR, WXK_NEXT]
2703 or (keycode in [WXK_UP, WXK_DOWN] and event.ShiftDown() ) ) ):
2704 # Select the 1st thing from the list:
2705 match_index = 0
2706 if( match_index is not None
2707 and ( keycode in self._autoCompleteKeycodes + [WXK_PRIOR, WXK_NEXT]
2708 or (keycode in [WXK_UP, WXK_DOWN] and event.ShiftDown())
2709 or (keycode == WXK_DOWN and len(text) < len(choices[match_index])) ) ):
2710
2711 # We're allowed to auto-complete:
2712 value = self._GetValue()
2713 newvalue = value[:edit_start] + choices[match_index] + value[edit_end:]
2714 dbg('match found; setting value to "%s"' % newvalue)
2715 self._SetValue(newvalue)
2716 self._SetInsertionPoint(edit_end)
2717 self._CheckValid() # recolor as appopriate
2718
2719 if keycode in (WXK_UP, WXK_DOWN, WXK_LEFT, WXK_RIGHT):
2720 # treat as left right arrow if unshifted, tab/shift tab if shifted.
2721 if event.ShiftDown():
2722 if keycode in (WXK_DOWN, WXK_RIGHT):
2723 # remove "shifting" and treat as (forward) tab:
2724 event.m_shiftDown = False
2725 keep_processing = self._OnChangeField(event)
2726 else:
2727 keep_processing = self._OnArrow(event)
2728 # some other key; keep processing the key
2729
2730 dbg('keep processing?', keep_processing, indent=0)
2731 return keep_processing
2732
2733
2734 def _autoComplete(self, direction, choices, value, compareNoCase):
2735 """
2736 This function gets called in response to Auto-complete events.
2737 It attempts to find a match to the specified value against the
2738 list of choices; if exact match, the index of then next
2739 appropriate value in the list, based on the given direction.
2740 If not an exact match, it will return the index of the 1st value from
2741 the choice list for which the partial value can be extended to match.
2742 If no match found, it will return None.
2743 """
2744 if value is None:
2745 dbg('nothing to match against', indent=0)
2746 return None
2747
2748 if compareNoCase:
2749 choices = [choice.strip().lower() for choice in choices]
2750 value = value.lower()
2751 else:
2752 choices = [choice.strip() for choice in choices]
2753
2754 if value in choices:
2755 index = choices.index(value)
2756 dbg('matched "%s"' % choices[index])
2757 if direction == -1:
2758 if index == 0: index = len(choices) - 1
2759 else: index -= 1
2760 else:
2761 if index == len(choices) - 1: index = 0
2762 else: index += 1
2763 dbg('change value to ', index)
2764 match = index
2765 else:
2766 value = value.strip()
2767 dbg('no match; try to auto-complete:')
2768 match = None
2769 dbg('searching for "%s"' % value)
2770 for index in range(len(choices)):
2771 choice = choices[index]
2772 if choice.find(value, 0) == 0:
2773 dbg('match found:', choice)
2774 match = index
2775 break
2776 if match is not None:
2777 dbg('matched', match)
2778 else:
2779 dbg('no match found')
2780 dbg(indent=0)
2781 return match
2782
2783
2784 def _AdjustField(self, pos):
2785 """
2786 This function gets called by default whenever the cursor leaves a field.
2787 The pos argument given is the char position before leaving that field.
2788 By default, floating point, integer and date values are adjusted to be
2789 legal in this function. Derived classes may override this function
2790 to modify the value of the control in a different way when changing fields.
2791
2792 NOTE: these change the value immediately, and restore the cursor to
2793 the passed location, so that any subsequent code can then move it
2794 based on the operation being performed.
2795 """
2796 newvalue = value = self._GetValue()
2797 field = self._FindField(pos)
2798 start, end, slice = self._FindFieldExtent(getslice=True)
2799 newfield = field._AdjustField(slice)
2800 newvalue = value[:start] + newfield + value[end:]
2801
2802 if self._isDec and newvalue != self._template:
2803 newvalue = self._adjustDec(newvalue)
2804
2805 if self._ctrl_constraints._isInt and value != self._template:
2806 newvalue = self._adjustInt(value)
2807
2808 if self._isDate and value != self._template:
2809 newvalue = self._adjustDate(value, fixcentury=True)
2810 if self._4digityear:
2811 year2dig = self._dateExtent - 2
2812 if pos == year2dig and value[year2dig] != newvalue[year2dig]:
2813 pos = pos+2
2814
2815 if newvalue != value:
2816 self._SetValue(newvalue)
2817 self._SetInsertionPoint(pos)
2818
2819
2820 def _adjustKey(self, pos, key):
2821 """ Apply control formatting to the key (e.g. convert to upper etc). """
2822 field = self._FindField(pos)
2823 if field._forceupper and key in range(97,123):
2824 key = ord( chr(key).upper())
2825
2826 if field._forcelower and key in range(97,123):
2827 key = ord( chr(key).lower())
2828
2829 return key
2830
2831
2832 def _adjustPos(self, pos, key):
2833 """
2834 Checks the current insertion point position and adjusts it if
2835 necessary to skip over non-editable characters.
2836 """
2837 dbg('_adjustPos', pos, key, indent=1)
2838 sel_start, sel_to = self._GetSelection()
2839 # If a numeric or decimal mask, and negatives allowed, reserve the first space for sign
2840 if( self._signOk
2841 and pos == self._signpos
2842 and key in (ord('-'), ord('+'), ord(' ')) ):
2843 return pos
2844
2845 if key not in self._nav:
2846 field = self._FindField(pos)
2847
2848 dbg('field._insertRight?', field._insertRight)
2849 if field._insertRight: # if allow right-insert
2850 start, end = field._extent
2851 slice = self._GetValue()[start:end].strip()
2852 field_len = end - start
2853 if pos == end: # if cursor at right edge of field
2854 # if not filled or supposed to stay in field, keep current position
2855 if len(slice) < field_len or not field._moveOnFieldFull:
2856 dbg('pos==end; len (slice) < field_len or not field._moveOnFieldFull')
2857 pass
2858 else:
2859 # if at start of control, move to right edge
2860 if sel_to == sel_start and self._isTemplateChar(pos) and pos != end:
2861 pos = end # move to right edge
2862 elif sel_start <= start and sel_to == end:
2863 # select to right edge of field - 1 (to replace char)
2864 pos = end - 1
2865 self._SetInsertionPoint(pos)
2866 # restore selection
2867 self._SetSelection(sel_start, pos)
2868
2869 elif self._signOk and sel_start == 0: # if selected to beginning and signed,
2870 # adjust to past reserved sign position:
2871 pos = self._fields[0]._extent[0]
2872 self._SetInsertionPoint(pos)
2873 # restore selection
2874 self._SetSelection(pos, sel_to)
2875 else:
2876 pass # leave position/selection alone
2877
2878 # else make sure the user is not trying to type over a template character
2879 # If they are, move them to the next valid entry position
2880 elif self._isTemplateChar(pos):
2881 if( not field._moveOnFieldFull
2882 and (not self._signOk
2883 or (self._signOk
2884 and field._index == 0
2885 and pos > 0) ) ): # don't move to next field without explicit cursor movement
2886 pass
2887 else:
2888 # find next valid position
2889 pos = self._findNextEntry(pos)
2890 self._SetInsertionPoint(pos)
2891 if pos < sel_to: # restore selection
2892 self._SetSelection(pos, sel_to)
2893 dbg('adjusted pos:', pos, indent=0)
2894 return pos
2895
2896
2897 def _adjustDec(self, candidate=None):
2898 """
2899 'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
2900 """
2901 dbg('wxMaskedEditMixin::_adjustDec, candidate = "%s"' % candidate, indent=1)
2902 lenOrd,lenDec = self._mask.split(self._decimalChar) ## Get ordinal, decimal lengths
2903 lenOrd = len(lenOrd)
2904 lenDec = len(lenDec)
2905 if candidate is None: value = self._GetValue()
2906 else: value = candidate
2907 dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
2908 ordStr,decStr = value.split(self._decimalChar)
2909
2910 ordStr = self._fields[0]._AdjustField(ordStr)
2911 dbg('adjusted ordStr: "%s"' % ordStr)
2912 lenOrd = len(ordStr)
2913 decStr = decStr + ('0'*(lenDec-len(decStr))) # add trailing spaces to decimal
2914
2915 dbg('ordStr "%(ordStr)s"' % locals())
2916 dbg('lenOrd:', lenOrd)
2917
2918 ordStr = string.rjust( ordStr[-lenOrd:], lenOrd)
2919 dbg('right-justifed ordStr = "%(ordStr)s"' % locals())
2920 newvalue = ordStr + self._decimalChar + decStr
2921 if self._signOk:
2922 if len(newvalue) < len(self._mask):
2923 newvalue = ' ' + newvalue
2924 signedvalue = self._getSignedValue(newvalue)[0]
2925 if signedvalue is not None: newvalue = signedvalue
2926
2927 # Finally, align string with decimal position, left-padding with
2928 # fillChar:
2929 newdecpos = newvalue.find(self._decimalChar)
2930 if newdecpos < self._decimalpos:
2931 padlen = self._decimalpos - newdecpos
2932 newvalue = string.join([' ' * padlen] + [newvalue] ,'')
2933 dbg('newvalue = "%s"' % newvalue)
2934 if candidate is None:
2935 wxCallAfter(self._SetValue, newvalue)
2936 dbg(indent=0)
2937 return newvalue
2938
2939
2940 def _adjustInt(self, candidate=None):
2941 """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
2942 dbg("wxMaskedEditMixin::_adjustInt")
2943 lenInt = len(self._mask)
2944 if candidate is None: value = self._GetValue()
2945 else: value = candidate
2946 intStr = self._fields[0]._AdjustField(value)
2947 intStr = intStr.strip() # drop extra spaces
2948 if self._isNeg and intStr.find('-') == -1:
2949 intStr = '-' + intStr
2950 elif self._signOk and intStr.find('-') == -1:
2951 intStr = ' ' + intStr
2952
2953 if self._fields[0]._alignRight: ## Only if right-alignment is enabled
2954 intStr = intStr.rjust( lenInt )
2955 else:
2956 intStr = intStr.ljust( lenInt )
2957
2958 if candidate is None:
2959 wxCallAfter(self._SetValue, intStr )
2960 return intStr
2961
2962
2963 def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False):
2964 """
2965 'Fixes' a date control, expanding the year if it can.
2966 Applies various self-formatting options.
2967 """
2968 dbg("wxMaskedEditMixin::_adjustDate", indent=1)
2969 if candidate is None: text = self._GetValue()
2970 else: text = candidate
2971 dbg('text=', text)
2972 if self._datestyle == "YMD":
2973 year_field = 0
2974 else:
2975 year_field = 2
2976
2977 dbg('getYear: "%s"' % getYear(text, self._datestyle))
2978 year = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars
2979 month = getMonth( text, self._datestyle)
2980 day = getDay( text, self._datestyle)
2981 dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day)
2982
2983 yearVal = None
2984 yearstart = self._dateExtent - 4
2985 if( len(year) < 4
2986 and (fixcentury
2987 or force4digit_year
2988 or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ')
2989 or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ):
2990 ## user entered less than four digits and changing fields or past point where we could
2991 ## enter another digit:
2992 try:
2993 yearVal = int(year)
2994 except:
2995 dbg('bad year=', year)
2996 year = text[yearstart:self._dateExtent]
2997
2998 if len(year) < 4 and yearVal:
2999 if len(year) == 2:
3000 # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the
3001 # years pass...
3002 now = wxDateTime_Now()
3003 century = (now.GetYear() /100) * 100 # "this century"
3004 twodig_year = now.GetYear() - century # "this year" (2 digits)
3005 # if separation between today's 2-digit year and typed value > 50,
3006 # assume last century,
3007 # else assume this century.
3008 #
3009 # Eg: if 2003 and yearVal == 30, => 2030
3010 # if 2055 and yearVal == 80, => 2080
3011 # if 2010 and yearVal == 96, => 1996
3012 #
3013 if abs(yearVal - twodig_year) > 50:
3014 yearVal = (century - 100) + yearVal
3015 else:
3016 yearVal = century + yearVal
3017 year = str( yearVal )
3018 else: # pad with 0's to make a 4-digit year
3019 year = "%04d" % yearVal
3020 if self._4digityear or force4digit_year:
3021 text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:]
3022 dbg('newdate: "%s"' % text, indent=0)
3023 return text
3024
3025
3026 def _goEnd(self, getPosOnly=False):
3027 """ Moves the insertion point to the end of user-entry """
3028 dbg("wxMaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
3029 text = self._GetValue()
3030 for i in range( min( len(self._mask)-1, len(text)-1 ), -1, -1):
3031 if self._isMaskChar(i):
3032 char = text[i]
3033 if char != ' ':
3034 break
3035
3036 pos = min(i+1,len(self._mask))
3037 field = self._FindField(pos)
3038 start, end = field._extent
3039 if field._insertRight and pos < end:
3040 pos = end
3041 dbg('next pos:', pos)
3042 dbg(indent=0)
3043 if getPosOnly:
3044 return pos
3045 else:
3046 self._SetInsertionPoint(pos)
3047
3048
3049 def _goHome(self):
3050 """ Moves the insertion point to the beginning of user-entry """
3051 dbg("wxMaskedEditMixin::_goHome", indent=1)
3052 text = self._GetValue()
3053 for i in range(len(self._mask)):
3054 if self._isMaskChar(i):
3055 break
3056 self._SetInsertionPoint(max(i,0))
3057 dbg(indent=0)
3058
3059
3060 def _getAllowedChars(self, pos):
3061 """ Returns a string of all allowed user input characters for the provided
3062 mask character plus control options
3063 """
3064 maskChar = self.maskdict[pos]
3065 okchars = self.maskchardict[maskChar] ## entry, get mask approved characters
3066 field = self._FindField(pos)
3067 if okchars and field._okSpaces: ## Allow spaces?
3068 okchars += " "
3069 if okchars and field._includeChars: ## any additional included characters?
3070 okchars += field._includeChars
3071 ## dbg('okchars[%d]:' % pos, okchars)
3072 return okchars
3073
3074
3075 def _isMaskChar(self, pos):
3076 """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
3077 """
3078 if pos < len(self._mask):
3079 return self.ismasked[pos]
3080 else:
3081 return False
3082
3083
3084 def _isTemplateChar(self,Pos):
3085 """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#)
3086 """
3087 if Pos < len(self._mask):
3088 return not self._isMaskChar(Pos)
3089 else:
3090 return False
3091
3092
3093 def _isCharAllowed(self, char, pos, checkRegex=False):
3094 """ Returns True if character is allowed at the specific position, otherwise False."""
3095 dbg('_isCharAllowed', char, pos, checkRegex, indent=1)
3096 field = self._FindField(pos)
3097 right_insert = False
3098
3099 if self.controlInitialized:
3100 sel_start, sel_to = self._GetSelection()
3101 else:
3102 sel_start, sel_to = pos, pos
3103
3104 if (field._insertRight or self._ctrl_constraints._insertRight):
3105 start, end = field._extent
3106 if pos == end or (sel_start, sel_to) == field._extent:
3107 pos = end - 1
3108 right_insert = True
3109
3110 if self._isTemplateChar( pos ): ## if a template character, return empty
3111 dbg(indent=0)
3112 return False
3113
3114 if self._isMaskChar( pos ):
3115 okChars = self._getAllowedChars(pos)
3116 if self._fields[0]._groupdigits and (self._isInt or (self._isDec and pos < self._decimalpos)):
3117 okChars += self._fields[0]._groupChar
3118 if self._signOk and (self._isInt or (self._isDec and pos < self._decimalpos)):
3119 okChars += '-'
3120 ## dbg('%s in %s?' % (char, okChars), char in okChars)
3121 approved = char in okChars
3122
3123 if approved and checkRegex:
3124 dbg("checking appropriate regex's")
3125 value = self._eraseSelection(self._GetValue())
3126 if right_insert:
3127 newvalue, newpos = self._insertKey(char, pos+1, sel_start, sel_to, value)
3128 else:
3129 newvalue, newpos = self._insertKey(char, pos, sel_start, sel_to, value)
3130 dbg('newvalue: "%s"' % newvalue)
3131
3132 fields = [self._FindField(pos)] + [self._ctrl_constraints]
3133 for field in fields: # includes fields[-1] == "ctrl_constraints"
3134 if field._regexMask and field._filter:
3135 dbg('checking vs. regex')
3136 start, end = field._extent
3137 slice = newvalue[start:end]
3138 approved = (re.match( field._filter, slice) is not None)
3139 dbg('approved?', approved)
3140 if not approved: break
3141 dbg(indent=0)
3142 return approved
3143 else:
3144 dbg(indent=0)
3145 return False
3146
3147
3148 def _applyFormatting(self):
3149 """ Apply formatting depending on the control's state.
3150 Need to find a way to call this whenever the value changes, in case the control's
3151 value has been changed or set programatically.
3152 """
3153 dbg(suspend=1)
3154 dbg('wxMaskedEditMixin::_applyFormatting', indent=1)
3155
3156 # Handle negative numbers
3157 if self._signOk:
3158 ## value = self._GetValue()
3159 text, signpos = self._getSignedValue()
3160 dbg('text: "%s", signpos:' % text, signpos)
3161 if not text or text[signpos] != '-':
3162 self._isNeg = False
3163 dbg('supposedly negative, but no sign found; new sign:', self._isNeg)
3164 if text and signpos != self._signpos:
3165 self._signpos = signpos
3166 elif text and self._valid and not self._isNeg and text[signpos] == '-':
3167 self._isNeg = True
3168
3169 if self._signOk and self._isNeg:
3170 dbg('setting foreground to', self._signedForegroundColor)
3171 self.SetForegroundColour(self._signedForegroundColor)
3172 else:
3173 dbg('setting foreground to', self._foregroundColor)
3174 self.SetForegroundColour(self._foregroundColor)
3175
3176 if self._valid:
3177 dbg('valid')
3178 if self.IsEmpty():
3179 dbg('setting background to', self._emptyBackgroundColor)
3180 self.SetBackgroundColour(self._emptyBackgroundColor)
3181 else:
3182 self.SetBackgroundColour(self._validBackgroundColor)
3183 else:
3184 dbg('invalid; coloring', self._invalidBackgroundColor)
3185 self.SetBackgroundColour(self._invalidBackgroundColor) ## Change BG color if invalid
3186
3187 self._Refresh()
3188 dbg(indent=0, suspend=0)
3189
3190
3191 def _getAbsValue(self, candidate=None):
3192 """ Return an unsigned value (i.e. strip the '-' prefix if any).
3193 """
3194 dbg('wxMaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1)
3195 if candidate is None: text = self._GetValue()
3196 else: text = candidate
3197
3198 if self._isInt:
3199 if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ':
3200 signpos = text.find('-')
3201 if signpos == -1:
3202 rstripped_text = text.rstrip()
3203 signpos = rstripped_text.rfind(' ')
3204 dbg('signpos:', signpos)
3205 if signpos == -1:
3206 signpos = 0
3207 abstext = text[:signpos] + ' ' + text[signpos+1:]
3208 else:
3209 signpos = 0
3210 text = self._template[0] + text[1:]
3211 groupchar = self._fields[0]._groupChar
3212 try:
3213 value = long(text.replace(groupchar,''))
3214 except:
3215 dbg('invalid number', indent=0)
3216 return None, None
3217
3218 else: # decimal value
3219 try:
3220 groupchar = self._fields[0]._groupChar
3221 value = float(text.replace(groupchar,'').replace(self._decimalChar, '.'))
3222 if value < 0:
3223 signpos = text.find('-')
3224 text = text[:signpos] + self._template[signpos] + text[signpos+1:]
3225 else:
3226 # look backwards from the decimal point for the 1st non-digit
3227 dbg('decimal pos:', self._decimalpos)
3228 signpos = self._decimalpos-1
3229 dbg('text: "%s"' % text)
3230 dbg('text[%d]:' % signpos, text[signpos])
3231 dbg('text[signpos] in list(string.digits) + [groupchar]?', text[signpos] in list(string.digits) + [groupchar])
3232 while text[signpos] in list(string.digits) + [groupchar] and signpos > 0:
3233 signpos -= 1
3234 dbg('text[%d]:' % signpos, text[signpos])
3235 except ValueError:
3236 dbg('invalid number', indent=0)
3237 return None, None
3238
3239 dbg('abstext = "%s"' % text, 'signpos:', signpos)
3240 dbg(indent=0)
3241 return text, signpos
3242
3243
3244 def _getSignedValue(self, candidate=None):
3245 """ Return a signed value by adding a "-" prefix if the value
3246 is set to negative, or a space if positive.
3247 """
3248 dbg('wxMaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1)
3249 if candidate is None: text = self._GetValue()
3250 else: text = candidate
3251
3252
3253 abstext, signpos = self._getAbsValue(text)
3254 if self._signOk:
3255 if abstext is None:
3256 dbg(indent=0)
3257 return abstext, signpos
3258
3259 if self._isNeg or text[signpos] == '-':
3260 sign = '-'
3261 else:
3262 sign = ' '
3263 if text[signpos] not in string.digits:
3264 text = text[:signpos] + sign + text[signpos+1:]
3265 else:
3266 # this can happen if value passed is too big; sign assumed to be
3267 # in position 0, but if already filled with a digit, prepend sign...
3268 text = sign + text
3269 else:
3270 text = abstext
3271 dbg('signedtext = "%s"' % text, 'signpos:', signpos)
3272 dbg(indent=0)
3273 return text, signpos
3274
3275
3276 def GetPlainValue(self, candidate=None):
3277 """ Returns control's value stripped of the template text.
3278 plainvalue = wxMaskedEditMixin.GetPlainValue()
3279 """
3280 if candidate is None: text = self._GetValue()
3281 else: text = candidate
3282
3283 if self.IsEmpty():
3284 return ""
3285 else:
3286 plain = ""
3287 for idx in range( len( self._template)):
3288 if self._mask[idx] in maskchars:
3289 plain += text[idx]
3290
3291 if self._isDec or self._isInt:
3292 if self._fields[0]._alignRight:
3293 lpad = plain.count(',')
3294 plain = ' ' * lpad + plain.replace(',','')
3295 else:
3296 plain = plain.replace(',','')
3297
3298 if self._signOk and self._isNeg and plain.count('-') == 0:
3299 # must be in reserved position; add to "plain value"
3300 plain = '-' + plain
3301
3302 return plain.rstrip()
3303
3304
3305 def IsEmpty(self, value=None):
3306 """
3307 Returns True if control is equal to an empty value.
3308 (Empty means all editable positions in the template == fillChar.)
3309 """
3310 if value is None: value = self._GetValue()
3311 if value == self._template and not self._defaultValue:
3312 ## dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)")
3313 return True # (all mask chars == fillChar by defn)
3314 elif value == self._template:
3315 empty = True
3316 for pos in range(len(self._template)):
3317 ## dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos))
3318 ## dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos])
3319 if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]):
3320 empty = False
3321 ## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals())
3322 return empty
3323 else:
3324 ## dbg("IsEmpty? 0 (value doesn't match template)")
3325 return False
3326
3327
3328 def IsDefault(self, value=None):
3329 """
3330 Returns True if the value specified (or the value of the control if not specified)
3331 is equal to the default value.
3332 """
3333 if value is None: value = self._GetValue()
3334 return value == self._template
3335
3336
3337 def IsValid(self, value=None):
3338 """ Indicates whether the value specified (or the current value of the control
3339 if not specified) is considered valid."""
3340 ## dbg('wxMaskedEditMixin::IsValid("%s")' % value, indent=1)
3341 if value is None: value = self._GetValue()
3342 ret = self._CheckValid(value)
3343 ## dbg(indent=0)
3344 return ret
3345
3346
3347 def _eraseSelection(self, value=None, sel_start=None, sel_to=None):
3348 """ Used to blank the selection when inserting a new character. """
3349 dbg("wxMaskedEditMixin::_eraseSelection", indent=1)
3350 if value is None: value = self._GetValue()
3351 if sel_start is None or sel_to is None:
3352 sel_start, sel_to = self._GetSelection() ## check for a range of selected text
3353 dbg('value: "%s"' % value)
3354 dbg("current sel_start, sel_to:", sel_start, sel_to)
3355
3356 newvalue = list(value)
3357 for i in range(sel_start, sel_to):
3358 if self._isMaskChar(i):
3359 field = self._FindField(i)
3360 if field._padZero:
3361 newvalue[i] = '0'
3362 else:
3363 newvalue[i] = self._template[i]
3364 elif self._signOk and i == 0 and newvalue[i] == '-':
3365 newvalue[i] = ' '
3366 value = string.join(newvalue,"")
3367 dbg('new value: "%s"' % value)
3368 dbg(indent=0)
3369 return value
3370
3371
3372 def _insertKey(self, char, pos, sel_start, sel_to, value):
3373 """ Handles replacement of the character at the current insertion point."""
3374 dbg('wxMaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
3375
3376 text = self._eraseSelection(value)
3377 field = self._FindField(pos)
3378 start, end = field._extent
3379 newtext = ""
3380 if field._insertRight:
3381 # special case; do right insertion if either whole field selected or cursor at right edge of field
3382 if pos == end or (sel_start, sel_to) == field._extent: # right edge insert
3383 fstr = text[start:end]
3384 erasable_chars = [field._fillChar, ' ']
3385 if field._padZero: erasable_chars.append('0')
3386 erased = ''
3387 if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] == '-'):
3388 erased = fstr[0]
3389 fstr = fstr[1:] + char
3390 dbg('field str: "%s"' % fstr)
3391 newtext = text[:start] + fstr + text[end:]
3392 if erased == '-' and self._signOk:
3393 newtext = '-' + newtext[1:]
3394 dbg('newtext: "%s"' % newtext)
3395 if self._signOk and field._index == 0: start -= 1 # account for sign position
3396 if( field._moveOnFieldFull
3397 and len(fstr.lstrip()) == end-start): # if field now full
3398 newpos = self._findNextEntry(end) # go to next field
3399 else:
3400 newpos = end # else keep cursor at right edge
3401
3402 if not newtext:
3403 before = text[0:pos]
3404 after = text[pos+1:]
3405 newtext = before + char + after
3406 newpos = pos+1
3407
3408 dbg('newtext: "%s"' % newtext, 'newpos:', newpos)
3409
3410 dbg(indent=0)
3411 return newtext, newpos
3412
3413
3414 def _OnFocus(self,event):
3415 """
3416 This event handler is currently necessary to work around new default
3417 behavior as of wxPython2.3.3;
3418 The TAB key auto selects the entire contents of the wxTextCtrl *after*
3419 the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
3420 *here*, because it hasn't happened yet. So to prevent this behavior, and
3421 preserve the correct selection when the focus event is not due to tab,
3422 we need to pull the following trick:
3423 """
3424 dbg('wxMaskedEditMixin::_OnFocus')
3425 wxCallAfter(self._fixSelection)
3426 event.Skip()
3427 self.Refresh()
3428
3429
3430 def _CheckValid(self, candidate=None):
3431 """
3432 This is the default validation checking routine; It verifies that the
3433 current value of the control is a "valid value," and has the side
3434 effect of coloring the control appropriately.
3435 """
3436 dbg(suspend=1)
3437 dbg('wxMaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1)
3438 oldValid = self._valid
3439 if candidate is None: value = self._GetValue()
3440 else: value = candidate
3441 dbg('value: "%s"' % value)
3442 oldvalue = value
3443 valid = True # assume True
3444
3445 if not self.IsDefault(value) and self._isDate: ## Date type validation
3446 valid = self._validateDate(value)
3447 dbg("valid date?", valid)
3448
3449 elif not self.IsDefault(value) and self._isTime:
3450 valid = self._validateTime(value)
3451 dbg("valid time?", valid)
3452
3453 elif not self.IsDefault(value) and (self._isInt or self._isDec): ## Numeric type
3454 valid = self._validateNumeric(value)
3455 dbg("valid Number?", valid)
3456
3457 if valid and not self.IsDefault(value):
3458 ## valid so far; ensure also allowed by any list or regex provided:
3459 valid = self._validateGeneric(value)
3460 dbg("valid value?", valid)
3461
3462 if valid:
3463 for field in self._fields.values(): # (includes field -1, ie: "global setting")
3464 if field._emptyInvalid:
3465 start, end = field._extent
3466 if field.IsEmpty(value[start:end]):
3467 valid = False
3468 break
3469
3470 dbg('valid?', valid)
3471
3472 if not candidate:
3473 self._valid = valid
3474 self._applyFormatting()
3475 if self._valid != oldValid:
3476 dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
3477 dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
3478 dbg(indent=0, suspend=0)
3479 return valid
3480
3481
3482 def _validateGeneric(self, candidate=None):
3483 """ Validate the current value using the provided list or Regex filter (if any).
3484 """
3485 if candidate is None:
3486 text = self._GetValue()
3487 else:
3488 text = candidate
3489
3490 valid = True # assume true
3491 for i in [-1] + self._field_indices: # process global constraints first:
3492 field = self._fields[i]
3493 start, end = field._extent
3494 slice = text[start:end]
3495 valid = field.IsValid(slice)
3496 if not valid:
3497 break
3498
3499 return valid
3500
3501
3502 def _validateNumeric(self, candidate=None):
3503 """ Validate that the value is within the specified range (if specified.)"""
3504 if candidate is None: value = self._GetValue()
3505 else: value = candidate
3506 try:
3507 groupchar = self._fields[0]._groupChar
3508 if self._isDec:
3509 number = float(value.replace(groupchar, '').replace(self._decimalChar, '.'))
3510 else:
3511 number = int( value.replace(groupchar, ''))
3512 dbg('number:', number)
3513 if self._ctrl_constraints._hasRange:
3514 valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh
3515 else:
3516 valid = True
3517 groupcharpos = value.rfind(groupchar)
3518 if groupcharpos != -1: # group char present
3519 dbg('groupchar found at', groupcharpos)
3520 if self._isDec and groupcharpos > self._decimalpos:
3521 # 1st one found on right-hand side is past decimal point
3522 dbg('groupchar in fraction; illegal')
3523 valid = False
3524 elif self._isDec:
3525 ord = value[:self._decimalpos]
3526 else:
3527 ord = value.strip()
3528
3529 parts = ord.split(groupchar)
3530 for i in range(len(parts)):
3531 if i == 0 and abs(int(parts[0])) > 999:
3532 dbg('group 0 too long; illegal')
3533 valid = False
3534 break
3535 elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]):
3536 dbg('group %i (%s) not right size; illegal' % (i, parts[i]))
3537 valid = False
3538 break
3539 except ValueError:
3540 dbg('value not a valid number')
3541 valid = False
3542 return valid
3543
3544
3545 def _validateDate(self, candidate=None):
3546 """ Validate the current date value using the provided Regex filter.
3547 Generally used for character types.BufferType
3548 """
3549 dbg('wxMaskedEditMixin::_validateDate', indent=1)
3550 if candidate is None: value = self._GetValue()
3551 else: value = candidate
3552 dbg('value = "%s"' % value)
3553 text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it
3554 dbg('text =', text)
3555 valid = True # assume True until proven otherwise
3556
3557 try:
3558 # replace fillChar in each field with space:
3559 datestr = text[0:self._dateExtent]
3560 for i in range(3):
3561 field = self._fields[i]
3562 start, end = field._extent
3563 fstr = datestr[start:end]
3564 fstr.replace(field._fillChar, ' ')
3565 datestr = datestr[:start] + fstr + datestr[end:]
3566
3567 year, month, day = getDateParts( datestr, self._datestyle)
3568 year = int(year)
3569 dbg('self._dateExtent:', self._dateExtent)
3570 if self._dateExtent == 11:
3571 month = charmonths_dict[month.lower()]
3572 else:
3573 month = int(month)
3574 day = int(day)
3575 dbg('year, month, day:', year, month, day)
3576
3577 except ValueError:
3578 dbg('cannot convert string to integer parts')
3579 valid = False
3580 except KeyError:
3581 dbg('cannot convert string to integer month')
3582 valid = False
3583
3584 if valid:
3585 # use wxDateTime to unambiguously try to parse the date:
3586 # ### Note: because wxDateTime is *brain-dead* and expects months 0-11,
3587 # rather than 1-12, so handle accordingly:
3588 if month > 12:
3589 valid = False
3590 else:
3591 month -= 1
3592 try:
3593 dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year))
3594 dateHandler = wxDateTimeFromDMY(day,month,year)
3595 dbg("succeeded")
3596 dateOk = True
3597 except:
3598 dbg('cannot convert string to valid date')
3599 dateOk = False
3600 if not dateOk:
3601 valid = False
3602
3603 if valid:
3604 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
3605 # so we eliminate them here:
3606 timeStr = text[self._dateExtent+1:].strip() ## time portion of the string
3607 if timeStr:
3608 dbg('timeStr: "%s"' % timeStr)
3609 try:
3610 checkTime = dateHandler.ParseTime(timeStr)
3611 valid = checkTime == len(timeStr)
3612 except:
3613 valid = False
3614 if not valid:
3615 dbg('cannot convert string to valid time')
3616 if valid: dbg('valid date')
3617 dbg(indent=0)
3618 return valid
3619
3620
3621 def _validateTime(self, candidate=None):
3622 """ Validate the current time value using the provided Regex filter.
3623 Generally used for character types.BufferType
3624 """
3625 dbg('wxMaskedEditMixin::_validateTime', indent=1)
3626 # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
3627 # so we eliminate them here:
3628 if candidate is None: value = self._GetValue().strip()
3629 else: value = candidate.strip()
3630 dbg('value = "%s"' % value)
3631 valid = True # assume True until proven otherwise
3632
3633 dateHandler = wxDateTime_Today()
3634 try:
3635 checkTime = dateHandler.ParseTime(value)
3636 dbg('checkTime:', checkTime, 'len(value)', len(value))
3637 valid = checkTime == len(value)
3638 except:
3639 valid = False
3640
3641 if not valid:
3642 dbg('cannot convert string to valid time')
3643 if valid: dbg('valid time')
3644 dbg(indent=0)
3645 return valid
3646
3647
3648 def _OnKillFocus(self,event):
3649 """ Handler for EVT_KILL_FOCUS event.
3650 """
3651 dbg('wxMaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1)
3652 if self._mask and self._IsEditable():
3653 self._AdjustField(self._GetInsertionPoint())
3654 self._CheckValid() ## Call valid handler
3655
3656 self._LostFocus() ## Provided for subclass use
3657 event.Skip()
3658 dbg(indent=0)
3659
3660
3661 def _fixSelection(self):
3662 """
3663 This gets called after the TAB traversal selection is made, if the
3664 focus event was due to this, but before the EVT_LEFT_* events if
3665 the focus shift was due to a mouse event.
3666
3667 The trouble is that, a priori, there's no explicit notification of
3668 why the focus event we received. However, the whole reason we need to
3669 do this is because the default behavior on TAB traveral in a wxTextCtrl is
3670 now to select the entire contents of the window, something we don't want.
3671 So we can *now* test the selection range, and if it's "the whole text"
3672 we can assume the cause, change the insertion point to the start of
3673 the control, and deselect.
3674 """
3675 dbg('wxMaskedEditMixin::_fixSelection', indent=1)
3676 if not self._mask or not self._IsEditable():
3677 dbg(indent=0)
3678 return
3679
3680 sel_start, sel_to = self._GetSelection()
3681 dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty())
3682
3683 if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space)
3684 or self.IsEmpty() or self.IsDefault()):
3685 # This isn't normally allowed, and so assume we got here by the new
3686 # "tab traversal" behavior, so we need to reset the selection
3687 # and insertion point:
3688 dbg('entire text selected; resetting selection to start of control')
3689 self._goHome()
3690 if self._FindField(0)._selectOnFieldEntry:
3691 edit_start, edit_end = self._FindFieldExtent(self._GetInsertionPoint())
3692 self._SetSelection(edit_start, edit_end)
3693 elif self._fields[0]._insertRight:
3694 self._SetInsertionPoint(self._fields[0]._extent[1])
3695
3696 elif sel_start == 0 and self._GetValue()[0] == '-' and (self._isDec or self._isInt) and self._signOk:
3697 dbg('control is empty; start at beginning after -')
3698 self._SetInsertionPoint(1) ## Move past minus sign space if signed
3699 if self._FindField(0)._selectOnFieldEntry:
3700 edit_start, edit_end = self._FindFieldExtent(self._GetInsertionPoint())
3701 self._SetSelection(1, edit_end)
3702 elif self._fields[0]._insertRight:
3703 self._SetInsertionPoint(self._fields[0]._extent[1])
3704
3705 elif sel_start > self._goEnd(getPosOnly=True):
3706 dbg('cursor beyond the end of the user input; go to end of it')
3707 self._goEnd()
3708 else:
3709 dbg('sel_start, sel_to:', sel_start, sel_to, 'len(self._mask):', len(self._mask))
3710 dbg(indent=0)
3711
3712
3713 def _Keypress(self,key):
3714 """ Method provided to override OnChar routine. Return False to force
3715 a skip of the 'normal' OnChar process. Called before class OnChar.
3716 """
3717 return True
3718
3719
3720 def _LostFocus(self):
3721 """ Method provided for subclasses. _LostFocus() is called after
3722 the class processes its EVT_KILL_FOCUS event code.
3723 """
3724 pass
3725
3726
3727 def _OnDoubleClick(self, event):
3728 """ selects field under cursor on dclick."""
3729 pos = self._GetInsertionPoint()
3730 field = self._FindField(pos)
3731 start, end = field._extent
3732 self._SetInsertionPoint(start)
3733 self._SetSelection(start, end)
3734
3735
3736 def _Change(self):
3737 """ Method provided for subclasses. Called by internal EVT_TEXT
3738 handler. Return False to override the class handler, True otherwise.
3739 """
3740 return True
3741
3742
3743 def _Cut(self):
3744 """
3745 Used to override the default Cut() method in base controls, instead
3746 copying the selection to the clipboard and then blanking the selection,
3747 leaving only the mask in the selected area behind.
3748 Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the
3749 derived control because the mixin functions can't override a method of
3750 a sibling class.
3751 """
3752 dbg("wxMaskedEditMixin::_Cut", indent=1)
3753 value = self._GetValue()
3754 dbg('current value: "%s"' % value)
3755 sel_start, sel_to = self._GetSelection() ## check for a range of selected text
3756 dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
3757 do = wxTextDataObject()
3758 do.SetText(value[sel_start:sel_to].strip())
3759 wxTheClipboard.Open()
3760 wxTheClipboard.SetData(do)
3761 wxTheClipboard.Close()
3762
3763 wxCallAfter(self._SetValue, self._eraseSelection() )
3764 wxCallAfter(self._SetInsertionPoint, sel_start)
3765 dbg(indent=0)
3766
3767
3768 # WS Note: overriding Copy is no longer necessary given that you
3769 # can no longer select beyond the last non-empty char in the control.
3770 #
3771 ## def _Copy( self ):
3772 ## """
3773 ## Override the wxTextCtrl's .Copy function, with our own
3774 ## that does validation. Need to strip trailing spaces.
3775 ## """
3776 ## sel_start, sel_to = self._GetSelection()
3777 ## select_len = sel_to - sel_start
3778 ## textval = wxTextCtrl._GetValue(self)
3779 ##
3780 ## do = wxTextDataObject()
3781 ## do.SetText(textval[sel_start:sel_to].strip())
3782 ## wxTheClipboard.Open()
3783 ## wxTheClipboard.SetData(do)
3784 ## wxTheClipboard.Close()
3785
3786
3787 def _getClipboardContents( self ):
3788 """ Subroutine for getting the current contents of the clipboard.
3789 """
3790 do = wxTextDataObject()
3791 wxTheClipboard.Open()
3792 success = wxTheClipboard.GetData(do)
3793 wxTheClipboard.Close()
3794
3795 if not success:
3796 return None
3797 else:
3798 # Remove leading and trailing spaces before evaluating contents
3799 return do.GetText().strip()
3800
3801
3802 def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False):
3803 """
3804 Used by paste routine and field choice validation to see
3805 if a given slice of paste text is legal for the area in question:
3806 returns validity, replacement text, and extent of paste in
3807 template.
3808 """
3809 dbg(suspend=1)
3810 dbg('wxMaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1)
3811 select_length = sel_to - sel_start
3812 maxlength = select_length
3813 dbg('sel_to - sel_start:', maxlength)
3814 if maxlength == 0:
3815 maxlength = len(self._mask) - sel_start
3816 dbg('maxlength:', maxlength)
3817 length_considered = len(paste_text)
3818 if length_considered > maxlength:
3819 dbg('paste text will not fit into the control:', indent=0)
3820 if raise_on_invalid:
3821 raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
3822 else:
3823 return False, None, None
3824
3825 text = self._template
3826 dbg('length_considered:', length_considered)
3827
3828 valid_paste = True
3829 replacement_text = ""
3830 replace_to = sel_start
3831 i = 0
3832 while valid_paste and i < length_considered and replace_to < len(self._mask):
3833 char = paste_text[i]
3834 field = self._FindField(replace_to)
3835 if field._forceupper: char = char.upper()
3836 elif field._forcelower: char = char.lower()
3837
3838 dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to)
3839 dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to))
3840 if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to):
3841 replacement_text += char
3842 dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals())
3843 dbg("replacement_text:", '"'+replacement_text+'"')
3844 i += 1
3845 replace_to += 1
3846 elif char == self._template[replace_to] or (i == 0 and char == '-' and self._signOk):
3847 replacement_text += char
3848 dbg("'%(char)s' == template(%(replace_to)d)" % locals())
3849 dbg("replacement_text:", '"'+replacement_text+'"')
3850 i += 1
3851 replace_to += 1
3852 else:
3853 next_entry = self._findNextEntry(replace_to, adjustInsert=False)
3854 if next_entry == replace_to:
3855 valid_paste = False
3856 else:
3857 replacement_text += self._template[replace_to:next_entry]
3858 dbg("skipping template; next_entry =", next_entry)
3859 dbg("replacement_text:", '"'+replacement_text+'"')
3860 replace_to = next_entry # so next_entry will be considered on next loop
3861
3862 if not valid_paste and raise_on_invalid:
3863 dbg('raising exception')
3864 raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
3865
3866 elif i < len(paste_text):
3867 valid_paste = False
3868 if raise_on_invalid:
3869 dbg('raising exception')
3870 raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
3871
3872 dbg('valid_paste?', valid_paste)
3873 if valid_paste:
3874 dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
3875 dbg(indent=0, suspend=0)
3876 return valid_paste, replacement_text, replace_to
3877
3878
3879 def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
3880 """
3881 Used to override the base control's .Paste() function,
3882 with our own that does validation.
3883 Note: _Paste must be called from a Paste() override in the
3884 derived control because the mixin functions can't override a
3885 method of a sibling class.
3886 """
3887 dbg('wxMaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
3888 if value is None:
3889 paste_text = self._getClipboardContents()
3890 else:
3891 paste_text = value
3892
3893 if paste_text is not None:
3894 dbg('paste text:', paste_text)
3895 # (conversion will raise ValueError if paste isn't legal)
3896 sel_start, sel_to = self._GetSelection()
3897 try:
3898 valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
3899 except:
3900 dbg('exception thrown', indent=0)
3901 raise
3902
3903 if not valid_paste:
3904 dbg('paste text not legal for the selection or portion of the control following the cursor;')
3905 dbg(indent=0)
3906 return False
3907 # else...
3908 text = self._eraseSelection()
3909
3910 new_text = text[:sel_start] + replacement_text + text[replace_to:]
3911 if new_text:
3912 new_text = string.ljust(new_text,len(self._mask))
3913 dbg("new_text:", '"'+new_text+'"')
3914
3915 if not just_return_value:
3916 if new_text == '':
3917 self.ClearValue()
3918 else:
3919 wxCallAfter(self._SetValue, new_text)
3920 new_pos = sel_start + len(replacement_text)
3921 wxCallAfter(self._SetInsertionPoint, new_pos)
3922 else:
3923 return new_text
3924 elif just_return_value:
3925 return self._GetValue()
3926 dbg(indent=0)
3927
3928
3929
3930 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
3931
3932 class wxMaskedTextCtrl( wxTextCtrl, wxMaskedEditMixin ):
3933 """
3934 This is the primary derivation from wxMaskedEditMixin. It provides
3935 a general masked text control that can be configured with different
3936 masks.
3937 """
3938
3939 def __init__( self, parent, id=-1, value = '',
3940 pos = wxDefaultPosition,
3941 size = wxDefaultSize,
3942 style = wxTE_PROCESS_TAB,
3943 validator=wxDefaultValidator, ## placeholder provided for data-transfer logic
3944 name = 'maskedTextCtrl',
3945 setupEventHandling = True, ## setup event handling by default
3946 **kwargs):
3947
3948 wxTextCtrl.__init__(self, parent, id, value='',
3949 pos=pos, size = size,
3950 style=style, validator=validator,
3951 name=name)
3952
3953 self.controlInitialized = True
3954 wxMaskedEditMixin.__init__( self, name, **kwargs )
3955 self._SetInitialValue(value)
3956
3957 if setupEventHandling:
3958 ## Setup event handlers
3959 EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
3960 EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
3961 EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick
3962 EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
3963 EVT_CHAR( self, self._OnChar ) ## handle each keypress
3964 EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately
3965
3966
3967 def __repr__(self):
3968 return "<wxMaskedTextCtrl: %s>" % self.GetValue()
3969
3970
3971 def _GetSelection(self):
3972 """
3973 Allow mixin to get the text selection of this control.
3974 REQUIRED by any class derived from wxMaskedEditMixin.
3975 """
3976 return self.GetSelection()
3977
3978 def _SetSelection(self, sel_start, sel_to):
3979 """
3980 Allow mixin to set the text selection of this control.
3981 REQUIRED by any class derived from wxMaskedEditMixin.
3982 """
3983 return self.SetSelection( sel_start, sel_to )
3984
3985 def SetSelection(self, sel_start, sel_to):
3986 """
3987 This is just for debugging...
3988 """
3989 dbg("wxMaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
3990 wxTextCtrl.SetSelection(self, sel_start, sel_to)
3991
3992
3993 def _GetInsertionPoint(self):
3994 return self.GetInsertionPoint()
3995
3996 def _SetInsertionPoint(self, pos):
3997 self.SetInsertionPoint(pos)
3998
3999 def SetInsertionPoint(self, pos):
4000 """
4001 This is just for debugging...
4002 """
4003 dbg("wxMaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals())
4004 wxTextCtrl.SetInsertionPoint(self, pos)
4005
4006
4007 def _GetValue(self):
4008 """
4009 Allow mixin to get the raw value of the control with this function.
4010 REQUIRED by any class derived from wxMaskedEditMixin.
4011 """
4012 return self.GetValue()
4013
4014 def _SetValue(self, value):
4015 """
4016 Allow mixin to set the raw value of the control with this function.
4017 REQUIRED by any class derived from wxMaskedEditMixin.
4018 """
4019 dbg('wxMaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
4020 wxTextCtrl.SetValue(self, value)
4021 dbg(indent=0)
4022
4023 def SetValue(self, value):
4024 """
4025 This function redefines the externally accessible .SetValue to be
4026 a smart "paste" of the text in question, so as not to corrupt the
4027 masked control. NOTE: this must be done in the class derived
4028 from the base wx control.
4029 """
4030 dbg('wxMaskedTextCtrl::SetValue = "%s"' % value, indent=1)
4031
4032 # empty previous contents, replacing entire value:
4033 self._SetInsertionPoint(0)
4034 self._SetSelection(0, len(self._mask))
4035
4036 if( len(value) < len(self._mask) # value shorter than control
4037 and (self._isDec or self._isInt) # and it's a numeric control
4038 and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
4039 # try to intelligently "pad out" the value to the right size:
4040 value = self._template[0:len(self._mask) - len(value)] + value
4041 dbg('padded value = "%s"' % value)
4042
4043 # make SetValue behave the same as if you had typed the value in:
4044 try:
4045 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
4046 if self._isDec:
4047 self._isNeg = False # (clear current assumptions)
4048 value = self._adjustDec(value)
4049 elif self._isInt:
4050 self._isNeg = False # (clear current assumptions)
4051 value = self._adjustInt(value)
4052 elif self._isDate and not self.IsValid(value) and self._4digityear:
4053 value = self._adjustDate(value, fixcentury=true)
4054 except ValueError:
4055 # If date, year might be 2 digits vs. 4; try adjusting it:
4056 if self._isDate and self._4digityear:
4057 dateparts = value.split(' ')
4058 dateparts[0] = self._adjustDate(dateparts[0], fixcentury=true)
4059 value = string.join(dateparts, ' ')
4060 dbg('adjusted value: "%s"' % value)
4061 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
4062 else:
4063 raise
4064
4065 self._SetValue(value)
4066 dbg(indent=0)
4067
4068
4069 def _Refresh(self):
4070 """
4071 Allow mixin to refresh the base control with this function.
4072 REQUIRED by any class derived from wxMaskedEditMixin.
4073 """
4074 dbg('wxMaskedTextCtrl::_Refresh', indent=1)
4075 wxTextCtrl.Refresh(self)
4076 dbg(indent=0)
4077
4078
4079 def Refresh(self):
4080 """
4081 This function redefines the externally accessible .Refresh() to
4082 validate the contents of the masked control as it refreshes.
4083 NOTE: this must be done in the class derived from the base wx control.
4084 """
4085 dbg('wxMaskedTextCtrl::Refresh', indent=1)
4086 self._CheckValid()
4087 self._Refresh()
4088 dbg(indent=0)
4089
4090
4091 def _IsEditable(self):
4092 """
4093 Allow mixin to determine if the base control is editable with this function.
4094 REQUIRED by any class derived from wxMaskedEditMixin.
4095 """
4096 return wxTextCtrl.IsEditable(self)
4097
4098
4099 def Cut(self):
4100 """
4101 This function redefines the externally accessible .Cut to be
4102 a smart "erase" of the text in question, so as not to corrupt the
4103 masked control. NOTE: this must be done in the class derived
4104 from the base wx control.
4105 """
4106 self._Cut() # call the mixin's Cut method
4107
4108
4109 def Paste(self):
4110 """
4111 This function redefines the externally accessible .Paste to be
4112 a smart "paste" of the text in question, so as not to corrupt the
4113 masked control. NOTE: this must be done in the class derived
4114 from the base wx control.
4115 """
4116 self._Paste() # call the mixin's Paste method
4117
4118
4119 def IsModified(self):
4120 """
4121 This function overrides the raw wxTextCtrl method, because the
4122 masked edit mixin uses SetValue to change the value, which doesn't
4123 modify the state of this attribute. So, we keep track on each
4124 keystroke to see if the value changes, and if so, it's been
4125 modified.
4126 """
4127 return wxTextCtrl.IsModified(self) or self.modified
4128
4129
4130
4131
4132 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4133 ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
4134 ## events, we have to do it ourselves when we auto-complete.
4135 class wxMaskedComboBoxSelectEvent(wxPyCommandEvent):
4136 def __init__(self, id, selection = 0, object=None):
4137 wxPyCommandEvent.__init__(self, wxEVT_COMMAND_COMBOBOX_SELECTED, id)
4138
4139 self.__selection = selection
4140 self.SetEventObject(object)
4141
4142 def GetSelection(self):
4143 """Retrieve the value of the control at the time
4144 this event was generated."""
4145 return self.__selection
4146
4147
4148 class wxMaskedComboBox( wxComboBox, wxMaskedEditMixin ):
4149 """
4150 This masked edit control adds the ability to use a masked input
4151 on a combobox, and do auto-complete of such values.
4152 """
4153 def __init__( self, parent, id=-1, value = '',
4154 pos = wxDefaultPosition,
4155 size = wxDefaultSize,
4156 choices = [],
4157 style = wxCB_DROPDOWN,
4158 validator = wxDefaultValidator,
4159 name = "maskedComboBox",
4160 setupEventHandling = True, ## setup event handling by default):
4161 **kwargs):
4162
4163
4164 # This is necessary, because wxComboBox currently provides no
4165 # method for determining later if this was specified in the
4166 # constructor for the control...
4167 self.__readonly = style & wxCB_READONLY == wxCB_READONLY
4168
4169 kwargs['choices'] = choices ## set up maskededit to work with choice list too
4170
4171 ## Since combobox completion is case-insensitive, always validate same way
4172 if not kwargs.has_key('compareNoCase'):
4173 kwargs['compareNoCase'] = True
4174
4175 wxMaskedEditMixin.__init__( self, name, **kwargs )
4176 self._choices = self._ctrl_constraints._choices
4177 dbg('self._choices:', self._choices)
4178
4179 if self._ctrl_constraints._alignRight:
4180 choices = [choice.rjust(len(self._mask)) for choice in choices]
4181 else:
4182 choices = [choice.ljust(len(self._mask)) for choice in choices]
4183
4184 wxComboBox.__init__(self, parent, id, value='',
4185 pos=pos, size = size,
4186 choices=choices, style=style|wxWANTS_CHARS,
4187 validator=validator,
4188 name=name)
4189
4190 self.controlInitialized = True
4191
4192 # Set control font - fixed width by default
4193 self._setFont()
4194
4195 if self._autofit:
4196 self.SetClientSize(self.calcSize())
4197
4198 if value:
4199 # ensure value is width of the mask of the control:
4200 if self._ctrl_constraints._alignRight:
4201 value = value.rjust(len(self._mask))
4202 else:
4203 value = value.ljust(len(self._mask))
4204
4205 if self.__readonly:
4206 self.SetStringSelection(value)
4207 else:
4208 self._SetInitialValue(value)
4209
4210
4211 self._SetKeycodeHandler(WXK_UP, self.OnSelectChoice)
4212 self._SetKeycodeHandler(WXK_DOWN, self.OnSelectChoice)
4213
4214 if setupEventHandling:
4215 ## Setup event handlers
4216 EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
4217 EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
4218 EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick
4219 EVT_CHAR( self, self._OnChar ) ## handle each keypress
4220 EVT_KEY_DOWN( self, self.OnKeyDown ) ## for special processing of up/down keys
4221 EVT_KEY_DOWN( self, self._OnKeyDown ) ## for processing the rest of the control keys
4222 ## (next in evt chain)
4223 EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately
4224
4225
4226 def __repr__(self):
4227 return "<wxMaskedComboBox: %s>" % self.GetValue()
4228
4229
4230 def calcSize(self, size=None):
4231 """
4232 Calculate automatic size if allowed; override base mixin function
4233 to account for the selector button.
4234 """
4235 size = self._calcSize(size)
4236 return (size[0]+20, size[1])
4237
4238
4239 def _GetSelection(self):
4240 """
4241 Allow mixin to get the text selection of this control.
4242 REQUIRED by any class derived from wxMaskedEditMixin.
4243 """
4244 return self.GetMark()
4245
4246 def _SetSelection(self, sel_start, sel_to):
4247 """
4248 Allow mixin to set the text selection of this control.
4249 REQUIRED by any class derived from wxMaskedEditMixin.
4250 """
4251 return self.SetMark( sel_start, sel_to )
4252
4253
4254 def _GetInsertionPoint(self):
4255 return self.GetInsertionPoint()
4256
4257 def _SetInsertionPoint(self, pos):
4258 self.SetInsertionPoint(pos)
4259
4260
4261 def _GetValue(self):
4262 """
4263 Allow mixin to get the raw value of the control with this function.
4264 REQUIRED by any class derived from wxMaskedEditMixin.
4265 """
4266 return self.GetValue()
4267
4268 def _SetValue(self, value):
4269 """
4270 Allow mixin to set the raw value of the control with this function.
4271 REQUIRED by any class derived from wxMaskedEditMixin.
4272 """
4273 # For wxComboBox, ensure that values are properly padded so that
4274 # if varying length choices are supplied, they always show up
4275 # in the window properly, and will be the appropriate length
4276 # to match the mask:
4277 if self._ctrl_constraints._alignRight:
4278 value = value.rjust(len(self._mask))
4279 else:
4280 value = value.ljust(len(self._mask))
4281 wxComboBox.SetValue(self, value)
4282
4283 def SetValue(self, value):
4284 """
4285 This function redefines the externally accessible .SetValue to be
4286 a smart "paste" of the text in question, so as not to corrupt the
4287 masked control. NOTE: this must be done in the class derived
4288 from the base wx control.
4289 """
4290
4291 # empty previous contents, replacing entire value:
4292 self._SetInsertionPoint(0)
4293 self._SetSelection(0, len(self._mask))
4294
4295 if( len(value) < len(self._mask) # value shorter than control
4296 and (self._isDec or self._isInt) # and it's a numeric control
4297 and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
4298 # try to intelligently "pad out" the value to the right size:
4299 value = self._template[0:len(self._mask) - len(value)] + value
4300 dbg('padded value = "%s"' % value)
4301
4302 # make SetValue behave the same as if you had typed the value in:
4303 try:
4304 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
4305 if self._isDec:
4306 self._isNeg = False # (clear current assumptions)
4307 value = self._adjustDec(value)
4308 elif self._isInt:
4309 self._isNeg = False # (clear current assumptions)
4310 value = self._adjustInt(value)
4311 elif self._isDate and not self.IsValid(value) and self._4digityear:
4312 value = self._adjustDate(value, fixcentury=true)
4313 except ValueError:
4314 # If date, year might be 2 digits vs. 4; try adjusting it:
4315 if self._isDate and self._4digityear:
4316 dateparts = value.split(' ')
4317 dateparts[0] = self._adjustDate(dateparts[0], fixcentury=true)
4318 value = string.join(dateparts, ' ')
4319 dbg('adjusted value: "%s"' % value)
4320 value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
4321 else:
4322 raise
4323
4324 self._SetValue(value)
4325
4326 def _Refresh(self):
4327 """
4328 Allow mixin to refresh the base control with this function.
4329 REQUIRED by any class derived from wxMaskedEditMixin.
4330 """
4331 wxComboBox.Refresh(self)
4332
4333 def Refresh(self):
4334 """
4335 This function redefines the externally accessible .Refresh() to
4336 validate the contents of the masked control as it refreshes.
4337 NOTE: this must be done in the class derived from the base wx control.
4338 """
4339 self._CheckValid()
4340 self._Refresh()
4341
4342
4343 def _IsEditable(self):
4344 """
4345 Allow mixin to determine if the base control is editable with this function.
4346 REQUIRED by any class derived from wxMaskedEditMixin.
4347 """
4348 return not self.__readonly
4349
4350
4351 def Cut(self):
4352 """
4353 This function redefines the externally accessible .Cut to be
4354 a smart "erase" of the text in question, so as not to corrupt the
4355 masked control. NOTE: this must be done in the class derived
4356 from the base wx control.
4357 """
4358 self._Cut() # call the mixin's Cut method
4359
4360
4361 def Paste(self):
4362 """
4363 This function redefines the externally accessible .Paste to be
4364 a smart "paste" of the text in question, so as not to corrupt the
4365 masked control. NOTE: this must be done in the class derived
4366 from the base wx control.
4367 """
4368 self._Paste() # call the mixin's Paste method
4369
4370
4371 def Append( self, choice ):
4372 """
4373 This function override is necessary so we can keep track of any additions to the list
4374 of choices, because wxComboBox doesn't have an accessor for the choice list.
4375 """
4376 if self._ctrl_constraints._alignRight:
4377 choice = choice.rjust(len(self._mask))
4378 else:
4379 choice = choice.ljust(len(self._mask))
4380
4381 if self._ctrl_constraints._choiceRequired:
4382 choice = choice.lower().strip()
4383 self._choices.append(choice)
4384
4385 if not self.IsValid(choice):
4386 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (self.name, choice))
4387
4388
4389 wxComboBox.Append(self, choice)
4390
4391
4392 def Clear( self ):
4393 """
4394 This function override is necessary so we can keep track of any additions to the list
4395 of choices, because wxComboBox doesn't have an accessor for the choice list.
4396 """
4397 self._choices = []
4398 if self._ctrl_constraints._choices:
4399 self.SetCtrlParameters(choices=[])
4400 wxComboBox.Clear(self)
4401
4402
4403 def GetMark(self):
4404 """
4405 This function is a hack to make up for the fact that wxComboBox has no
4406 method for returning the selected portion of its edit control. It
4407 works, but has the nasty side effect of generating lots of intermediate
4408 events.
4409 """
4410 dbg(suspend=1) # turn off debugging around this function
4411 dbg('wxMaskedComboBox::GetMark', indent=1)
4412 sel_start = sel_to = self.GetInsertionPoint()
4413 dbg("current sel_start:", sel_start)
4414 value = self.GetValue()
4415 dbg('value: "%s"' % value)
4416
4417 self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
4418
4419 wxComboBox.Cut(self)
4420 newvalue = self.GetValue()
4421 dbg("value after Cut operation:", newvalue)
4422
4423 if newvalue != value: # something was selected; calculate extent
4424 dbg("something selected")
4425 sel_to = sel_start + len(value) - len(newvalue)
4426 wxComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
4427 wxComboBox.SetInsertionPoint(self, sel_start)
4428 wxComboBox.SetMark(self, sel_start, sel_to)
4429
4430 self._ignoreChange = False # tell _OnTextChange() to pay attn again
4431
4432 dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
4433 return sel_start, sel_to
4434
4435
4436 def OnKeyDown(self, event):
4437 """
4438 This function is necessary because navigation and control key
4439 events do not seem to normally be seen by the wxComboBox's
4440 EVT_CHAR routine. (Tabs don't seem to be visible no matter
4441 what... {:-( )
4442 """
4443 if event.GetKeyCode() in self._nav + self._control:
4444 self._OnChar(event)
4445 return
4446 else:
4447 event.Skip() # let mixin default KeyDown behavior occur
4448
4449
4450 def OnSelectChoice(self, event):
4451 """
4452 This function appears to be necessary, because the processing done
4453 on the text of the control somehow interferes with the combobox's
4454 selection mechanism for the arrow keys.
4455 """
4456 dbg('wxMaskedComboBox::OnSelectChoice', indent=1)
4457
4458 # force case-insensitive comparison for matching purposes:
4459 value = self.GetValue().lower().strip()
4460 if event.GetKeyCode() == WXK_UP:
4461 direction = -1
4462 else:
4463 direction = 1
4464 match_index = self._autoComplete(direction, self._choices, value, self._ctrl_constraints._compareNoCase)
4465 if match_index is not None:
4466 dbg('setting selection to', match_index)
4467 self.SetSelection(match_index)
4468 # issue appropriate event to outside:
4469 self.GetEventHandler().ProcessEvent(
4470 wxMaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
4471 self._CheckValid()
4472 keep_processing = False
4473 else:
4474 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
4475 field = self._FindField(pos)
4476 if self.IsEmpty() or not field._hasList:
4477 dbg('selecting 1st value in list')
4478 self.SetSelection(0)
4479 self.GetEventHandler().ProcessEvent(
4480 wxMaskedComboBoxSelectEvent( self.GetId(), 0, self ) )
4481 self._CheckValid()
4482 keep_processing = False
4483 else:
4484 # attempt field-level auto-complete
4485 dbg(indent=0)
4486 keep_processing = self._OnAutoCompleteField(event)
4487 dbg(indent=0)
4488 return keep_processing
4489
4490
4491 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4492
4493 class wxIpAddrCtrl( wxMaskedTextCtrl ):
4494 """
4495 This class is a particular type of wxMaskedTextCtrl that accepts
4496 and understands the semantics of IP addresses, reformats input
4497 as you move from field to field, and accepts '.' as a navigation
4498 character, so that typing an IP address can be done naturally.
4499 """
4500 def __init__( self, parent, id=-1, value = '',
4501 pos = wxDefaultPosition,
4502 size = wxDefaultSize,
4503 style = wxTE_PROCESS_TAB,
4504 validator = wxDefaultValidator,
4505 name = 'wxIpAddrCtrl',
4506 setupEventHandling = True, ## setup event handling by default
4507 **kwargs):
4508
4509 if not kwargs.has_key('mask'):
4510 kwargs['mask'] = mask = "###.###.###.###"
4511 if not kwargs.has_key('formatcodes'):
4512 kwargs['formatcodes'] = 'F_Sr<'
4513 if not kwargs.has_key('validRegex'):
4514 kwargs['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}"
4515
4516 if not kwargs.has_key('emptyInvalid'):
4517 kwargs['emptyInvalid'] = True
4518
4519 wxMaskedTextCtrl.__init__(
4520 self, parent, id=id, value = value,
4521 pos=pos, size=size,
4522 style = style,
4523 validator = validator,
4524 name = name,
4525 setupEventHandling = setupEventHandling,
4526 **kwargs)
4527
4528 field_params = {}
4529 if not kwargs.has_key('validRequired'):
4530 field_params['validRequired'] = True
4531
4532 field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))"
4533
4534 # require "valid" string; this prevents entry of any value > 255, but allows
4535 # intermediate constructions; overall control validation requires well-formatted value.
4536 field_params['formatcodes'] = 'V'
4537
4538 if field_params:
4539 for i in self._field_indices:
4540 self.SetFieldParameters(i, **field_params)
4541
4542 # This makes '.' act like tab:
4543 self._AddNavKey('.', handler=self.OnDot)
4544 self._AddNavKey('>', handler=self.OnDot) # for "shift-."
4545
4546
4547 def OnDot(self, event):
4548 dbg('wxIpAddrCtrl::OnDot', indent=1)
4549 pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
4550 oldvalue = self.GetValue()
4551 edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
4552 if not event.ShiftDown():
4553 if pos < edit_end:
4554 # clip data in field to the right of pos, if adjusting fields
4555 # when not at delimeter; (assumption == they hit '.')
4556 newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:]
4557 self._SetValue(newvalue)
4558 self._SetInsertionPoint(pos)
4559 dbg(indent=0)
4560 return self._OnChangeField(event)
4561
4562
4563
4564 def GetAddress(self):
4565 value = wxMaskedTextCtrl.GetValue(self)
4566 return value.replace(' ','') # remove spaces from the value
4567
4568
4569 def _OnCtrl_S(self, event):
4570 dbg("wxIpAddrCtrl::_OnCtrl_S")
4571 if self._demo:
4572 print "value:", self.GetAddress()
4573 return False
4574
4575 def SetValue(self, value):
4576 dbg('wxIpAddrCtrl::SetValue(%s)' % str(value), indent=1)
4577 if type(value) != types.StringType:
4578 raise ValueError('%s must be a string', str(value))
4579
4580 bValid = True # assume true
4581 parts = value.split('.')
4582 if len(parts) != 4:
4583 bValid = False
4584 else:
4585 for i in range(4):
4586 part = parts[i]
4587 if not 0 <= len(part) <= 3:
4588 bValid = False
4589 break
4590 elif part.strip(): # non-empty part
4591 try:
4592 j = string.atoi(part)
4593 if not 0 <= j <= 255:
4594 bValid = False
4595 break
4596 else:
4597 parts[i] = '%3d' % j
4598 except:
4599 bValid = False
4600 break
4601 else:
4602 # allow empty sections for SetValue (will result in "invalid" value,
4603 # but this may be useful for initializing the control:
4604 parts[i] = ' ' # convert empty field to 3-char length
4605
4606 if not bValid:
4607 dbg(indent=0)
4608 raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value))
4609 else:
4610 dbg('parts:', parts)
4611 value = string.join(parts, '.')
4612 wxMaskedTextCtrl.SetValue(self, value)
4613 dbg(indent=0)
4614
4615
4616 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4617 ## these are helper subroutines:
4618
4619 def movetodec( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '):
4620 """ addseparators = add separator character every three numerals if True
4621 """
4622 fmt0 = fmtstring.split('.')
4623 fmt1 = fmt0[0]
4624 fmt2 = fmt0[1]
4625 val = origvalue.split('.')[0].strip()
4626 ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2)
4627 if neg:
4628 ret = '-' + ret[1:]
4629 return (ret,len(fmt1))
4630
4631
4632 def isDateType( fmtstring ):
4633 """ Checks the mask and returns True if it fits an allowed
4634 date or datetime format.
4635 """
4636 dateMasks = ("^##/##/####",
4637 "^##-##-####",
4638 "^##.##.####",
4639 "^####/##/##",
4640 "^####-##-##",
4641 "^####.##.##",
4642 "^##/CCC/####",
4643 "^##.CCC.####",
4644 "^##/##/##$",
4645 "^##/##/## ",
4646 "^##/CCC/##$",
4647 "^##.CCC.## ",)
4648 reString = "|".join(dateMasks)
4649 filter = re.compile( reString)
4650 if re.match(filter,fmtstring): return True
4651 return False
4652
4653 def isTimeType( fmtstring ):
4654 """ Checks the mask and returns True if it fits an allowed
4655 time format.
4656 """
4657 reTimeMask = "^##:##(:##)?( (AM|PM))?"
4658 filter = re.compile( reTimeMask )
4659 if re.match(filter,fmtstring): return True
4660 return False
4661
4662
4663 def isDecimal( fmtstring, decimalchar ):
4664 filter = re.compile("[ ]?[#]+\%c[#]+\n" % decimalchar)
4665 if re.match(filter,fmtstring+"\n"): return True
4666 return False
4667
4668
4669 def isInteger( fmtstring ):
4670 filter = re.compile("[#]+\n")
4671 if re.match(filter,fmtstring+"\n"): return True
4672 return False
4673
4674
4675 def getDateParts( dateStr, dateFmt ):
4676 if len(dateStr) > 11: clip = dateStr[0:11]
4677 else: clip = dateStr
4678 if clip[-2] not in string.digits:
4679 clip = clip[:-1] # (got part of time; drop it)
4680
4681 dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
4682 slices = clip.split(dateSep)
4683 if dateFmt == "MDY":
4684 y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts
4685 elif dateFmt == "DMY":
4686 y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts
4687 elif dateFmt == "YMD":
4688 y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts
4689 else:
4690 y,m,d = None, None, None
4691 if not y:
4692 return None
4693 else:
4694 return y,m,d
4695
4696
4697 def getDateSepChar(dateStr):
4698 clip = dateStr[0:10]
4699 dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
4700 return dateSep
4701
4702
4703 def makeDate( year, month, day, dateFmt, dateStr):
4704 sep = getDateSepChar( dateStr)
4705 if dateFmt == "MDY":
4706 return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts
4707 elif dateFmt == "DMY":
4708 return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts
4709 elif dateFmt == "YMD":
4710 return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts
4711 else:
4712 return none
4713
4714
4715 def getYear(dateStr,dateFmt):
4716 parts = getDateParts( dateStr, dateFmt)
4717 return parts[0]
4718
4719 def getMonth(dateStr,dateFmt):
4720 parts = getDateParts( dateStr, dateFmt)
4721 return parts[1]
4722
4723 def getDay(dateStr,dateFmt):
4724 parts = getDateParts( dateStr, dateFmt)
4725 return parts[2]
4726
4727 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4728 class test(wxPySimpleApp):
4729 def OnInit(self):
4730 from wxPython.lib.rcsizer import RowColSizer
4731 self.frame = wxFrame( NULL, -1, "wxMaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
4732 self.panel = wxPanel( self.frame, -1)
4733 self.sizer = RowColSizer()
4734 self.labels = []
4735 self.editList = []
4736 rowcount = 4
4737
4738 id, id1 = wxNewId(), wxNewId()
4739 self.command1 = wxButton( self.panel, id, "&Close" )
4740 self.command2 = wxButton( self.panel, id1, "&AutoFormats" )
4741 self.sizer.Add(self.command1, row=0, col=0, flag=wxALL, border = 5)
4742 self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wxALL, border = 5)
4743 EVT_BUTTON( self.panel, id, self.onClick )
4744 ## self.panel.SetDefaultItem(self.command1 )
4745 EVT_BUTTON( self.panel, id1, self.onClickPage )
4746
4747 self.check1 = wxCheckBox( self.panel, -1, "Disallow Empty" )
4748 self.check2 = wxCheckBox( self.panel, -1, "Highlight Empty" )
4749 self.sizer.Add( self.check1, row=0,col=3, flag=wxALL,border=5 )
4750 self.sizer.Add( self.check2, row=0,col=4, flag=wxALL,border=5 )
4751 EVT_CHECKBOX( self.panel, self.check1.GetId(), self._onCheck1 )
4752 EVT_CHECKBOX( self.panel, self.check2.GetId(), self._onCheck2 )
4753
4754
4755 label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field.
4756 Note that all controls have been auto-sized by including F in the format code.
4757 Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status)."""
4758 label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)."
4759
4760 self.label1 = wxStaticText( self.panel, -1, label)
4761 self.label2 = wxStaticText( self.panel, -1, "Description")
4762 self.label3 = wxStaticText( self.panel, -1, "Mask Value")
4763 self.label4 = wxStaticText( self.panel, -1, "Format")
4764 self.label5 = wxStaticText( self.panel, -1, "Reg Expr Val. (opt)")
4765 self.label6 = wxStaticText( self.panel, -1, "wxMaskedEdit Ctrl")
4766 self.label7 = wxStaticText( self.panel, -1, label2)
4767 self.label7.SetForegroundColour("Blue")
4768 self.label1.SetForegroundColour("Blue")
4769 self.label2.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4770 self.label3.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4771 self.label4.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4772 self.label5.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4773 self.label6.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4774
4775 self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wxALL,border=5)
4776 self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wxALL,border=5)
4777 self.sizer.Add( self.label2, row=3,col=0, flag=wxALL,border=5)
4778 self.sizer.Add( self.label3, row=3,col=1, flag=wxALL,border=5)
4779 self.sizer.Add( self.label4, row=3,col=2, flag=wxALL,border=5)
4780 self.sizer.Add( self.label5, row=3,col=3, flag=wxALL,border=5)
4781 self.sizer.Add( self.label6, row=3,col=4, flag=wxALL,border=5)
4782
4783 # The following list is of the controls for the demo. Feel free to play around with
4784 # the options!
4785 controls = [
4786 #description mask excl format regexp range,list,initial
4787 ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''),
4788 ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''),
4789 ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''),
4790 ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''),
4791 ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''),
4792 ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'),
4793 ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''),
4794 ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'),
4795 ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''),
4796 ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '),
4797 ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '),
4798 ("Month selector", "XXX", "", 'F', "", (),
4799 ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""),
4800 ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (),
4801 ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "")
4802 ]
4803
4804 for control in controls:
4805 self.sizer.Add( wxStaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wxALL)
4806 self.sizer.Add( wxStaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wxALL)
4807 self.sizer.Add( wxStaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wxALL)
4808 self.sizer.Add( wxStaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wxALL)
4809
4810 if control in controls[:]:#-2]:
4811 newControl = wxMaskedTextCtrl( self.panel, -1, "",
4812 mask = control[1],
4813 excludeChars = control[2],
4814 formatcodes = control[3],
4815 includeChars = "",
4816 validRegex = control[4],
4817 validRange = control[5],
4818 choices = control[6],
4819 defaultValue = control[7],
4820 demo = True)
4821 if control[6]: newControl.SetCtrlParameters(choiceRequired = True)
4822 else:
4823 newControl = wxMaskedComboBox( self.panel, -1, "",
4824 choices = control[7],
4825 choiceRequired = True,
4826 mask = control[1],
4827 formatcodes = control[3],
4828 excludeChars = control[2],
4829 includeChars = "",
4830 validRegex = control[4],
4831 validRange = control[5],
4832 demo = True)
4833 self.editList.append( newControl )
4834
4835 self.sizer.Add( newControl, row=rowcount,col=4,flag=wxALL,border=5)
4836 rowcount += 1
4837
4838 self.sizer.AddGrowableCol(4)
4839
4840 self.panel.SetSizer(self.sizer)
4841 self.panel.SetAutoLayout(1)
4842
4843 self.frame.Show(1)
4844 self.MainLoop()
4845
4846 return True
4847
4848 def onClick(self, event):
4849 self.frame.Close()
4850
4851 def onClickPage(self, event):
4852 self.page2 = test2(self.frame,-1,"")
4853 self.page2.Show(True)
4854
4855 def _onCheck1(self,event):
4856 """ Set required value on/off """
4857 value = event.Checked()
4858 if value:
4859 for control in self.editList:
4860 control.SetCtrlParameters(emptyInvalid=True)
4861 control.Refresh()
4862 else:
4863 for control in self.editList:
4864 control.SetCtrlParameters(emptyInvalid=False)
4865 control.Refresh()
4866 self.panel.Refresh()
4867
4868 def _onCheck2(self,event):
4869 """ Highlight empty values"""
4870 value = event.Checked()
4871 if value:
4872 for control in self.editList:
4873 control.SetCtrlParameters( emptyBackgroundColor = 'Aquamarine')
4874 control.Refresh()
4875 else:
4876 for control in self.editList:
4877 control.SetCtrlParameters( emptyBackgroundColor = 'White')
4878 control.Refresh()
4879 self.panel.Refresh()
4880
4881
4882 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4883
4884 class test2(wxFrame):
4885 def __init__(self, parent, id, caption):
4886 wxFrame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
4887 from wxPython.lib.rcsizer import RowColSizer
4888 self.panel = wxPanel( self, -1)
4889 self.sizer = RowColSizer()
4890 self.labels = []
4891 self.texts = []
4892 rowcount = 4
4893
4894 label = """\
4895 All these controls have been created by passing a single parameter, the AutoFormat code.
4896 The class contains an internal dictionary of types and formats (autoformats).
4897 To see a great example of validations in action, try entering a bad email address, then tab out."""
4898
4899 self.label1 = wxStaticText( self.panel, -1, label)
4900 self.label2 = wxStaticText( self.panel, -1, "Description")
4901 self.label3 = wxStaticText( self.panel, -1, "AutoFormat Code")
4902 self.label4 = wxStaticText( self.panel, -1, "wxMaskedEdit Control")
4903 self.label1.SetForegroundColour("Blue")
4904 self.label2.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4905 self.label3.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4906 self.label4.SetFont(wxFont(9,wxSWISS,wxNORMAL,wxBOLD))
4907
4908 self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wxALL,border=5)
4909 self.sizer.Add( self.label2, row=3,col=0, flag=wxALL,border=5)
4910 self.sizer.Add( self.label3, row=3,col=1, flag=wxALL,border=5)
4911 self.sizer.Add( self.label4, row=3,col=2, flag=wxALL,border=5)
4912
4913 id, id1 = wxNewId(), wxNewId()
4914 self.command1 = wxButton( self.panel, id, "&Close")
4915 self.command2 = wxButton( self.panel, id1, "&Print Formats")
4916 EVT_BUTTON( self.panel, id, self.onClick)
4917 self.panel.SetDefaultItem(self.command1)
4918 EVT_BUTTON( self.panel, id1, self.onClickPrint)
4919
4920 # The following list is of the controls for the demo. Feel free to play around with
4921 # the options!
4922 controls = [
4923 ("Phone No","USPHONEFULLEXT"),
4924 ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"),
4925 ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
4926 ("Time (with seconds)","TIMEHHMMSS"),
4927 ("Military Time\n(without seconds)","MILTIMEHHMM"),
4928 ("Social Sec#","USSOCIALSEC"),
4929 ("Credit Card","CREDITCARD"),
4930 ("Expiration MM/YY","EXPDATEMMYY"),
4931 ("Percentage","PERCENT"),
4932 ("Person's Age","AGE"),
4933 ("US Zip Code","USZIP"),
4934 ("US Zip+4","USZIPPLUS4"),
4935 ("Email Address","EMAIL"),
4936 ("IP Address", "(derived control wxIpAddrCtrl)")
4937 ]
4938
4939 for control in controls:
4940 self.sizer.Add( wxStaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wxALL)
4941 self.sizer.Add( wxStaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wxALL)
4942 if control in controls[:-1]:
4943 self.sizer.Add( wxMaskedTextCtrl( self.panel, -1, "",
4944 autoformat = control[1],
4945 demo = True),
4946 row=rowcount,col=2,flag=wxALL,border=5)
4947 else:
4948 self.sizer.Add( wxIpAddrCtrl( self.panel, -1, "", demo=True ),
4949 row=rowcount,col=2,flag=wxALL,border=5)
4950 rowcount += 1
4951
4952 self.sizer.Add(self.command1, row=0, col=0, flag=wxALL, border = 5)
4953 self.sizer.Add(self.command2, row=0, col=1, flag=wxALL, border = 5)
4954 self.sizer.AddGrowableCol(3)
4955
4956 self.panel.SetSizer(self.sizer)
4957 self.panel.SetAutoLayout(1)
4958
4959 def onClick(self, event):
4960 self.Close()
4961
4962 def onClickPrint(self, event):
4963 for format in masktags.keys():
4964 sep = "+------------------------+"
4965 print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format][0], masktags[format][3])
4966
4967 ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
4968
4969 if __name__ == "__main__":
4970 app = test()
4971
4972 i=1
4973 ##
4974 ## Current Issues:
4975 ## ===================================
4976 ##
4977 ## 1. WS: For some reason I don't understand, the control is generating two (2)
4978 ## EVT_TEXT events for every one (1) .SetValue() of the underlying control.
4979 ## I've been unsuccessful in determining why or in my efforts to make just one
4980 ## occur. So, I've added a hack to save the last seen value from the
4981 ## control in the EVT_TEXT handler, and if *different*, call event.Skip()
4982 ## to propagate it down the event chain, and let the application see it.
4983 ##
4984 ## 2. WS: wxMaskedComboBox is deficient in several areas, all having to do with the
4985 ## behavior of the underlying control that I can't fix. The problems are:
4986 ## a) The background coloring doesn't work in the text field of the control;
4987 ## instead, there's a only border around it that assumes the correct color.
4988 ## b) The control will not pass WXK_TAB to the event handler, no matter what
4989 ## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to
4990 ## indicate that we want these events. As a result, wxMaskedComboBox
4991 ## doesn't do the nice field-tabbing that wxMaskedTextCtrl does.
4992 ## c) Auto-complete had to be reimplemented for the control because programmatic
4993 ## setting of the value of the text field does not set up the auto complete
4994 ## the way that the control processing keystrokes does. (But I think I've
4995 ## implemented a fairly decent approximation.) Because of this the control
4996 ## also won't auto-complete on dropdown, and there's no event I can catch
4997 ## to work around this problem.
4998 ## d) There is no method provided for getting the selection; the hack I've
4999 ## implemented has its flaws, not the least of which is that due to the
5000 ## strategy that I'm using, the paste buffer is always replaced by the
5001 ## contents of the control's selection when in focus, on each keystroke;
5002 ## this makes it impossible to paste anything into a wxMaskedComboBox
5003 ## at the moment... :-(
5004 ## e) The other deficient behavior, likely induced by the workaround for (d),
5005 ## is that you can can't shift-left to select more than one character
5006 ## at a time.
5007 ##
5008 ##
5009 ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their
5010 ## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in
5011 ## wxWindows, shift-tab won't take you backwards through the fields of
5012 ## a wxMaskedTextCtrl like it should. Until then Shifted arrow keys will
5013 ## work like shift-tab and tab ought to.
5014 ##
5015
5016 ## To-Do's:
5017 ## =============================##
5018 ## 1. Add Popup list for auto-completable fields that simulates combobox on individual
5019 ## fields. Example: City validates against list of cities, or zip vs zip code list.
5020 ## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal"
5021 ## control.
5022 ## 3. Fix shift-left selection for wxMaskedComboBox.
5023 ## 5. Transform notion of "decimal control" to be less "entire control"-centric,
5024 ## so that monetary symbols can be included and still have the appropriate
5025 ## semantics. (Big job, as currently written, but would make control even
5026 ## more useful for business applications.)
5027
5028
5029 ## CHANGELOG:
5030 ## ====================
5031 ## Version 1.3
5032 ## 1. Made it possible to configure grouping, decimal and shift-decimal characters,
5033 ## to make controls more usable internationally.
5034 ## 2. Added code to smart "adjust" value strings presented to .SetValue()
5035 ## for right-aligned numeric format controls if they are shorter than
5036 ## than the control width, prepending the missing portion, prepending control
5037 ## template left substring for the missing characters, so that setting
5038 ## numeric values is easier.
5039 ## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved
5040 ## for b-c), as this makes more sense.
5041 ##
5042 ## Version 1.2
5043 ## 1. Fixed .SetValue() to replace the current value, rather than the current
5044 ## selection. Also changed it to generate ValueError if presented with
5045 ## either a value which doesn't follow the format or won't fit. Also made
5046 ## set value adjust numeric and date controls as if user entered the value.
5047 ## Expanded doc explaining how SetValue() works.
5048 ## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
5049 ## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
5050 ## 3. Made all date autoformats automatically pick implied "datestyle".
5051 ## 4. Added IsModified override, since base wxTextCtrl never reports modified if
5052 ## .SetValue used to change the value, which is what the masked edit controls
5053 ## use internally.
5054 ## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when
5055 ## using tab to "leave field" and auto-adjust.
5056 ## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes,
5057 ## and bug in ._Paste() that didn't account for signs in signed masks either.
5058 ## 7. Fixed issues with _adjustPos for right-insert fields causing improper
5059 ## selection/replacement of values
5060 ## 8. Fixed _OnHome handler to properly handle extending current selection to
5061 ## beginning of control.
5062 ## 9. Exposed all (valid) autoformats to demo, binding descriptions to
5063 ## autoformats.
5064 ## 10. Fixed a couple of bugs in email regexp.
5065 ## 11. Made maskchardict an instance var, to make mask chars to be more
5066 ## amenable to international use.
5067 ## 12. Clarified meaning of '-' formatcode in doc.
5068 ## 13. Fixed a couple of coding bugs being flagged by Python2.1.
5069 ## 14. Fixed several issues with sign positioning, erasure and validity
5070 ## checking for "numeric" masked controls.
5071 ## 15. Added validation to wxIpAddrCtrl.SetValue().
5072 ##
5073 ## Version 1.1
5074 ## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default)
5075 ## vs. literal font facename, and use wxTELETYPE as the font family
5076 ## if so specified.
5077 ## 2. Switched to use of dbg module vs. locally defined version.
5078 ## 3. Revamped entire control structure to use Field classes to hold constraint
5079 ## and formatting data, to make code more hierarchical, allow for more
5080 ## sophisticated masked edit construction.
5081 ## 4. Better strategy for managing options, and better validation on keywords.
5082 ## 5. Added 'V' format code, which requires that in order for a character
5083 ## to be accepted, it must result in a string that passes the validRegex.
5084 ## 6. Added 'S' format code which means "select entire field when navigating
5085 ## to new field."
5086 ## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment)
5087 ## 8. Added '<' format code to allow fields to require explicit cursor movement
5088 ## to leave field.
5089 ## 9. Added validFunc option to other validation mechanisms, that allows derived
5090 ## classes to add dynamic validation constraints to the control.
5091 ## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also
5092 ## fixed failure to obey case conversion codes when pasting.
5093 ## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere...
5094 ## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.'
5095 ## 13. Enhanced wxIpAddrCtrl to use right-insert fields, selection on field traversal,
5096 ## individual field validation to prevent field values > 255, and require explicit
5097 ## tab/. to change fields.
5098 ## 14. Added handler for left double-click to select field under cursor.
5099 ## 15. Fixed handling for "Read-only" styles.
5100 ## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor
5101 ## attribute, for more consistent and controllable coloring.
5102 ## 17. Added retainFieldValidation parameter, allowing top-level constraints
5103 ## such as "validRequired" to be set independently of field-level equivalent.
5104 ## (needed in wxTimeCtrl for bounds constraints.)
5105 ## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed
5106 ## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue,
5107 ## etc.
5108 ## 19. Fixed maskchar setting for upper/lowercase, to work in all locales.
5109 ##
5110 ##
5111 ## Version 1.0
5112 ## 1. Decimal point behavior restored for decimal and integer type controls:
5113 ## decimal point now trucates the portion > 0.
5114 ## 2. Return key now works like the tab character and moves to the next field,
5115 ## provided no default button is set for the form panel on which the control
5116 ## resides.
5117 ## 3. Support added in _FindField() for subclasses controls (like timecontrol)
5118 ## to determine where the current insertion point is within the mask (i.e.
5119 ## which sub-'field'). See method documentation for more info and examples.
5120 ## 4. Added Field class and support for all constraints to be field-specific
5121 ## in addition to being globally settable for the control.
5122 ## Choices for each field are validated for length and pastability into
5123 ## the field in question, raising ValueError if not appropriate for the control.
5124 ## Also added selective additional validation based on individual field constraints.
5125 ## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all
5126 ## auto-complete fields with choice lists, supplying the 1st entry in
5127 ## the choice list if the field is empty, and cycling through the list in
5128 ## the appropriate direction if already a match. WXK_DOWN will also auto-
5129 ## complete if the field is partially completed and a match can be made.
5130 ## SHIFT-WXK_UP/DOWN will also take you to the next field after any
5131 ## auto-completion performed.
5132 ## 5. Added autoCompleteKeycodes=[] parameters for allowing further
5133 ## customization of the control. Any keycode supplied as a member
5134 ## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If
5135 ## requireFieldChoice is set, then a valid value from each non-empty
5136 ## choice list will be required for the value of the control to validate.
5137 ## 6. Fixed "auto-sizing" to be relative to the font actually used, rather
5138 ## than making assumptions about character width.
5139 ## 7. Fixed GetMaskParameter(), which was non-functional in previous version.
5140 ## 8. Fixed exceptions raised to provide info on which control had the error.
5141 ## 9. Fixed bug in choice management of wxMaskedComboBox.
5142 ## 10. Fixed bug in wxIpAddrCtrl causing traceback if field value was of
5143 ## the form '# #'. Modified control code for wxIpAddrCtrl so that '.'
5144 ## in the middle of a field clips the rest of that field, similar to
5145 ## decimal and integer controls.
5146 ##
5147 ##
5148 ## Version 0.0.7
5149 ## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive.
5150 ## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333).
5151 ## 3. New support for selecting text within the control.(thanks Will Sadkin!)
5152 ## Shift-End and Shift-Home now select text as you would expect
5153 ## Control-Shift-End selects to the end of the mask string, even if value not entered.
5154 ## Control-A selects all *entered* text, Shift-Control-A selects everything in the control.
5155 ## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed-
5156 ## for some reason I couldn't find the original email but thanks!!!)
5157 ## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome,
5158 ## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc.
5159 ## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!).
5160 ## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...)
5161 ## 7. New mechanism for replacing default behavior for any given key, using
5162 ## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available
5163 ## for easier subclassing of the control.
5164 ## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs
5165 ## with insertion point/selection modification. Changed Ctrl-X to use standard "cut"
5166 ## semantics, erasing the selection, rather than erasing the entire control.
5167 ## 9. Added option for an "default value" (ie. the template) for use when a single fillChar
5168 ## is not desired in every position. Added IsDefault() function to mean "does the value
5169 ## equal the template?" and modified .IsEmpty() to mean "do all of the editable
5170 ## positions in the template == the fillChar?"
5171 ## 10. Extracted mask logic into mixin, so we can have both wxMaskedTextCtrl and wxMaskedComboBox,
5172 ## now included.
5173 ## 11. wxMaskedComboBox now adds the capability to validate from list of valid values.
5174 ## Example: City validates against list of cities, or zip vs zip code list.
5175 ## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being
5176 ## passed to the next handler in the event chain, causing updates to the
5177 ## control to be invisible to the parent code.
5178 ## 13. Added IPADDR autoformat code, and subclass wxIpAddrCtrl for controlling tabbing within
5179 ## the control, that auto-reformats as you move between cells.
5180 ## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'.
5181 ## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14}
5182 ## 16. Fixed major bugs in date validation, due to the fact that
5183 ## wxDateTime.ParseDate is too liberal, and will accept any form that
5184 ## makes any kind of sense, regardless of the datestyle you specified
5185 ## for the control. Unfortunately, the strategy used to fix it only
5186 ## works for versions of wxPython post 2.3.3.1, as a C++ assert box
5187 ## seems to show up on an invalid date otherwise, instead of a catchable
5188 ## exception.
5189 ## 17. Enhanced date adjustment to automatically adjust heuristic based on
5190 ## current year, making last century/this century determination on
5191 ## 2-digit year based on distance between today's year and value;
5192 ## if > 50 year separation, assume last century (and don't assume last
5193 ## century is 20th.)
5194 ## 18. Added autoformats and support for including HHMMSS as well as HHMM for
5195 ## date times, and added similar time, and militaray time autoformats.
5196 ## 19. Enhanced tabbing logic so that tab takes you to the next field if the
5197 ## control is a multi-field control.
5198 ## 20. Added stub method called whenever the control "changes fields", that
5199 ## can be overridden by subclasses (eg. wxIpAddrCtrl.)
5200 ## 21. Changed a lot of code to be more functionally-oriented so side-effects
5201 ## aren't as problematic when maintaining code and/or adding features.
5202 ## Eg: IsValid() now does not have side-effects; it merely reflects the
5203 ## validity of the value of the control; to determine validity AND recolor
5204 ## the control, _CheckValid() should be used with a value argument of None.
5205 ## Similarly, made most reformatting function take an optional candidate value
5206 ## rather than just using the current value of the control, and only
5207 ## have them change the value of the control if a candidate is not specified.
5208 ## In this way, you can do validation *before* changing the control.
5209 ## 22. Changed validRequired to mean "disallow chars that result in invalid
5210 ## value." (Old meaning now represented by emptyInvalid.) (This was
5211 ## possible once I'd made the changes in (19) above.)
5212 ## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they
5213 ## can be set/modified/retrieved after construction. Removed individual
5214 ## parameter setting functions, in favor of this mechanism, so that
5215 ## all adjustment of the control based on changing parameter values can
5216 ## be handled in one place with unified mechanism.
5217 ## 24. Did a *lot* of testing and fixing re: numeric values. Added ability
5218 ## to type "grouping char" (ie. ',') and validate as appropriate.
5219 ## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9.
5220 ## 26. Fixed assumption about "decimal or integer" masks so that they're only
5221 ## made iff there's no validRegex associated with the field. (This
5222 ## is so things like zipcodes which look like integers can have more
5223 ## restrictive validation (ie. must be 5 digits.)
5224 ## 27. Added a ton more doc strings to explain use and derivation requirements
5225 ## and did regularization of the naming conventions.
5226 ## 28. Fixed a range bug in _adjustKey preventing z from being handled properly.
5227 ## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to
5228 ## reformat the value and move the next field as appropriate. (shift-'.',
5229 ## ie. '>' moves to the previous field.
5230
5231 ## Version 0.0.6
5232 ## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending
5233 ## in '0'.
5234 ## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the
5235 ## year position, the control will expand the value to four digits, using numerals below
5236 ## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn).
5237 ## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM}
5238 ## 3. revalid parameter renamed validRegex to conform to standard for all validation
5239 ## parameters (see 2 new ones below).
5240 ## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types.
5241 ## Allows the developer to specify a valid low/high range of values.
5242 ## 5. New optional init parameter = validList. Used for character types. Allows developer
5243 ## to send a list of values to the control to be used for specific validation.
5244 ## See the Last Name Only example - it is list restricted to Smith/Jones/Williams.
5245 ## 6. Date type fields now use wxDateTime's parser to validate the date and time.
5246 ## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing
5247 ## me toward this solution!
5248 ## 7. Date fields now automatically expand 2-digit years when it can. For example,
5249 ## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year
5250 ## date is entered it will be expanded in any case when the user tabs out of the
5251 ## field.
5252 ## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor,
5253 ## SetSignedForeColor allow accessto override default class coloring behavior.
5254 ## 9. Documentation updated and improved.
5255 ## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better.
5256 ## Two new options (checkboxes) - test highlight empty and disallow empty.
5257 ## 11. Home and End now work more intuitively, moving to the first and last user-entry
5258 ## value, respectively.
5259 ## 12. New class function: SetRequired(bool). Sets the control's entry required flag
5260 ## (i.e. disallow empty values if True).
5261 ##
5262 ## Version 0.0.5
5263 ## 1. get_plainValue method renamed to GetPlainValue following the wxWindows
5264 ## StudlyCaps(tm) standard (thanks Paul Moore). ;)
5265 ## 2. New format code 'F' causes the control to auto-fit (auto-size) itself
5266 ## based on the length of the mask template.
5267 ## 3. Class now supports "autoformat" codes. These can be passed to the class
5268 ## on instantiation using the parameter autoformat="code". If the code is in
5269 ## the dictionary, it will self set the mask, formatting, and validation string.
5270 ## I have included a number of samples, but I am hoping that someone out there
5271 ## can help me to define a whole bunch more.
5272 ## 4. I have added a second page to the demo (as well as a second demo class, test2)
5273 ## to showcase how autoformats work. The way they self-format and self-size is,
5274 ## I must say, pretty cool.
5275 ## 5. Comments added and some internal cosmetic revisions re: matching the code
5276 ## standards for class submission.
5277 ## 6. Regex validation is now done in real time - field turns yellow immediately
5278 ## and stays yellow until the entered value is valid
5279 ## 7. Cursor now skips over template characters in a more intuitive way (before the
5280 ## next keypress).
5281 ## 8. Change, Keypress and LostFocus methods added for convenience of subclasses.
5282 ## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR,
5283 ## and EVT_KILL_FOCUS, respectively.
5284 ## 9. Decimal and numeric handlers have been rewritten and now work more intuitively.
5285 ##
5286 ## Version 0.0.4
5287 ## 1. New .IsEmpty() method returns True if the control's value is equal to the
5288 ## blank template string
5289 ## 2. Control now supports a new init parameter: revalid. Pass a regular expression
5290 ## that the value will have to match when the control loses focus. If invalid,
5291 ## the control's BackgroundColor will turn yellow, and an internal flag is set (see next).
5292 ## 3. Demo now shows revalid functionality. Try entering a partial value, such as a
5293 ## partial social security number.
5294 ## 4. New .IsValid() value returns True if the control is empty, or if the value matches
5295 ## the revalid expression. If not, .IsValid() returns False.
5296 ## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never
5297 ## presses the decimal point.
5298 ## 6. Cursor now goes to the beginning of the field if the user clicks in an
5299 ## "empty" field intead of leaving the insertion point in the middle of the
5300 ## field.
5301 ## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9.
5302 ## 8. New formatcodes init parameter replaces other init params and adds functions.
5303 ## String passed to control on init controls:
5304 ## _ Allow spaces
5305 ## ! Force upper
5306 ## ^ Force lower
5307 ## R Show negative #s in red
5308 ## , Group digits
5309 ## - Signed numerals
5310 ## 0 Numeric fields get leading zeros
5311 ## 9. Ctrl-X in any field clears the current value.
5312 ## 10. Code refactored and made more modular (esp in OnChar method). Should be more
5313 ## easy to read and understand.
5314 ## 11. Demo enhanced.
5315 ## 12. Now has _doc_.
5316 ##
5317 ## Version 0.0.3
5318 ## 1. GetPlainValue() now returns the value without the template characters;
5319 ## so, for example, a social security number (123-33-1212) would return as
5320 ## 123331212; also removes white spaces from numeric/decimal values, so
5321 ## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value.
5322 ## 2. Press '.' in an integer style masked control and truncate any trailing digits.
5323 ## 3. Code moderately refactored. Internal names improved for clarity. Additional
5324 ## internal documentation.
5325 ## 4. Home and End keys now supported to move cursor to beginning or end of field.
5326 ## 5. Un-signed integers and decimals now supported.
5327 ## 6. Cosmetic improvements to the demo.
5328 ## 7. Class renamed to wxMaskedTextCtrl.
5329 ## 8. Can now specify include characters that will override the basic
5330 ## controls: for example, includeChars = "@." for email addresses
5331 ## 9. Added mask character 'C' -> allow any upper or lowercase character
5332 ## 10. .SetSignColor(str:color) sets the foreground color for negative values
5333 ## in signed controls (defaults to red)
5334 ## 11. Overview documentation written.
5335 ##
5336 ## Version 0.0.2
5337 ## 1. Tab now works properly when pressed in last position
5338 ## 2. Decimal types now work (e.g. #####.##)
5339 ## 3. Signed decimal or numeric values supported (i.e. negative numbers)
5340 ## 4. Negative decimal or numeric values now can show in red.
5341 ## 5. Can now specify an "exclude list" with the excludeChars parameter.
5342 ## See date/time formatted example - you can only enter A or P in the
5343 ## character mask space (i.e. AM/PM).
5344 ## 6. Backspace now works properly, including clearing data from a selected
5345 ## region but leaving template characters intact. Also delete key.
5346 ## 7. Left/right arrows now work properly.
5347 ## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3
5348 ##