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