]>
Commit | Line | Data |
---|---|---|
d14a1e28 RD |
1 | #---------------------------------------------------------------------------- |
2 | # Name: maskededit.py | |
3 | # Authors: Jeff Childers, Will Sadkin | |
4 | # Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com | |
5 | # Created: 02/11/2003 | |
fffd96b7 | 6 | # Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003 |
d14a1e28 RD |
7 | # Portions: (c) 2002 by Will Sadkin, 2002-2003 |
8 | # RCS-ID: $Id$ | |
fffd96b7 | 9 | # License: wxWidgets license |
d14a1e28 RD |
10 | #---------------------------------------------------------------------------- |
11 | # NOTE: | |
d4b73b1b | 12 | # MaskedEdit controls are based on a suggestion made on [wxPython-Users] by |
d14a1e28 | 13 | # Jason Hihn, and borrows liberally from Will Sadkin's original masked edit |
d4b73b1b | 14 | # control for time entry, TimeCtrl (which is now rewritten using this |
d14a1e28 RD |
15 | # control!). |
16 | # | |
d4b73b1b | 17 | # MaskedEdit controls do not normally use validators, because they do |
d14a1e28 RD |
18 | # careful manipulation of the cursor in the text window on each keystroke, |
19 | # and validation is cursor-position specific, so the control intercepts the | |
20 | # key codes before the validator would fire. However, validators can be | |
21 | # provided to do data transfer to the controls. | |
b881fc78 RD |
22 | # |
23 | #---------------------------------------------------------------------------- | |
24 | # | |
25 | # 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
26 | # | |
27 | # o Updated for wx namespace. No guarantees. This is one huge file. | |
28 | # | |
29 | # 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
30 | # | |
31 | # o Missed wx.DateTime stuff earlier. | |
32 | # | |
d4b73b1b RD |
33 | # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) |
34 | # | |
35 | # o wxMaskedEditMixin -> MaskedEditMixin | |
36 | # o wxMaskedTextCtrl -> MaskedTextCtrl | |
37 | # o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent | |
38 | # o wxMaskedComboBox -> MaskedComboBox | |
39 | # o wxIpAddrCtrl -> IpAddrCtrl | |
40 | # o wxTimeCtrl -> TimeCtrl | |
41 | # | |
1fded56b | 42 | |
d14a1e28 RD |
43 | """\ |
44 | <b>Masked Edit Overview: | |
45 | =====================</b> | |
d4b73b1b | 46 | <b>MaskedTextCtrl</b> |
d14a1e28 RD |
47 | is a sublassed text control that can carefully control the user's input |
48 | based on a mask string you provide. | |
1fded56b | 49 | |
d14a1e28 | 50 | General usage example: |
d4b73b1b | 51 | control = MaskedTextCtrl( win, -1, '', mask = '(###) ###-####') |
d14a1e28 RD |
52 | |
53 | The example above will create a text control that allows only numbers to be | |
54 | entered and then only in the positions indicated in the mask by the # sign. | |
55 | ||
d4b73b1b | 56 | <b>MaskedComboBox</b> |
d14a1e28 RD |
57 | is a similar subclass of wxComboBox that allows the same sort of masking, |
58 | but also can do auto-complete of values, and can require the value typed | |
59 | to be in the list of choices to be colored appropriately. | |
60 | ||
61 | <b>wxMaskedCtrl</b> | |
62 | is actually a factory function for several types of masked edit controls: | |
63 | ||
d4b73b1b RD |
64 | <b>MaskedTextCtrl</b> - standard masked edit text box |
65 | <b>MaskedComboBox</b> - adds combobox capabilities | |
66 | <b>IpAddrCtrl</b> - adds special semantics for IP address entry | |
67 | <b>TimeCtrl</b> - special subclass handling lots of types as values | |
d14a1e28 RD |
68 | <b>wxMaskedNumCtrl</b> - special subclass handling numeric values |
69 | ||
70 | It works by looking for a <b><i>controlType</i></b> parameter in the keyword | |
71 | arguments of the control, to determine what kind of instance to return. | |
72 | If not specified as a keyword argument, the default control type returned | |
d4b73b1b | 73 | will be MaskedTextCtrl. |
d14a1e28 RD |
74 | |
75 | Each of the above classes has its own set of arguments, but wxMaskedCtrl | |
76 | provides a single "unified" interface for masked controls. Those for | |
d4b73b1b | 77 | MaskedTextCtrl, MaskedComboBox and IpAddrCtrl are all documented |
d14a1e28 RD |
78 | below; the others have their own demo pages and interface descriptions. |
79 | (See end of following discussion for how to configure the wxMaskedCtrl() | |
80 | to select the above control types.) | |
81 | ||
82 | ||
83 | <b>INITILIZATION PARAMETERS | |
84 | ======================== | |
85 | mask=</b> | |
86 | Allowed mask characters and function: | |
87 | Character Function | |
88 | # Allow numeric only (0-9) | |
89 | N Allow letters and numbers (0-9) | |
90 | A Allow uppercase letters only | |
91 | a Allow lowercase letters only | |
92 | C Allow any letter, upper or lower | |
93 | X Allow string.letters, string.punctuation, string.digits | |
94 | & Allow string.punctuation only | |
95 | ||
96 | ||
97 | These controls define these sets of characters using string.letters, | |
98 | string.uppercase, etc. These sets are affected by the system locale | |
99 | setting, so in order to have the masked controls accept characters | |
100 | that are specific to your users' language, your application should | |
101 | set the locale. | |
102 | For example, to allow international characters to be used in the | |
103 | above masks, you can place the following in your code as part of | |
104 | your application's initialization code: | |
105 | ||
106 | import locale | |
107 | locale.setlocale(locale.LC_ALL, '') | |
108 | ||
109 | ||
110 | Using these mask characters, a variety of template masks can be built. See | |
111 | the demo for some other common examples include date+time, social security | |
112 | number, etc. If any of these characters are needed as template rather | |
113 | than mask characters, they can be escaped with \, ie. \N means "literal N". | |
114 | (use \\ for literal backslash, as in: r'CCC\\NNN'.) | |
115 | ||
116 | ||
117 | <b>Note:</b> | |
118 | Masks containing only # characters and one optional decimal point | |
119 | character are handled specially, as "numeric" controls. Such | |
120 | controls have special handling for typing the '-' key, handling | |
121 | the "decimal point" character as truncating the integer portion, | |
122 | optionally allowing grouping characters and so forth. | |
123 | There are several parameters and format codes that only make sense | |
124 | when combined with such masks, eg. groupChar, decimalChar, and so | |
125 | forth (see below). These allow you to construct reasonable | |
126 | numeric entry controls. | |
127 | ||
128 | <b>Note:</b> | |
129 | Changing the mask for a control deletes any previous field classes | |
130 | (and any associated validation or formatting constraints) for them. | |
131 | ||
132 | <b>useFixedWidthFont=</b> | |
133 | By default, masked edit controls use a fixed width font, so that | |
134 | the mask characters are fixed within the control, regardless of | |
135 | subsequent modifications to the value. Set to False if having | |
136 | the control font be the same as other controls is required. | |
137 | ||
138 | ||
139 | <b>formatcodes=</b> | |
140 | These other properties can be passed to the class when instantiating it: | |
141 | Formatcodes are specified as a string of single character formatting | |
142 | codes that modify behavior of the control: | |
143 | _ Allow spaces | |
144 | ! Force upper | |
145 | ^ Force lower | |
146 | R Right-align field(s) | |
147 | r Right-insert in field(s) (implies R) | |
148 | < Stay in field until explicit navigation out of it | |
149 | ||
150 | > Allow insert/delete within partially filled fields (as | |
151 | opposed to the default "overwrite" mode for fixed-width | |
152 | masked edit controls.) This allows single-field controls | |
153 | or each field within a multi-field control to optionally | |
154 | behave more like standard text controls. | |
155 | (See EMAIL or phone number autoformat examples.) | |
156 | ||
157 | <i>Note: This also governs whether backspace/delete operations | |
158 | shift contents of field to right of cursor, or just blank the | |
159 | erased section. | |
160 | ||
161 | Also, when combined with 'r', this indicates that the field | |
162 | or control allows right insert anywhere within the current | |
163 | non-empty value in the field. (Otherwise right-insert behavior | |
164 | is only performed to when the entire right-insertable field is | |
165 | selected or the cursor is at the right edge of the field.</i> | |
166 | ||
167 | ||
168 | , Allow grouping character in integer fields of numeric controls | |
169 | and auto-group/regroup digits (if the result fits) when leaving | |
170 | such a field. (If specified, .SetValue() will attempt to | |
171 | auto-group as well.) | |
172 | ',' is also the default grouping character. To change the | |
173 | grouping character and/or decimal character, use the groupChar | |
174 | and decimalChar parameters, respectively. | |
175 | Note: typing the "decimal point" character in such fields will | |
176 | clip the value to that left of the cursor for integer | |
177 | fields of controls with "integer" or "floating point" masks. | |
178 | If the ',' format code is specified, this will also cause the | |
179 | resulting digits to be regrouped properly, using the current | |
180 | grouping character. | |
181 | - Prepend and reserve leading space for sign to mask and allow | |
182 | signed values (negative #s shown in red by default.) Can be | |
183 | used with argument useParensForNegatives (see below.) | |
184 | 0 integer fields get leading zeros | |
185 | D Date[/time] field | |
186 | T Time field | |
187 | F Auto-Fit: the control calulates its size from | |
188 | the length of the template mask | |
189 | V validate entered chars against validRegex before allowing them | |
190 | to be entered vs. being allowed by basic mask and then having | |
191 | the resulting value just colored as invalid. | |
192 | (See USSTATE autoformat demo for how this can be used.) | |
193 | S select entire field when navigating to new field | |
194 | ||
195 | <b>fillChar= | |
196 | defaultValue=</b> | |
197 | These controls have two options for the initial state of the control. | |
198 | If a blank control with just the non-editable characters showing | |
199 | is desired, simply leave the constructor variable fillChar as its | |
200 | default (' '). If you want some other character there, simply | |
201 | change the fillChar to that value. Note: changing the control's fillChar | |
202 | will implicitly reset all of the fields' fillChars to this value. | |
203 | ||
204 | If you need different default characters in each mask position, | |
205 | you can specify a defaultValue parameter in the constructor, or | |
206 | set them for each field individually. | |
207 | This value must satisfy the non-editable characters of the mask, | |
208 | but need not conform to the replaceable characters. | |
209 | ||
210 | <b>groupChar= | |
211 | decimalChar=</b> | |
212 | These parameters govern what character is used to group numbers | |
213 | and is used to indicate the decimal point for numeric format controls. | |
214 | The default groupChar is ',', the default decimalChar is '.' | |
215 | By changing these, you can customize the presentation of numbers | |
216 | for your location. | |
217 | eg: formatcodes = ',', groupChar="'" allows 12'345.34 | |
218 | formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34 | |
219 | ||
220 | <b>shiftDecimalChar=</b> | |
221 | The default "shiftDecimalChar" (used for "backwards-tabbing" until | |
222 | shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for | |
223 | other keyboards, you may want to customize this, eg '?' for shift ',' on | |
224 | AZERTY keyboards, ':' or ';' for other European keyboards, etc. | |
225 | ||
226 | <b>useParensForNegatives=False</b> | |
227 | This option can be used with signed numeric format controls to | |
228 | indicate signs via () rather than '-'. | |
229 | ||
230 | <b>autoSelect=False</b> | |
231 | This option can be used to have a field or the control try to | |
232 | auto-complete on each keystroke if choices have been specified. | |
233 | ||
234 | <b>autoCompleteKeycodes=[]</b> | |
235 | By default, DownArrow, PageUp and PageDown will auto-complete a | |
236 | partially entered field. Shift-DownArrow, Shift-UpArrow, PageUp | |
237 | and PageDown will also auto-complete, but if the field already | |
238 | contains a matched value, these keys will cycle through the list | |
239 | of choices forward or backward as appropriate. Shift-Up and | |
240 | Shift-Down also take you to the next/previous field after any | |
241 | auto-complete action. | |
242 | ||
243 | Additional auto-complete keys can be specified via this parameter. | |
244 | Any keys so specified will act like PageDown. | |
245 | ||
246 | ||
247 | ||
248 | <b>Validating User Input: | |
249 | ======================</b> | |
250 | There are a variety of initialization parameters that are used to validate | |
251 | user input. These parameters can apply to the control as a whole, and/or | |
252 | to individual fields: | |
253 | ||
254 | excludeChars= A string of characters to exclude even if otherwise allowed | |
255 | includeChars= A string of characters to allow even if otherwise disallowed | |
256 | validRegex= Use a regular expression to validate the contents of the text box | |
257 | validRange= Pass a rangeas list (low,high) to limit numeric fields/values | |
258 | choices= A list of strings that are allowed choices for the control. | |
259 | choiceRequired= value must be member of choices list | |
260 | compareNoCase= Perform case-insensitive matching when validating against list | |
d4b73b1b | 261 | <i>Note: for MaskedComboBox, this defaults to True.</i> |
d14a1e28 RD |
262 | emptyInvalid= Boolean indicating whether an empty value should be considered invalid |
263 | ||
264 | validFunc= A function to call of the form: bool = func(candidate_value) | |
265 | which will return True if the candidate_value satisfies some | |
266 | external criteria for the control in addition to the the | |
267 | other validation, or False if not. (This validation is | |
268 | applied last in the chain of validations.) | |
269 | ||
270 | validRequired= Boolean indicating whether or not keys that are allowed by the | |
271 | mask, but result in an invalid value are allowed to be entered | |
272 | into the control. Setting this to True implies that a valid | |
273 | default value is set for the control. | |
274 | ||
275 | retainFieldValidation= | |
276 | False by default; if True, this allows individual fields to | |
277 | retain their own validation constraints independently of any | |
278 | subsequent changes to the control's overall parameters. | |
279 | ||
280 | validator= Validators are not normally needed for masked controls, because | |
281 | of the nature of the validation and control of input. However, | |
282 | you can supply one to provide data transfer routines for the | |
283 | controls. | |
284 | ||
285 | ||
286 | <b>Coloring Behavior: | |
287 | ==================</b> | |
288 | The following parameters have been provided to allow you to change the default | |
289 | coloring behavior of the control. These can be set at construction, or via | |
290 | the .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow': | |
291 | ||
292 | emptyBackgroundColour= Control Background color when identified as empty. Default=White | |
293 | invalidBackgroundColour= Control Background color when identified as Not valid. Default=Yellow | |
294 | validBackgroundColour= Control Background color when identified as Valid. Default=white | |
295 | ||
296 | ||
297 | The following parameters control the default foreground color coloring behavior of the | |
298 | control. Pass a color as string e.g. 'Yellow': | |
299 | foregroundColour= Control foreground color when value is not negative. Default=Black | |
300 | signedForegroundColour= Control foreground color when value is negative. Default=Red | |
301 | ||
302 | ||
303 | <b>Fields: | |
304 | =======</b> | |
305 | Each part of the mask that allows user input is considered a field. The fields | |
306 | are represented by their own class instances. You can specify field-specific | |
307 | constraints by constructing or accessing the field instances for the control | |
308 | and then specifying those constraints via parameters. | |
309 | ||
310 | <b>fields=</b> | |
311 | This parameter allows you to specify Field instances containing | |
312 | constraints for the individual fields of a control, eg: local | |
313 | choice lists, validation rules, functions, regexps, etc. | |
314 | It can be either an ordered list or a dictionary. If a list, | |
315 | the fields will be applied as fields 0, 1, 2, etc. | |
316 | If a dictionary, it should be keyed by field index. | |
317 | the values should be a instances of maskededit.Field. | |
318 | ||
319 | Any field not represented by the list or dictionary will be | |
320 | implicitly created by the control. | |
321 | ||
322 | eg: | |
323 | fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ] | |
324 | or | |
325 | fields = { | |
326 | 1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']), | |
327 | 3: ( Field(choices=['01', '02', '03'], choiceRequired=True) | |
328 | } | |
329 | ||
330 | The following parameters are available for individual fields, with the | |
331 | same semantics as for the whole control but applied to the field in question: | |
332 | ||
333 | fillChar # if set for a field, it will override the control's fillChar for that field | |
334 | groupChar # if set for a field, it will override the control's default | |
335 | defaultValue # sets field-specific default value; overrides any default from control | |
336 | compareNoCase # overrides control's settings | |
337 | emptyInvalid # determines whether field is required to be filled at all times | |
338 | validRequired # if set, requires field to contain valid value | |
339 | ||
340 | If any of the above parameters are subsequently specified for the control as a | |
341 | whole, that new value will be propagated to each field, unless the | |
342 | retainFieldValidation control-level parameter is set. | |
343 | ||
344 | formatcodes # Augments control's settings | |
345 | excludeChars # ' ' ' | |
346 | includeChars # ' ' ' | |
347 | validRegex # ' ' ' | |
348 | validRange # ' ' ' | |
349 | choices # ' ' ' | |
350 | choiceRequired # ' ' ' | |
351 | validFunc # ' ' ' | |
352 | ||
353 | ||
354 | ||
355 | <b>Control Class Functions: | |
356 | ======================== | |
357 | .GetPlainValue(value=None)</b> | |
358 | Returns the value specified (or the control's text value | |
359 | not specified) without the formatting text. | |
360 | In the example above, might return phone no='3522640075', | |
361 | whereas control.GetValue() would return '(352) 264-0075' | |
362 | <b>.ClearValue()</b> | |
363 | Returns the control's value to its default, and places the | |
364 | cursor at the beginning of the control. | |
365 | <b>.SetValue()</b> | |
366 | Does "smart replacement" of passed value into the control, as does | |
367 | the .Paste() method. As with other text entry controls, the | |
368 | .SetValue() text replacement begins at left-edge of the control, | |
369 | with missing mask characters inserted as appropriate. | |
370 | .SetValue will also adjust integer, float or date mask entry values, | |
371 | adding commas, auto-completing years, etc. as appropriate. | |
372 | For "right-aligned" numeric controls, it will also now automatically | |
373 | right-adjust any value whose length is less than the width of the | |
374 | control before attempting to set the value. | |
375 | If a value does not follow the format of the control's mask, or will | |
376 | not fit into the control, a ValueError exception will be raised. | |
377 | Eg: | |
378 | mask = '(###) ###-####' | |
379 | .SetValue('1234567890') => '(123) 456-7890' | |
380 | .SetValue('(123)4567890') => '(123) 456-7890' | |
381 | .SetValue('(123)456-7890') => '(123) 456-7890' | |
382 | .SetValue('123/4567-890') => illegal paste; ValueError | |
383 | ||
384 | mask = '#{6}.#{2}', formatcodes = '_,-', | |
385 | .SetValue('111') => ' 111 . ' | |
386 | .SetValue(' %9.2f' % -111.12345 ) => ' -111.12' | |
387 | .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00' | |
388 | .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError | |
389 | ||
390 | mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control | |
391 | .SetValue('111') => padded value misalignment ValueError: " 111" will not fit | |
392 | .SetValue('%.2f' % 111 ) => ' 111.00' | |
393 | .SetValue('%.2f' % -111.12345 ) => ' -111.12' | |
394 | ||
395 | ||
396 | <b>.IsValid(value=None)</b> | |
397 | Returns True if the value specified (or the value of the control | |
398 | if not specified) passes validation tests | |
399 | <b>.IsEmpty(value=None)</b> | |
400 | Returns True if the value specified (or the value of the control | |
401 | if not specified) is equal to an "empty value," ie. all | |
402 | editable characters == the fillChar for their respective fields. | |
403 | <b>.IsDefault(value=None)</b> | |
404 | Returns True if the value specified (or the value of the control | |
405 | if not specified) is equal to the initial value of the control. | |
406 | ||
407 | <b>.Refresh()</b> | |
408 | Recolors the control as appropriate to its current settings. | |
409 | ||
410 | <b>.SetCtrlParameters(**kwargs)</b> | |
411 | This function allows you to set up and/or change the control parameters | |
412 | after construction; it takes a list of key/value pairs as arguments, | |
413 | where the keys can be any of the mask-specific parameters in the constructor. | |
414 | Eg: | |
d4b73b1b | 415 | ctl = MaskedTextCtrl( self, -1 ) |
d14a1e28 RD |
416 | ctl.SetCtrlParameters( mask='###-####', |
417 | defaultValue='555-1212', | |
418 | formatcodes='F') | |
419 | ||
420 | <b>.GetCtrlParameter(parametername)</b> | |
421 | This function allows you to retrieve the current value of a parameter | |
422 | from the control. | |
423 | ||
424 | <b><i>Note:</i></b> Each of the control parameters can also be set using its | |
425 | own Set and Get function. These functions follow a regular form: | |
426 | All of the parameter names start with lower case; for their | |
427 | corresponding Set/Get function, the parameter name is capitalized. | |
428 | Eg: ctl.SetMask('###-####') | |
429 | ctl.SetDefaultValue('555-1212') | |
430 | ctl.GetChoiceRequired() | |
431 | ctl.GetFormatcodes() | |
432 | ||
fffd96b7 RD |
433 | <b><i>Note:</i></b> After any change in parameters, the choices for the |
434 | control are reevaluated to ensure that they are still legal. If you | |
435 | have large choice lists, it is therefore more efficient to set parameters | |
436 | before setting the choices available. | |
437 | ||
d14a1e28 RD |
438 | <b>.SetFieldParameters(field_index, **kwargs)</b> |
439 | This function allows you to specify change individual field | |
440 | parameters after construction. (Indices are 0-based.) | |
441 | ||
442 | <b>.GetFieldParameter(field_index, parametername)</b> | |
443 | Allows the retrieval of field parameters after construction | |
444 | ||
445 | ||
446 | The control detects certain common constructions. In order to use the signed feature | |
447 | (negative numbers and coloring), the mask has to be all numbers with optionally one | |
448 | decimal point. Without a decimal (e.g. '######', the control will treat it as an integer | |
449 | value. With a decimal (e.g. '###.##'), the control will act as a floating point control | |
450 | (i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the | |
fffd96b7 RD |
451 | integer control truncates the value. However, for a true numeric control, |
452 | MaskedNumCtrl provides all this, and true numeric input/output support as well. | |
d14a1e28 RD |
453 | |
454 | ||
455 | Check your controls by calling each control's .IsValid() function and the | |
456 | .IsEmpty() function to determine which controls have been a) filled in and | |
457 | b) filled in properly. | |
458 | ||
459 | ||
460 | Regular expression validations can be used flexibly and creatively. | |
461 | Take a look at the demo; the zip-code validation succeeds as long as the | |
462 | first five numerals are entered. the last four are optional, but if | |
463 | any are entered, there must be 4 to be valid. | |
464 | ||
465 | <B>wxMaskedCtrl Configuration | |
466 | ==========================</B> | |
467 | wxMaskedCtrl works by looking for a special <b><i>controlType</i></b> | |
468 | parameter in the variable arguments of the control, to determine | |
469 | what kind of instance to return. | |
470 | controlType can be one of: | |
471 | ||
472 | controlTypes.MASKEDTEXT | |
473 | controlTypes.MASKEDCOMBO | |
474 | controlTypes.IPADDR | |
475 | controlTypes.TIME | |
476 | controlTypes.NUMBER | |
477 | ||
478 | These constants are also available individually, ie, you can | |
479 | use either of the following: | |
480 | ||
481 | from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, controlTypes | |
482 | from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER | |
483 | ||
484 | If not specified as a keyword argument, the default controlType is | |
485 | controlTypes.TEXT. | |
486 | """ | |
487 | ||
488 | """ | |
489 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
490 | DEVELOPER COMMENTS: | |
491 | ||
492 | Naming Conventions | |
493 | ------------------ | |
494 | All methods of the Mixin that are not meant to be exposed to the external | |
495 | interface are prefaced with '_'. Those functions that are primarily | |
496 | intended to be internal subroutines subsequently start with a lower-case | |
497 | letter; those that are primarily intended to be used and/or overridden | |
498 | by derived subclasses start with a capital letter. | |
499 | ||
500 | The following methods must be used and/or defined when deriving a control | |
501 | from wxMaskedEditMixin. NOTE: if deriving from a *masked edit* control | |
d4b73b1b | 502 | (eg. class IpAddrCtrl(MaskedTextCtrl) ), then this is NOT necessary, |
d14a1e28 RD |
503 | as it's already been done for you in the base class. |
504 | ||
505 | ._SetInitialValue() | |
506 | This function must be called after the associated base | |
507 | control has been initialized in the subclass __init__ | |
508 | function. It sets the initial value of the control, | |
509 | either to the value specified if non-empty, the | |
510 | default value if specified, or the "template" for | |
511 | the empty control as necessary. It will also set/reset | |
512 | the font if necessary and apply formatting to the | |
513 | control at this time. | |
514 | ||
515 | ._GetSelection() | |
516 | REQUIRED | |
517 | Each class derived from wxMaskedEditMixin must define | |
518 | the function for getting the start and end of the | |
519 | current text selection. The reason for this is | |
520 | that not all controls have the same function name for | |
521 | doing this; eg. wxTextCtrl uses .GetSelection(), | |
522 | whereas we had to write a .GetMark() function for | |
523 | wxComboBox, because .GetSelection() for the control | |
524 | gets the currently selected list item from the combo | |
525 | box, and the control doesn't (yet) natively provide | |
526 | a means of determining the text selection. | |
527 | ._SetSelection() | |
528 | REQUIRED | |
529 | Similarly to _GetSelection, each class derived from | |
530 | wxMaskedEditMixin must define the function for setting | |
531 | the start and end of the current text selection. | |
d4b73b1b RD |
532 | (eg. .SetSelection() for MaskedTextCtrl, and .SetMark() for |
533 | MaskedComboBox. | |
d14a1e28 RD |
534 | |
535 | ._GetInsertionPoint() | |
536 | ._SetInsertionPoint() | |
537 | REQUIRED | |
538 | For consistency, and because the mixin shouldn't rely | |
539 | on fixed names for any manipulations it does of any of | |
540 | the base controls, we require each class derived from | |
541 | wxMaskedEditMixin to define these functions as well. | |
542 | ||
543 | ._GetValue() | |
544 | ._SetValue() REQUIRED | |
545 | Each class derived from wxMaskedEditMixin must define | |
546 | the functions used to get and set the raw value of the | |
547 | control. | |
548 | This is necessary so that recursion doesn't take place | |
549 | when setting the value, and so that the mixin can | |
550 | call the appropriate function after doing all its | |
551 | validation and manipulation without knowing what kind | |
552 | of base control it was mixed in with. To handle undo | |
553 | functionality, the ._SetValue() must record the current | |
554 | selection prior to setting the value. | |
555 | ||
556 | .Cut() | |
557 | .Paste() | |
558 | .Undo() | |
559 | .SetValue() REQUIRED | |
560 | Each class derived from wxMaskedEditMixin must redefine | |
561 | these functions to call the _Cut(), _Paste(), _Undo() | |
562 | and _SetValue() methods, respectively for the control, | |
563 | so as to prevent programmatic corruption of the control's | |
564 | value. This must be done in each derivation, as the | |
565 | mixin cannot itself override a member of a sibling class. | |
566 | ||
567 | ._Refresh() REQUIRED | |
568 | Each class derived from wxMaskedEditMixin must define | |
569 | the function used to refresh the base control. | |
570 | ||
571 | .Refresh() REQUIRED | |
572 | Each class derived from wxMaskedEditMixin must redefine | |
573 | this function so that it checks the validity of the | |
574 | control (via self._CheckValid) and then refreshes | |
575 | control using the base class method. | |
576 | ||
577 | ._IsEditable() REQUIRED | |
578 | Each class derived from wxMaskedEditMixin must define | |
579 | the function used to determine if the base control is | |
d4b73b1b | 580 | editable or not. (For MaskedComboBox, this has to |
d14a1e28 RD |
581 | be done with code, rather than specifying the proper |
582 | function in the base control, as there isn't one...) | |
583 | ._CalcSize() REQUIRED | |
584 | Each class derived from wxMaskedEditMixin must define | |
585 | the function used to determine how wide the control | |
586 | should be given the mask. (The mixin function | |
587 | ._calcSize() provides a baseline estimate.) | |
588 | ||
589 | ||
590 | Event Handling | |
591 | -------------- | |
592 | Event handlers are "chained", and wxMaskedEditMixin usually | |
593 | swallows most of the events it sees, thereby preventing any other | |
594 | handlers from firing in the chain. It is therefore required that | |
595 | each class derivation using the mixin to have an option to hook up | |
596 | the event handlers itself or forego this operation and let a | |
597 | subclass of the masked control do so. For this reason, each | |
598 | subclass should probably include the following code: | |
599 | ||
600 | if setupEventHandling: | |
601 | ## Setup event handlers | |
602 | EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection | |
603 | EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator | |
604 | EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick | |
605 | EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu | |
606 | EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. | |
607 | EVT_CHAR( self, self._OnChar ) ## handle each keypress | |
608 | EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately & keep | |
609 | ## track of previous value for undo | |
610 | ||
611 | where setupEventHandling is an argument to its constructor. | |
612 | ||
613 | These 5 handlers must be "wired up" for the wxMaskedEdit | |
614 | control to provide default behavior. (The setupEventHandling | |
d4b73b1b | 615 | is an argument to MaskedTextCtrl and MaskedComboBox, so |
d14a1e28 RD |
616 | that controls derived from *them* may replace one of these |
617 | handlers if they so choose.) | |
618 | ||
619 | If your derived control wants to preprocess events before | |
620 | taking action, it should then set up the event handling itself, | |
621 | so it can be first in the event handler chain. | |
622 | ||
623 | ||
624 | The following routines are available to facilitate changing | |
625 | the default behavior of wxMaskedEdit controls: | |
626 | ||
627 | ._SetKeycodeHandler(keycode, func) | |
628 | ._SetKeyHandler(char, func) | |
629 | Use to replace default handling for any given keycode. | |
630 | func should take the key event as argument and return | |
631 | False if no further action is required to handle the | |
632 | key. Eg: | |
633 | self._SetKeycodeHandler(WXK_UP, self.IncrementValue) | |
634 | self._SetKeyHandler('-', self._OnChangeSign) | |
635 | ||
636 | "Navigation" keys are assumed to change the cursor position, and | |
637 | therefore don't cause automatic motion of the cursor as insertable | |
638 | characters do. | |
639 | ||
640 | ._AddNavKeycode(keycode, handler=None) | |
641 | ._AddNavKey(char, handler=None) | |
642 | Allows controls to specify other keys (and optional handlers) | |
d4b73b1b | 643 | to be treated as navigational characters. (eg. '.' in IpAddrCtrl) |
d14a1e28 RD |
644 | |
645 | ._GetNavKeycodes() Returns the current list of navigational keycodes. | |
646 | ||
647 | ._SetNavKeycodes(key_func_tuples) | |
648 | Allows replacement of the current list of keycode | |
649 | processed as navigation keys, and bind associated | |
650 | optional keyhandlers. argument is a list of key/handler | |
651 | tuples. Passing a value of None for the handler in a | |
652 | given tuple indicates that default processing for the key | |
653 | is desired. | |
654 | ||
655 | ._FindField(pos) Returns the Field object associated with this position | |
656 | in the control. | |
657 | ||
658 | ._FindFieldExtent(pos, getslice=False, value=None) | |
659 | Returns edit_start, edit_end of the field corresponding | |
660 | to the specified position within the control, and | |
661 | optionally also returns the current contents of that field. | |
662 | If value is specified, it will retrieve the slice the corresponding | |
663 | slice from that value, rather than the current value of the | |
664 | control. | |
665 | ||
666 | ._AdjustField(pos) | |
667 | This is, the function that gets called for a given position | |
668 | whenever the cursor is adjusted to leave a given field. | |
669 | By default, it adjusts the year in date fields if mask is a date, | |
670 | It can be overridden by a derived class to | |
671 | adjust the value of the control at that time. | |
d4b73b1b | 672 | (eg. IpAddrCtrl reformats the address in this way.) |
d14a1e28 RD |
673 | |
674 | ._Change() Called by internal EVT_TEXT handler. Return False to force | |
675 | skip of the normal class change event. | |
676 | ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force | |
677 | skip of the normal class keypress event. | |
678 | ._LostFocus() Called by internal EVT_KILL_FOCUS handler | |
679 | ||
680 | ._OnKeyDown(event) | |
681 | This is the default EVT_KEY_DOWN routine; it just checks for | |
682 | "navigation keys", and if event.ControlDown(), it fires the | |
683 | mixin's _OnChar() routine, as such events are not always seen | |
684 | by the "cooked" EVT_CHAR routine. | |
685 | ||
686 | ._OnChar(event) This is the main EVT_CHAR handler for the | |
687 | wxMaskedEditMixin. | |
688 | ||
689 | The following routines are used to handle standard actions | |
690 | for control keys: | |
691 | _OnArrow(event) used for arrow navigation events | |
692 | _OnCtrl_A(event) 'select all' | |
693 | _OnCtrl_C(event) 'copy' (uses base control function, as copy is non-destructive) | |
694 | _OnCtrl_S(event) 'save' (does nothing) | |
695 | _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste | |
696 | _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection | |
697 | _OnCtrl_Z(event) 'undo' - resets value to previous value (if any) | |
698 | ||
699 | _OnChangeField(event) primarily used for tab events, but can be | |
d4b73b1b | 700 | used for other keys (eg. '.' in IpAddrCtrl) |
d14a1e28 RD |
701 | |
702 | _OnErase(event) used for backspace and delete | |
703 | _OnHome(event) | |
704 | _OnEnd(event) | |
705 | ||
fffd96b7 RD |
706 | The following routine provides a hook back to any class derivations, so that |
707 | they can react to parameter changes before any value is set/reset as a result of | |
708 | those changes. (eg. MaskedComboBox needs to detect when the choices list is | |
709 | modified, either implicitly or explicitly, so it can reset the base control | |
710 | to have the appropriate choice list *before* the initial value is reset to match.) | |
711 | ||
712 | _OnCtrlParametersChanged() | |
713 | ||
714 | Accessor Functions | |
715 | ------------------ | |
716 | For convenience, each class derived from MaskedEditMixin should | |
717 | define an accessors mixin, so that it exposes only those parameters | |
718 | that make sense for the derivation. This is done with an intermediate | |
719 | level of inheritance, ie: | |
720 | ||
721 | class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ): | |
722 | ||
723 | class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): | |
724 | class MaskedNumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ): | |
725 | class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): | |
726 | class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ): | |
727 | ||
728 | etc. | |
729 | ||
730 | Each accessors mixin defines Get/Set functions for the base class parameters | |
731 | that are appropriate for that derivation. | |
732 | This allows the base classes to be "more generic," exposing the widest | |
733 | set of options, while not requiring derived classes to be so general. | |
d14a1e28 RD |
734 | """ |
735 | ||
b881fc78 RD |
736 | import copy |
737 | import difflib | |
738 | import re | |
739 | import string | |
740 | import types | |
741 | ||
742 | import wx | |
743 | ||
744 | # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would | |
745 | # be a good place to implement the 2.3 logger class | |
746 | from wx.tools.dbg import Logger | |
d14a1e28 | 747 | |
d14a1e28 | 748 | dbg = Logger() |
fffd96b7 | 749 | ##dbg(enable=0) |
d14a1e28 RD |
750 | |
751 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
752 | ||
753 | ## Constants for identifying control keys and classes of keys: | |
754 | ||
755 | WXK_CTRL_A = (ord('A')+1) - ord('A') ## These keys are not already defined in wx | |
756 | WXK_CTRL_C = (ord('C')+1) - ord('A') | |
757 | WXK_CTRL_S = (ord('S')+1) - ord('A') | |
758 | WXK_CTRL_V = (ord('V')+1) - ord('A') | |
759 | WXK_CTRL_X = (ord('X')+1) - ord('A') | |
760 | WXK_CTRL_Z = (ord('Z')+1) - ord('A') | |
761 | ||
b881fc78 RD |
762 | nav = ( |
763 | wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB, | |
764 | wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT | |
765 | ) | |
766 | ||
767 | control = ( | |
768 | wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V, | |
769 | WXK_CTRL_X, WXK_CTRL_Z | |
770 | ) | |
d14a1e28 RD |
771 | |
772 | ||
773 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
774 | ||
775 | ## Constants for masking. This is where mask characters | |
776 | ## are defined. | |
777 | ## maskchars used to identify valid mask characters from all others | |
778 | ## #- allow numeric 0-9 only | |
779 | ## A- allow uppercase only. Combine with forceupper to force lowercase to upper | |
780 | ## a- allow lowercase only. Combine with forcelower to force upper to lowercase | |
781 | ## X- allow any character (string.letters, string.punctuation, string.digits) | |
782 | ## Note: locale settings affect what "uppercase", lowercase, etc comprise. | |
783 | ## | |
784 | maskchars = ("#","A","a","X","C","N", '&') | |
785 | ||
786 | months = '(01|02|03|04|05|06|07|08|09|10|11|12)' | |
787 | 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)' | |
788 | charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, | |
789 | 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12} | |
790 | ||
791 | 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)' | |
792 | hours = '(0\d| \d|1[012])' | |
793 | 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)' | |
794 | minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\ | |
795 | 16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\ | |
796 | 36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\ | |
797 | 56|57|58|59)""" | |
798 | seconds = minutes | |
799 | 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' | |
800 | ||
801 | 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(',') | |
802 | ||
803 | state_names = ['Alabama','Alaska','Arizona','Arkansas', | |
804 | 'California','Colorado','Connecticut', | |
805 | 'Delaware','District of Columbia', | |
806 | 'Florida','Georgia','Hawaii', | |
807 | 'Idaho','Illinois','Indiana','Iowa', | |
808 | 'Kansas','Kentucky','Louisiana', | |
809 | 'Maine','Maryland','Massachusetts','Michigan', | |
810 | 'Minnesota','Mississippi','Missouri','Montana', | |
811 | 'Nebraska','Nevada','New Hampshire','New Jersey', | |
812 | 'New Mexico','New York','North Carolina','North Dakokta', | |
813 | 'Ohio','Oklahoma','Oregon', | |
814 | 'Pennsylvania','Puerto Rico','Rhode Island', | |
815 | 'South Carolina','South Dakota', | |
816 | 'Tennessee','Texas','Utah', | |
817 | 'Vermont','Virginia', | |
818 | 'Washington','West Virginia', | |
819 | 'Wisconsin','Wyoming'] | |
820 | ||
821 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
822 | ||
823 | ## The following dictionary defines the current set of autoformats: | |
824 | ||
825 | masktags = { | |
826 | "USPHONEFULLEXT": { | |
827 | 'mask': "(###) ###-#### x:###", | |
828 | 'formatcodes': 'F^->', | |
829 | 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", | |
830 | 'description': "Phone Number w/opt. ext" | |
831 | }, | |
832 | "USPHONETIGHTEXT": { | |
833 | 'mask': "###-###-#### x:###", | |
834 | 'formatcodes': 'F^->', | |
835 | 'validRegex': "^\d{3}-\d{3}-\d{4}", | |
836 | 'description': "Phone Number\n (w/hyphens and opt. ext)" | |
837 | }, | |
838 | "USPHONEFULL": { | |
839 | 'mask': "(###) ###-####", | |
840 | 'formatcodes': 'F^->', | |
841 | 'validRegex': "^\(\d{3}\) \d{3}-\d{4}", | |
842 | 'description': "Phone Number only" | |
843 | }, | |
844 | "USPHONETIGHT": { | |
845 | 'mask': "###-###-####", | |
846 | 'formatcodes': 'F^->', | |
847 | 'validRegex': "^\d{3}-\d{3}-\d{4}", | |
848 | 'description': "Phone Number\n(w/hyphens)" | |
849 | }, | |
850 | "USSTATE": { | |
851 | 'mask': "AA", | |
852 | 'formatcodes': 'F!V', | |
853 | 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'), | |
854 | 'choices': states, | |
855 | 'choiceRequired': True, | |
856 | 'description': "US State Code" | |
857 | }, | |
858 | "USSTATENAME": { | |
859 | 'mask': "ACCCCCCCCCCCCCCCCCCC", | |
860 | 'formatcodes': 'F_', | |
861 | 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'), | |
862 | 'choices': state_names, | |
863 | 'choiceRequired': True, | |
864 | 'description': "US State Name" | |
865 | }, | |
866 | ||
867 | "USDATETIMEMMDDYYYY/HHMMSS": { | |
868 | 'mask': "##/##/#### ##:##:## AM", | |
869 | 'excludeChars': am_pm_exclude, | |
870 | 'formatcodes': 'DF!', | |
871 | 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
872 | 'description': "US Date + Time" | |
873 | }, | |
874 | "USDATETIMEMMDDYYYY-HHMMSS": { | |
875 | 'mask': "##-##-#### ##:##:## AM", | |
876 | 'excludeChars': am_pm_exclude, | |
877 | 'formatcodes': 'DF!', | |
878 | 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
879 | 'description': "US Date + Time\n(w/hypens)" | |
880 | }, | |
fffd96b7 | 881 | "USDATE24HRTIMEMMDDYYYY/HHMMSS": { |
d14a1e28 RD |
882 | 'mask': "##/##/#### ##:##:##", |
883 | 'formatcodes': 'DF', | |
884 | 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 885 | 'description': "US Date + 24Hr (Military) Time" |
d14a1e28 | 886 | }, |
fffd96b7 | 887 | "USDATE24HRTIMEMMDDYYYY-HHMMSS": { |
d14a1e28 RD |
888 | 'mask': "##-##-#### ##:##:##", |
889 | 'formatcodes': 'DF', | |
890 | 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 891 | 'description': "US Date + 24Hr Time\n(w/hypens)" |
d14a1e28 RD |
892 | }, |
893 | "USDATETIMEMMDDYYYY/HHMM": { | |
894 | 'mask': "##/##/#### ##:## AM", | |
895 | 'excludeChars': am_pm_exclude, | |
896 | 'formatcodes': 'DF!', | |
897 | 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', | |
898 | 'description': "US Date + Time\n(without seconds)" | |
899 | }, | |
fffd96b7 | 900 | "USDATE24HRTIMEMMDDYYYY/HHMM": { |
d14a1e28 RD |
901 | 'mask': "##/##/#### ##:##", |
902 | 'formatcodes': 'DF', | |
903 | 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes, | |
fffd96b7 | 904 | 'description': "US Date + 24Hr Time\n(without seconds)" |
d14a1e28 RD |
905 | }, |
906 | "USDATETIMEMMDDYYYY-HHMM": { | |
907 | 'mask': "##-##-#### ##:## AM", | |
908 | 'excludeChars': am_pm_exclude, | |
909 | 'formatcodes': 'DF!', | |
910 | 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', | |
911 | 'description': "US Date + Time\n(w/hypens and w/o secs)" | |
912 | }, | |
fffd96b7 | 913 | "USDATE24HRTIMEMMDDYYYY-HHMM": { |
d14a1e28 RD |
914 | 'mask': "##-##-#### ##:##", |
915 | 'formatcodes': 'DF', | |
916 | 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes, | |
fffd96b7 | 917 | 'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)" |
d14a1e28 RD |
918 | }, |
919 | "USDATEMMDDYYYY/": { | |
920 | 'mask': "##/##/####", | |
921 | 'formatcodes': 'DF', | |
922 | 'validRegex': '^' + months + '/' + days + '/' + '\d{4}', | |
923 | 'description': "US Date\n(MMDDYYYY)" | |
924 | }, | |
925 | "USDATEMMDDYY/": { | |
926 | 'mask': "##/##/##", | |
927 | 'formatcodes': 'DF', | |
928 | 'validRegex': '^' + months + '/' + days + '/\d\d', | |
929 | 'description': "US Date\n(MMDDYY)" | |
930 | }, | |
931 | "USDATEMMDDYYYY-": { | |
932 | 'mask': "##-##-####", | |
933 | 'formatcodes': 'DF', | |
934 | 'validRegex': '^' + months + '-' + days + '-' +'\d{4}', | |
935 | 'description': "MM-DD-YYYY" | |
936 | }, | |
937 | ||
938 | "EUDATEYYYYMMDD/": { | |
939 | 'mask': "####/##/##", | |
940 | 'formatcodes': 'DF', | |
941 | 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days, | |
942 | 'description': "YYYY/MM/DD" | |
943 | }, | |
944 | "EUDATEYYYYMMDD.": { | |
945 | 'mask': "####.##.##", | |
946 | 'formatcodes': 'DF', | |
947 | 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days, | |
948 | 'description': "YYYY.MM.DD" | |
949 | }, | |
950 | "EUDATEDDMMYYYY/": { | |
951 | 'mask': "##/##/####", | |
952 | 'formatcodes': 'DF', | |
953 | 'validRegex': '^' + days + '/' + months + '/' + '\d{4}', | |
954 | 'description': "DD/MM/YYYY" | |
955 | }, | |
956 | "EUDATEDDMMYYYY.": { | |
957 | 'mask': "##.##.####", | |
958 | 'formatcodes': 'DF', | |
959 | 'validRegex': '^' + days + '.' + months + '.' + '\d{4}', | |
960 | 'description': "DD.MM.YYYY" | |
961 | }, | |
962 | "EUDATEDDMMMYYYY.": { | |
963 | 'mask': "##.CCC.####", | |
964 | 'formatcodes': 'DF', | |
965 | 'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}', | |
966 | 'description': "DD.Month.YYYY" | |
967 | }, | |
968 | "EUDATEDDMMMYYYY/": { | |
969 | 'mask': "##/CCC/####", | |
970 | 'formatcodes': 'DF', | |
971 | 'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}', | |
972 | 'description': "DD/Month/YYYY" | |
973 | }, | |
974 | ||
975 | "EUDATETIMEYYYYMMDD/HHMMSS": { | |
976 | 'mask': "####/##/## ##:##:## AM", | |
977 | 'excludeChars': am_pm_exclude, | |
978 | 'formatcodes': 'DF!', | |
979 | 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
980 | 'description': "YYYY/MM/DD HH:MM:SS" | |
981 | }, | |
982 | "EUDATETIMEYYYYMMDD.HHMMSS": { | |
983 | 'mask': "####.##.## ##:##:## AM", | |
984 | 'excludeChars': am_pm_exclude, | |
985 | 'formatcodes': 'DF!', | |
986 | 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
987 | 'description': "YYYY.MM.DD HH:MM:SS" | |
988 | }, | |
989 | "EUDATETIMEDDMMYYYY/HHMMSS": { | |
990 | 'mask': "##/##/#### ##:##:## AM", | |
991 | 'excludeChars': am_pm_exclude, | |
992 | 'formatcodes': 'DF!', | |
993 | 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
994 | 'description': "DD/MM/YYYY HH:MM:SS" | |
995 | }, | |
996 | "EUDATETIMEDDMMYYYY.HHMMSS": { | |
997 | 'mask': "##.##.#### ##:##:## AM", | |
998 | 'excludeChars': am_pm_exclude, | |
999 | 'formatcodes': 'DF!', | |
1000 | 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
1001 | 'description': "DD.MM.YYYY HH:MM:SS" | |
1002 | }, | |
1003 | ||
1004 | "EUDATETIMEYYYYMMDD/HHMM": { | |
1005 | 'mask': "####/##/## ##:## AM", | |
1006 | 'excludeChars': am_pm_exclude, | |
1007 | 'formatcodes': 'DF!', | |
1008 | 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M', | |
1009 | 'description': "YYYY/MM/DD HH:MM" | |
1010 | }, | |
1011 | "EUDATETIMEYYYYMMDD.HHMM": { | |
1012 | 'mask': "####.##.## ##:## AM", | |
1013 | 'excludeChars': am_pm_exclude, | |
1014 | 'formatcodes': 'DF!', | |
1015 | 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M', | |
1016 | 'description': "YYYY.MM.DD HH:MM" | |
1017 | }, | |
1018 | "EUDATETIMEDDMMYYYY/HHMM": { | |
1019 | 'mask': "##/##/#### ##:## AM", | |
1020 | 'excludeChars': am_pm_exclude, | |
1021 | 'formatcodes': 'DF!', | |
1022 | 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', | |
1023 | 'description': "DD/MM/YYYY HH:MM" | |
1024 | }, | |
1025 | "EUDATETIMEDDMMYYYY.HHMM": { | |
1026 | 'mask': "##.##.#### ##:## AM", | |
1027 | 'excludeChars': am_pm_exclude, | |
1028 | 'formatcodes': 'DF!', | |
1029 | 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M', | |
1030 | 'description': "DD.MM.YYYY HH:MM" | |
1031 | }, | |
1032 | ||
fffd96b7 | 1033 | "EUDATE24HRTIMEYYYYMMDD/HHMMSS": { |
d14a1e28 RD |
1034 | 'mask': "####/##/## ##:##:##", |
1035 | 'formatcodes': 'DF', | |
1036 | 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 1037 | 'description': "YYYY/MM/DD 24Hr Time" |
d14a1e28 | 1038 | }, |
fffd96b7 | 1039 | "EUDATE24HRTIMEYYYYMMDD.HHMMSS": { |
d14a1e28 RD |
1040 | 'mask': "####.##.## ##:##:##", |
1041 | 'formatcodes': 'DF', | |
1042 | 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 1043 | 'description': "YYYY.MM.DD 24Hr Time" |
d14a1e28 | 1044 | }, |
fffd96b7 | 1045 | "EUDATE24HRTIMEDDMMYYYY/HHMMSS": { |
d14a1e28 RD |
1046 | 'mask': "##/##/#### ##:##:##", |
1047 | 'formatcodes': 'DF', | |
1048 | 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 1049 | 'description': "DD/MM/YYYY 24Hr Time" |
d14a1e28 | 1050 | }, |
fffd96b7 | 1051 | "EUDATE24HRTIMEDDMMYYYY.HHMMSS": { |
d14a1e28 RD |
1052 | 'mask': "##.##.#### ##:##:##", |
1053 | 'formatcodes': 'DF', | |
1054 | 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 1055 | 'description': "DD.MM.YYYY 24Hr Time" |
d14a1e28 | 1056 | }, |
fffd96b7 | 1057 | "EUDATE24HRTIMEYYYYMMDD/HHMM": { |
d14a1e28 RD |
1058 | 'mask': "####/##/## ##:##", |
1059 | 'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes, | |
fffd96b7 | 1060 | 'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)" |
d14a1e28 | 1061 | }, |
fffd96b7 | 1062 | "EUDATE24HRTIMEYYYYMMDD.HHMM": { |
d14a1e28 RD |
1063 | 'mask': "####.##.## ##:##", |
1064 | 'formatcodes': 'DF', | |
1065 | 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes, | |
fffd96b7 | 1066 | 'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)" |
d14a1e28 | 1067 | }, |
fffd96b7 | 1068 | "EUDATE24HRTIMEDDMMYYYY/HHMM": { |
d14a1e28 RD |
1069 | 'mask': "##/##/#### ##:##", |
1070 | 'formatcodes': 'DF', | |
1071 | 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes, | |
fffd96b7 | 1072 | 'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)" |
d14a1e28 | 1073 | }, |
fffd96b7 | 1074 | "EUDATE24HRTIMEDDMMYYYY.HHMM": { |
d14a1e28 RD |
1075 | 'mask': "##.##.#### ##:##", |
1076 | 'formatcodes': 'DF', | |
1077 | 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes, | |
fffd96b7 | 1078 | 'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)" |
d14a1e28 RD |
1079 | }, |
1080 | ||
1081 | "TIMEHHMMSS": { | |
1082 | 'mask': "##:##:## AM", | |
1083 | 'excludeChars': am_pm_exclude, | |
1084 | 'formatcodes': 'TF!', | |
1085 | 'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M', | |
d4b73b1b | 1086 | 'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)" |
d14a1e28 RD |
1087 | }, |
1088 | "TIMEHHMM": { | |
1089 | 'mask': "##:## AM", | |
1090 | 'excludeChars': am_pm_exclude, | |
1091 | 'formatcodes': 'TF!', | |
1092 | 'validRegex': '^' + hours + ':' + minutes + ' (A|P)M', | |
d4b73b1b | 1093 | 'description': "HH:MM (A|P)M\n(see TimeCtrl)" |
d14a1e28 | 1094 | }, |
fffd96b7 | 1095 | "24HRTIMEHHMMSS": { |
d14a1e28 RD |
1096 | 'mask': "##:##:##", |
1097 | 'formatcodes': 'TF', | |
1098 | 'validRegex': '^' + milhours + ':' + minutes + ':' + seconds, | |
fffd96b7 | 1099 | 'description': "24Hr HH:MM:SS\n(see TimeCtrl)" |
d14a1e28 | 1100 | }, |
fffd96b7 | 1101 | "24HRTIMEHHMM": { |
d14a1e28 RD |
1102 | 'mask': "##:##", |
1103 | 'formatcodes': 'TF', | |
1104 | 'validRegex': '^' + milhours + ':' + minutes, | |
fffd96b7 | 1105 | 'description': "24Hr HH:MM\n(see TimeCtrl)" |
d14a1e28 RD |
1106 | }, |
1107 | "USSOCIALSEC": { | |
1108 | 'mask': "###-##-####", | |
1109 | 'formatcodes': 'F', | |
1110 | 'validRegex': "\d{3}-\d{2}-\d{4}", | |
1111 | 'description': "Social Sec#" | |
1112 | }, | |
1113 | "CREDITCARD": { | |
1114 | 'mask': "####-####-####-####", | |
1115 | 'formatcodes': 'F', | |
1116 | 'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}", | |
1117 | 'description': "Credit Card" | |
1118 | }, | |
1119 | "EXPDATEMMYY": { | |
1120 | 'mask': "##/##", | |
1121 | 'formatcodes': "F", | |
1122 | 'validRegex': "^" + months + "/\d\d", | |
1123 | 'description': "Expiration MM/YY" | |
1124 | }, | |
1125 | "USZIP": { | |
1126 | 'mask': "#####", | |
1127 | 'formatcodes': 'F', | |
1128 | 'validRegex': "^\d{5}", | |
1129 | 'description': "US 5-digit zip code" | |
1130 | }, | |
1131 | "USZIPPLUS4": { | |
1132 | 'mask': "#####-####", | |
1133 | 'formatcodes': 'F', | |
1134 | 'validRegex': "\d{5}-(\s{4}|\d{4})", | |
1135 | 'description': "US zip+4 code" | |
1136 | }, | |
1137 | "PERCENT": { | |
1138 | 'mask': "0.##", | |
1139 | 'formatcodes': 'F', | |
1140 | 'validRegex': "^0.\d\d", | |
1141 | 'description': "Percentage" | |
1142 | }, | |
1143 | "AGE": { | |
1144 | 'mask': "###", | |
1145 | 'formatcodes': "F", | |
1146 | 'validRegex': "^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]", | |
1147 | 'description': "Age" | |
1148 | }, | |
1149 | "EMAIL": { | |
1150 | 'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", | |
1151 | 'excludeChars': " \\/*&%$#!+='\"", | |
1152 | 'formatcodes': "F>", | |
1153 | '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}\]) *$", | |
1154 | 'description': "Email address" | |
1155 | }, | |
1156 | "IPADDR": { | |
1157 | 'mask': "###.###.###.###", | |
1158 | 'formatcodes': 'F_Sr', | |
1159 | 'validRegex': "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}", | |
d4b73b1b | 1160 | 'description': "IP Address\n(see IpAddrCtrl)" |
d14a1e28 RD |
1161 | } |
1162 | } | |
1163 | ||
1164 | # build demo-friendly dictionary of descriptions of autoformats | |
1165 | autoformats = [] | |
1166 | for key, value in masktags.items(): | |
1167 | autoformats.append((key, value['description'])) | |
1168 | autoformats.sort() | |
1169 | ||
1170 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
1171 | ||
1172 | class Field: | |
1173 | valid_params = { | |
1174 | 'index': None, ## which field of mask; set by parent control. | |
1175 | 'mask': "", ## mask chars for this field | |
1176 | 'extent': (), ## (edit start, edit_end) of field; set by parent control. | |
1177 | 'formatcodes': "", ## codes indicating formatting options for the control | |
1178 | 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given | |
1179 | 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits | |
1180 | 'decimalChar': '.', ## used with numeric fields; indicates what char separates integer from fraction | |
1181 | 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard | |
1182 | 'useParensForNegatives': False, ## used with numeric fields, indicates that () should be used vs. - to show negative numbers. | |
1183 | 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar | |
1184 | 'excludeChars': "", ## optional string of chars to exclude even if main mask type does | |
1185 | 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't | |
1186 | 'validRegex': "", ## optional regular expression to use to validate the control | |
1187 | 'validRange': (), ## Optional hi-low range for numerics | |
1188 | 'choices': [], ## Optional list for character expressions | |
1189 | 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list | |
1190 | 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search | |
1191 | 'autoSelect': False, ## Set to True to try auto-completion on each keystroke: | |
1192 | 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl | |
1193 | 'validRequired': False, ## Set to True to disallow input that results in an invalid value | |
1194 | 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID | |
1195 | 'description': "", ## primarily for autoformats, but could be useful elsewhere | |
1196 | } | |
1197 | ||
1198 | # This list contains all parameters that when set at the control level should | |
1199 | # propagate down to each field: | |
1200 | propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives', | |
1201 | 'compareNoCase', 'emptyInvalid', 'validRequired') | |
1202 | ||
1203 | def __init__(self, **kwargs): | |
1204 | """ | |
1205 | This is the "constructor" for setting up parameters for fields. | |
1206 | a field_index of -1 is used to indicate "the entire control." | |
1207 | """ | |
fffd96b7 | 1208 | #### dbg('Field::Field', indent=1) |
d14a1e28 RD |
1209 | # Validate legitimate set of parameters: |
1210 | for key in kwargs.keys(): | |
1211 | if key not in Field.valid_params.keys(): | |
fffd96b7 | 1212 | #### dbg(indent=0) |
d14a1e28 RD |
1213 | raise TypeError('invalid parameter "%s"' % (key)) |
1214 | ||
1215 | # Set defaults for each parameter for this instance, and fully | |
1216 | # populate initial parameter list for configuration: | |
1217 | for key, value in Field.valid_params.items(): | |
1218 | setattr(self, '_' + key, copy.copy(value)) | |
1219 | if not kwargs.has_key(key): | |
1220 | kwargs[key] = copy.copy(value) | |
1221 | ||
1222 | self._autoCompleteIndex = -1 | |
1223 | self._SetParameters(**kwargs) | |
1224 | self._ValidateParameters(**kwargs) | |
1225 | ||
fffd96b7 | 1226 | #### dbg(indent=0) |
d14a1e28 RD |
1227 | |
1228 | ||
1229 | def _SetParameters(self, **kwargs): | |
1230 | """ | |
1231 | This function can be used to set individual or multiple parameters for | |
1232 | a masked edit field parameter after construction. | |
1233 | """ | |
fffd96b7 RD |
1234 | ## dbg(suspend=1) |
1235 | ## dbg('maskededit.Field::_SetParameters', indent=1) | |
d14a1e28 RD |
1236 | # Validate keyword arguments: |
1237 | for key in kwargs.keys(): | |
1238 | if key not in Field.valid_params.keys(): | |
fffd96b7 | 1239 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1240 | raise AttributeError('invalid keyword argument "%s"' % key) |
1241 | ||
1242 | if self._index is not None: dbg('field index:', self._index) | |
fffd96b7 | 1243 | ## dbg('parameters:', indent=1) |
d14a1e28 | 1244 | for key, value in kwargs.items(): |
fffd96b7 RD |
1245 | ## dbg('%s:' % key, value) |
1246 | pass | |
1247 | ## dbg(indent=0) | |
1248 | ||
d14a1e28 RD |
1249 | |
1250 | old_fillChar = self._fillChar # store so we can change choice lists accordingly if it changes | |
1251 | ||
1252 | # First, Assign all parameters specified: | |
1253 | for key in Field.valid_params.keys(): | |
1254 | if kwargs.has_key(key): | |
1255 | setattr(self, '_' + key, kwargs[key] ) | |
1256 | ||
1257 | if kwargs.has_key('formatcodes'): # (set/changed) | |
1258 | self._forceupper = '!' in self._formatcodes | |
1259 | self._forcelower = '^' in self._formatcodes | |
1260 | self._groupdigits = ',' in self._formatcodes | |
1261 | self._okSpaces = '_' in self._formatcodes | |
1262 | self._padZero = '0' in self._formatcodes | |
1263 | self._autofit = 'F' in self._formatcodes | |
1264 | self._insertRight = 'r' in self._formatcodes | |
1265 | self._allowInsert = '>' in self._formatcodes | |
1266 | self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes | |
1267 | self._moveOnFieldFull = not '<' in self._formatcodes | |
1268 | self._selectOnFieldEntry = 'S' in self._formatcodes | |
1269 | ||
1270 | if kwargs.has_key('groupChar'): | |
1271 | self._groupChar = kwargs['groupChar'] | |
1272 | if kwargs.has_key('decimalChar'): | |
1273 | self._decimalChar = kwargs['decimalChar'] | |
1274 | if kwargs.has_key('shiftDecimalChar'): | |
1275 | self._shiftDecimalChar = kwargs['shiftDecimalChar'] | |
1276 | ||
1277 | if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'): | |
1278 | self._regexMask = 'V' in self._formatcodes and self._validRegex | |
1279 | ||
1280 | if kwargs.has_key('fillChar'): | |
1281 | self._old_fillChar = old_fillChar | |
fffd96b7 | 1282 | #### dbg("self._old_fillChar: '%s'" % self._old_fillChar) |
d14a1e28 RD |
1283 | |
1284 | if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed) | |
1285 | self._isInt = isInteger(self._mask) | |
fffd96b7 | 1286 | ## dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask) |
d14a1e28 | 1287 | |
fffd96b7 | 1288 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1289 | |
1290 | ||
1291 | def _ValidateParameters(self, **kwargs): | |
1292 | """ | |
1293 | This function can be used to validate individual or multiple parameters for | |
1294 | a masked edit field parameter after construction. | |
1295 | """ | |
fffd96b7 RD |
1296 | ## dbg(suspend=1) |
1297 | ## dbg('maskededit.Field::_ValidateParameters', indent=1) | |
d14a1e28 | 1298 | if self._index is not None: dbg('field index:', self._index) |
fffd96b7 | 1299 | #### dbg('parameters:', indent=1) |
d14a1e28 | 1300 | ## for key, value in kwargs.items(): |
fffd96b7 RD |
1301 | #### dbg('%s:' % key, value) |
1302 | #### dbg(indent=0) | |
1303 | #### dbg("self._old_fillChar: '%s'" % self._old_fillChar) | |
d14a1e28 RD |
1304 | |
1305 | # Verify proper numeric format params: | |
1306 | if self._groupdigits and self._groupChar == self._decimalChar: | |
fffd96b7 | 1307 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1308 | raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar)) |
1309 | ||
1310 | ||
1311 | # Now go do validation, semantic and inter-dependency parameter processing: | |
1312 | if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed) | |
1313 | ||
1314 | self._compareChoices = [choice.strip() for choice in self._choices] | |
1315 | ||
1316 | if self._compareNoCase and self._choices: | |
1317 | self._compareChoices = [item.lower() for item in self._compareChoices] | |
1318 | ||
1319 | if kwargs.has_key('choices'): | |
1320 | self._autoCompleteIndex = -1 | |
1321 | ||
1322 | ||
1323 | if kwargs.has_key('validRegex'): # (set/changed) | |
1324 | if self._validRegex: | |
1325 | try: | |
1326 | if self._compareNoCase: | |
1327 | self._filter = re.compile(self._validRegex, re.IGNORECASE) | |
1328 | else: | |
1329 | self._filter = re.compile(self._validRegex) | |
1330 | except: | |
fffd96b7 | 1331 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1332 | raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex)) |
1333 | else: | |
1334 | self._filter = None | |
1335 | ||
1336 | if kwargs.has_key('validRange'): # (set/changed) | |
1337 | self._hasRange = False | |
1338 | self._rangeHigh = 0 | |
1339 | self._rangeLow = 0 | |
1340 | if self._validRange: | |
1341 | if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]: | |
fffd96b7 | 1342 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1343 | raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b' |
1344 | % (str(self._index), repr(self._validRange)) ) | |
1345 | ||
1346 | self._hasRange = True | |
1347 | self._rangeLow = self._validRange[0] | |
1348 | self._rangeHigh = self._validRange[1] | |
1349 | ||
1350 | if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)): # (set/changed) | |
1351 | self._hasList = False | |
1352 | if self._choices and type(self._choices) not in (types.TupleType, types.ListType): | |
fffd96b7 | 1353 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1354 | raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) |
1355 | elif len( self._choices) > 0: | |
1356 | for choice in self._choices: | |
1357 | if type(choice) not in (types.StringType, types.UnicodeType): | |
fffd96b7 | 1358 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1359 | raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) |
1360 | ||
1361 | length = len(self._mask) | |
fffd96b7 | 1362 | ## dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight) |
d14a1e28 RD |
1363 | if len(self._choices) and length: |
1364 | if len(self._choices[0]) > length: | |
1365 | # changed mask without respecifying choices; readjust the width as appropriate: | |
1366 | self._choices = [choice.strip() for choice in self._choices] | |
1367 | if self._alignRight: | |
1368 | self._choices = [choice.rjust( length ) for choice in self._choices] | |
1369 | else: | |
1370 | self._choices = [choice.ljust( length ) for choice in self._choices] | |
fffd96b7 | 1371 | ## dbg('aligned choices:', self._choices) |
d14a1e28 RD |
1372 | |
1373 | if hasattr(self, '_template'): | |
1374 | # Verify each choice specified is valid: | |
1375 | for choice in self._choices: | |
1376 | if self.IsEmpty(choice) and not self._validRequired: | |
1377 | # allow empty values even if invalid, (just colored differently) | |
1378 | continue | |
1379 | if not self.IsValid(choice): | |
fffd96b7 | 1380 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1381 | raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) |
1382 | self._hasList = True | |
1383 | ||
fffd96b7 RD |
1384 | #### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0) |
1385 | #### dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar) | |
d14a1e28 RD |
1386 | if kwargs.has_key('fillChar') and len(self._choices) > 0: |
1387 | if kwargs['fillChar'] != ' ': | |
1388 | self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices] | |
1389 | else: | |
1390 | self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices] | |
fffd96b7 | 1391 | ## dbg('updated choices:', self._choices) |
d14a1e28 RD |
1392 | |
1393 | ||
1394 | if kwargs.has_key('autoSelect') and kwargs['autoSelect']: | |
1395 | if not self._hasList: | |
fffd96b7 | 1396 | ## dbg('no list to auto complete; ignoring "autoSelect=True"') |
d14a1e28 RD |
1397 | self._autoSelect = False |
1398 | ||
1399 | # reset field validity assumption: | |
1400 | self._valid = True | |
fffd96b7 | 1401 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1402 | |
1403 | ||
1404 | def _GetParameter(self, paramname): | |
1405 | """ | |
1406 | Routine for retrieving the value of any given parameter | |
1407 | """ | |
1408 | if Field.valid_params.has_key(paramname): | |
1409 | return getattr(self, '_' + paramname) | |
1410 | else: | |
1411 | TypeError('Field._GetParameter: invalid parameter "%s"' % key) | |
1412 | ||
1413 | ||
1414 | def IsEmpty(self, slice): | |
1415 | """ | |
1416 | Indicates whether the specified slice is considered empty for the | |
1417 | field. | |
1418 | """ | |
fffd96b7 | 1419 | ## dbg('Field::IsEmpty("%s")' % slice, indent=1) |
d14a1e28 | 1420 | if not hasattr(self, '_template'): |
fffd96b7 | 1421 | ## dbg(indent=0) |
d14a1e28 RD |
1422 | raise AttributeError('_template') |
1423 | ||
fffd96b7 RD |
1424 | ## dbg('self._template: "%s"' % self._template) |
1425 | ## dbg('self._defaultValue: "%s"' % str(self._defaultValue)) | |
d14a1e28 | 1426 | if slice == self._template and not self._defaultValue: |
fffd96b7 | 1427 | ## dbg(indent=0) |
d14a1e28 RD |
1428 | return True |
1429 | ||
1430 | elif slice == self._template: | |
1431 | empty = True | |
1432 | for pos in range(len(self._template)): | |
fffd96b7 | 1433 | #### dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos]) |
d14a1e28 RD |
1434 | if slice[pos] not in (' ', self._fillChar): |
1435 | empty = False | |
1436 | break | |
fffd96b7 | 1437 | ## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0) |
d14a1e28 RD |
1438 | return empty |
1439 | else: | |
fffd96b7 | 1440 | ## dbg("IsEmpty? 0 (slice doesn't match template)", indent=0) |
d14a1e28 RD |
1441 | return False |
1442 | ||
1443 | ||
1444 | def IsValid(self, slice): | |
1445 | """ | |
1446 | Indicates whether the specified slice is considered a valid value for the | |
1447 | field. | |
1448 | """ | |
fffd96b7 RD |
1449 | ## dbg(suspend=1) |
1450 | ## dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1) | |
d14a1e28 RD |
1451 | valid = True # assume true to start |
1452 | ||
1453 | if self.IsEmpty(slice): | |
fffd96b7 | 1454 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1455 | if self._emptyInvalid: |
1456 | return False | |
1457 | else: | |
1458 | return True | |
1459 | ||
1460 | elif self._hasList and self._choiceRequired: | |
fffd96b7 | 1461 | ## dbg("(member of list required)") |
d14a1e28 RD |
1462 | # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices): |
1463 | if self._fillChar != ' ': | |
1464 | slice = slice.replace(self._fillChar, ' ') | |
fffd96b7 | 1465 | ## dbg('updated slice:"%s"' % slice) |
d14a1e28 RD |
1466 | compareStr = slice.strip() |
1467 | ||
1468 | if self._compareNoCase: | |
1469 | compareStr = compareStr.lower() | |
1470 | valid = compareStr in self._compareChoices | |
1471 | ||
1472 | elif self._hasRange and not self.IsEmpty(slice): | |
fffd96b7 | 1473 | ## dbg('validating against range') |
d14a1e28 RD |
1474 | try: |
1475 | # allow float as well as int ranges (int comparisons for free.) | |
1476 | valid = self._rangeLow <= float(slice) <= self._rangeHigh | |
1477 | except: | |
1478 | valid = False | |
1479 | ||
1480 | elif self._validRegex and self._filter: | |
fffd96b7 | 1481 | ## dbg('validating against regex') |
d14a1e28 RD |
1482 | valid = (re.match( self._filter, slice) is not None) |
1483 | ||
1484 | if valid and self._validFunc: | |
fffd96b7 | 1485 | ## dbg('validating against supplied function') |
d14a1e28 | 1486 | valid = self._validFunc(slice) |
fffd96b7 | 1487 | ## dbg('valid?', valid, indent=0, suspend=0) |
d14a1e28 RD |
1488 | return valid |
1489 | ||
1490 | ||
1491 | def _AdjustField(self, slice): | |
1492 | """ 'Fixes' an integer field. Right or left-justifies, as required.""" | |
fffd96b7 | 1493 | ## dbg('Field::_AdjustField("%s")' % slice, indent=1) |
d14a1e28 | 1494 | length = len(self._mask) |
fffd96b7 RD |
1495 | #### dbg('length(self._mask):', length) |
1496 | #### dbg('self._useParensForNegatives?', self._useParensForNegatives) | |
d14a1e28 RD |
1497 | if self._isInt: |
1498 | if self._useParensForNegatives: | |
1499 | signpos = slice.find('(') | |
1500 | right_signpos = slice.find(')') | |
1501 | intStr = slice.replace('(', '').replace(')', '') # drop sign, if any | |
1502 | else: | |
1503 | signpos = slice.find('-') | |
1504 | intStr = slice.replace( '-', '' ) # drop sign, if any | |
1505 | right_signpos = -1 | |
1506 | ||
1507 | intStr = intStr.replace(' ', '') # drop extra spaces | |
1508 | intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars | |
1509 | intStr = string.replace(intStr,"-","") # drop sign, if any | |
1510 | intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots | |
fffd96b7 | 1511 | #### dbg('intStr:"%s"' % intStr) |
d14a1e28 RD |
1512 | start, end = self._extent |
1513 | field_len = end - start | |
1514 | if not self._padZero and len(intStr) != field_len and intStr.strip(): | |
1515 | intStr = str(long(intStr)) | |
fffd96b7 RD |
1516 | #### dbg('raw int str: "%s"' % intStr) |
1517 | #### dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes) | |
d14a1e28 RD |
1518 | if self._groupdigits: |
1519 | new = '' | |
1520 | cnt = 1 | |
1521 | for i in range(len(intStr)-1, -1, -1): | |
1522 | new = intStr[i] + new | |
1523 | if (cnt) % 3 == 0: | |
1524 | new = self._groupChar + new | |
1525 | cnt += 1 | |
1526 | if new and new[0] == self._groupChar: | |
1527 | new = new[1:] | |
1528 | if len(new) <= length: | |
1529 | # expanded string will still fit and leave room for sign: | |
1530 | intStr = new | |
1531 | # else... leave it without the commas... | |
1532 | ||
fffd96b7 RD |
1533 | ## dbg('padzero?', self._padZero) |
1534 | ## dbg('len(intStr):', len(intStr), 'field length:', length) | |
d14a1e28 RD |
1535 | if self._padZero and len(intStr) < length: |
1536 | intStr = '0' * (length - len(intStr)) + intStr | |
1537 | if signpos != -1: # we had a sign before; restore it | |
1538 | if self._useParensForNegatives: | |
1539 | intStr = '(' + intStr[1:] | |
1540 | if right_signpos != -1: | |
1541 | intStr += ')' | |
1542 | else: | |
1543 | intStr = '-' + intStr[1:] | |
1544 | elif signpos != -1 and slice[0:signpos].strip() == '': # - was before digits | |
1545 | if self._useParensForNegatives: | |
1546 | intStr = '(' + intStr | |
1547 | if right_signpos != -1: | |
1548 | intStr += ')' | |
1549 | else: | |
1550 | intStr = '-' + intStr | |
1551 | elif right_signpos != -1: | |
1552 | # must have had ')' but '(' was before field; re-add ')' | |
1553 | intStr += ')' | |
1554 | slice = intStr | |
1555 | ||
1556 | slice = slice.strip() # drop extra spaces | |
1557 | ||
1558 | if self._alignRight: ## Only if right-alignment is enabled | |
1559 | slice = slice.rjust( length ) | |
1560 | else: | |
1561 | slice = slice.ljust( length ) | |
1562 | if self._fillChar != ' ': | |
1563 | slice = slice.replace(' ', self._fillChar) | |
fffd96b7 | 1564 | ## dbg('adjusted slice: "%s"' % slice, indent=0) |
d14a1e28 RD |
1565 | return slice |
1566 | ||
1567 | ||
1568 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
1569 | ||
d4b73b1b | 1570 | class MaskedEditMixin: |
d14a1e28 RD |
1571 | """ |
1572 | This class allows us to abstract the masked edit functionality that could | |
1573 | be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.) | |
1574 | """ | |
1575 | valid_ctrl_params = { | |
1576 | 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control | |
1577 | 'autoformat': "", ## optional auto-format code to set format from masktags dictionary | |
1578 | 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask | |
1579 | 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year | |
1580 | 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete | |
1581 | 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control | |
1582 | 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently, | |
1583 | ## from field validation constraints | |
1584 | 'emptyBackgroundColour': "White", | |
1585 | 'validBackgroundColour': "White", | |
1586 | 'invalidBackgroundColour': "Yellow", | |
1587 | 'foregroundColour': "Black", | |
1588 | 'signedForegroundColour': "Red", | |
1589 | 'demo': False} | |
1590 | ||
1591 | ||
1592 | def __init__(self, name = 'wxMaskedEdit', **kwargs): | |
1593 | """ | |
1594 | This is the "constructor" for setting up the mixin variable parameters for the composite class. | |
1595 | """ | |
1596 | ||
1597 | self.name = name | |
1598 | ||
1599 | # set up flag for doing optional things to base control if possible | |
1600 | if not hasattr(self, 'controlInitialized'): | |
1601 | self.controlInitialized = False | |
1602 | ||
1603 | # Set internal state var for keeping track of whether or not a character | |
1604 | # action results in a modification of the control, since .SetValue() | |
1605 | # doesn't modify the base control's internal state: | |
1606 | self.modified = False | |
1607 | self._previous_mask = None | |
1608 | ||
1609 | # Validate legitimate set of parameters: | |
1610 | for key in kwargs.keys(): | |
d4b73b1b | 1611 | if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): |
d14a1e28 RD |
1612 | raise TypeError('%s: invalid parameter "%s"' % (name, key)) |
1613 | ||
1614 | ## Set up dictionary that can be used by subclasses to override or add to default | |
1615 | ## behavior for individual characters. Derived subclasses needing to change | |
1616 | ## default behavior for keys can either redefine the default functions for the | |
1617 | ## common keys or add functions for specific keys to this list. Each function | |
1618 | ## added should take the key event as argument, and return False if the key | |
1619 | ## requires no further processing. | |
1620 | ## | |
1621 | ## Initially populated with navigation and function control keys: | |
1622 | self._keyhandlers = { | |
1623 | # default navigation keys and handlers: | |
b881fc78 RD |
1624 | wx.WXK_BACK: self._OnErase, |
1625 | wx.WXK_LEFT: self._OnArrow, | |
1626 | wx.WXK_RIGHT: self._OnArrow, | |
1627 | wx.WXK_UP: self._OnAutoCompleteField, | |
1628 | wx.WXK_DOWN: self._OnAutoCompleteField, | |
1629 | wx.WXK_TAB: self._OnChangeField, | |
1630 | wx.WXK_HOME: self._OnHome, | |
1631 | wx.WXK_END: self._OnEnd, | |
1632 | wx.WXK_RETURN: self._OnReturn, | |
1633 | wx.WXK_PRIOR: self._OnAutoCompleteField, | |
1634 | wx.WXK_NEXT: self._OnAutoCompleteField, | |
d14a1e28 RD |
1635 | |
1636 | # default function control keys and handlers: | |
b881fc78 | 1637 | wx.WXK_DELETE: self._OnErase, |
d14a1e28 RD |
1638 | WXK_CTRL_A: self._OnCtrl_A, |
1639 | WXK_CTRL_C: self._OnCtrl_C, | |
1640 | WXK_CTRL_S: self._OnCtrl_S, | |
1641 | WXK_CTRL_V: self._OnCtrl_V, | |
1642 | WXK_CTRL_X: self._OnCtrl_X, | |
1643 | WXK_CTRL_Z: self._OnCtrl_Z, | |
1644 | } | |
1645 | ||
1646 | ## bind standard navigational and control keycodes to this instance, | |
1647 | ## so that they can be augmented and/or changed in derived classes: | |
1648 | self._nav = list(nav) | |
1649 | self._control = list(control) | |
1650 | ||
1651 | ## Dynamically evaluate and store string constants for mask chars | |
1652 | ## so that locale settings can be made after this module is imported | |
1653 | ## and the controls created after that is done can allow the | |
1654 | ## appropriate characters: | |
1655 | self.maskchardict = { | |
1656 | '#': string.digits, | |
1657 | 'A': string.uppercase, | |
1658 | 'a': string.lowercase, | |
1659 | 'X': string.letters + string.punctuation + string.digits, | |
1660 | 'C': string.letters, | |
1661 | 'N': string.letters + string.digits, | |
1662 | '&': string.punctuation | |
1663 | } | |
1664 | ||
d4b73b1b | 1665 | ## self._ignoreChange is used by MaskedComboBox, because |
d14a1e28 RD |
1666 | ## of the hack necessary to determine the selection; it causes |
1667 | ## EVT_TEXT messages from the combobox to be ignored if set. | |
1668 | self._ignoreChange = False | |
1669 | ||
1670 | # These are used to keep track of previous value, for undo functionality: | |
1671 | self._curValue = None | |
1672 | self._prevValue = None | |
1673 | ||
1674 | self._valid = True | |
1675 | ||
1676 | # Set defaults for each parameter for this instance, and fully | |
1677 | # populate initial parameter list for configuration: | |
d4b73b1b | 1678 | for key, value in MaskedEditMixin.valid_ctrl_params.items(): |
d14a1e28 RD |
1679 | setattr(self, '_' + key, copy.copy(value)) |
1680 | if not kwargs.has_key(key): | |
fffd96b7 | 1681 | #### dbg('%s: "%s"' % (key, repr(value))) |
d14a1e28 RD |
1682 | kwargs[key] = copy.copy(value) |
1683 | ||
1684 | # Create a "field" that holds global parameters for control constraints | |
1685 | self._ctrl_constraints = self._fields[-1] = Field(index=-1) | |
1686 | self.SetCtrlParameters(**kwargs) | |
1687 | ||
1688 | ||
1689 | ||
1690 | def SetCtrlParameters(self, **kwargs): | |
1691 | """ | |
1692 | This public function can be used to set individual or multiple masked edit | |
1693 | parameters after construction. | |
1694 | """ | |
fffd96b7 RD |
1695 | ## dbg(suspend=1) |
1696 | ## dbg('MaskedEditMixin::SetCtrlParameters', indent=1) | |
1697 | #### dbg('kwargs:', indent=1) | |
d14a1e28 | 1698 | ## for key, value in kwargs.items(): |
fffd96b7 RD |
1699 | #### dbg(key, '=', value) |
1700 | #### dbg(indent=0) | |
d14a1e28 RD |
1701 | |
1702 | # Validate keyword arguments: | |
1703 | constraint_kwargs = {} | |
1704 | ctrl_kwargs = {} | |
1705 | for key, value in kwargs.items(): | |
1706 | key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling | |
d4b73b1b | 1707 | if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys(): |
fffd96b7 | 1708 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1709 | raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name)) |
1710 | elif key in Field.valid_params.keys(): | |
1711 | constraint_kwargs[key] = value | |
1712 | else: | |
1713 | ctrl_kwargs[key] = value | |
1714 | ||
1715 | mask = None | |
1716 | reset_args = {} | |
1717 | ||
1718 | if ctrl_kwargs.has_key('autoformat'): | |
1719 | autoformat = ctrl_kwargs['autoformat'] | |
1720 | else: | |
1721 | autoformat = None | |
1722 | ||
fffd96b7 RD |
1723 | # handle "parochial name" backward compatibility: |
1724 | if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys(): | |
1725 | autoformat = autoformat.replace('MILTIME', '24HRTIME') | |
1726 | ||
d14a1e28 | 1727 | if autoformat != self._autoformat and autoformat in masktags.keys(): |
fffd96b7 | 1728 | ## dbg('autoformat:', autoformat) |
d14a1e28 RD |
1729 | self._autoformat = autoformat |
1730 | mask = masktags[self._autoformat]['mask'] | |
1731 | # gather rest of any autoformat parameters: | |
1732 | for param, value in masktags[self._autoformat].items(): | |
1733 | if param == 'mask': continue # (must be present; already accounted for) | |
1734 | constraint_kwargs[param] = value | |
1735 | ||
1736 | elif autoformat and not autoformat in masktags.keys(): | |
1737 | raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat)) | |
1738 | else: | |
fffd96b7 | 1739 | ## dbg('autoformat not selected') |
d14a1e28 RD |
1740 | if kwargs.has_key('mask'): |
1741 | mask = kwargs['mask'] | |
fffd96b7 | 1742 | ## dbg('mask:', mask) |
d14a1e28 RD |
1743 | |
1744 | ## Assign style flags | |
1745 | if mask is None: | |
fffd96b7 | 1746 | ## dbg('preserving previous mask') |
d14a1e28 RD |
1747 | mask = self._previous_mask # preserve previous mask |
1748 | else: | |
fffd96b7 | 1749 | ## dbg('mask (re)set') |
d14a1e28 RD |
1750 | reset_args['reset_mask'] = mask |
1751 | constraint_kwargs['mask'] = mask | |
1752 | ||
1753 | # wipe out previous fields; preserve new control-level constraints | |
1754 | self._fields = {-1: self._ctrl_constraints} | |
1755 | ||
1756 | ||
1757 | if ctrl_kwargs.has_key('fields'): | |
1758 | # do field parameter type validation, and conversion to internal dictionary | |
1759 | # as appropriate: | |
1760 | fields = ctrl_kwargs['fields'] | |
1761 | if type(fields) in (types.ListType, types.TupleType): | |
1762 | for i in range(len(fields)): | |
1763 | field = fields[i] | |
1764 | if not isinstance(field, Field): | |
fffd96b7 | 1765 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1766 | raise AttributeError('invalid type for field parameter: %s' % repr(field)) |
1767 | self._fields[i] = field | |
1768 | ||
1769 | elif type(fields) == types.DictionaryType: | |
1770 | for index, field in fields.items(): | |
1771 | if not isinstance(field, Field): | |
fffd96b7 | 1772 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1773 | raise AttributeError('invalid type for field parameter: %s' % repr(field)) |
1774 | self._fields[index] = field | |
1775 | else: | |
fffd96b7 | 1776 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1777 | raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields)) |
1778 | ||
1779 | # Assign constraint parameters for entire control: | |
fffd96b7 | 1780 | #### dbg('control constraints:', indent=1) |
d14a1e28 | 1781 | ## for key, value in constraint_kwargs.items(): |
fffd96b7 RD |
1782 | #### dbg('%s:' % key, value) |
1783 | #### dbg(indent=0) | |
d14a1e28 RD |
1784 | |
1785 | # determine if changing parameters that should affect the entire control: | |
d4b73b1b | 1786 | for key in MaskedEditMixin.valid_ctrl_params.keys(): |
d14a1e28 RD |
1787 | if key in ( 'mask', 'fields' ): continue # (processed separately) |
1788 | if ctrl_kwargs.has_key(key): | |
1789 | setattr(self, '_' + key, ctrl_kwargs[key]) | |
1790 | ||
1791 | # Validate color parameters, converting strings to named colors and validating | |
1792 | # result if appropriate: | |
1793 | for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour', | |
1794 | 'foregroundColour', 'signedForegroundColour'): | |
1795 | if ctrl_kwargs.has_key(key): | |
1796 | if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType): | |
b881fc78 | 1797 | c = wx.NamedColour(ctrl_kwargs[key]) |
d14a1e28 RD |
1798 | if c.Get() == (-1, -1, -1): |
1799 | raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) | |
1800 | else: | |
1801 | # replace attribute with wxColour object: | |
1802 | setattr(self, '_' + key, c) | |
1803 | # attach a python dynamic attribute to wxColour for debug printouts | |
1804 | c._name = ctrl_kwargs[key] | |
1805 | ||
b881fc78 | 1806 | elif type(ctrl_kwargs[key]) != type(wx.BLACK): |
d14a1e28 RD |
1807 | raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key)) |
1808 | ||
1809 | ||
fffd96b7 | 1810 | ## dbg('self._retainFieldValidation:', self._retainFieldValidation) |
d14a1e28 RD |
1811 | if not self._retainFieldValidation: |
1812 | # Build dictionary of any changing parameters which should be propagated to the | |
1813 | # component fields: | |
1814 | for arg in Field.propagating_params: | |
fffd96b7 RD |
1815 | #### dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg)) |
1816 | #### dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg)) | |
d14a1e28 | 1817 | reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg) |
fffd96b7 | 1818 | #### dbg('reset_args[%s]?' % arg, reset_args[arg]) |
d14a1e28 RD |
1819 | |
1820 | # Set the control-level constraints: | |
1821 | self._ctrl_constraints._SetParameters(**constraint_kwargs) | |
1822 | ||
1823 | # This routine does the bulk of the interdependent parameter processing, determining | |
1824 | # the field extents of the mask if changed, resetting parameters as appropriate, | |
1825 | # determining the overall template value for the control, etc. | |
1826 | self._configure(mask, **reset_args) | |
1827 | ||
1828 | # now that we've propagated the field constraints and mask portions to the | |
1829 | # various fields, validate the constraints | |
1830 | self._ctrl_constraints._ValidateParameters(**constraint_kwargs) | |
1831 | ||
1832 | # Validate that all choices for given fields are at least of the | |
1833 | # necessary length, and that they all would be valid pastes if pasted | |
1834 | # into their respective fields: | |
fffd96b7 | 1835 | #### dbg('validating choices') |
d14a1e28 RD |
1836 | self._validateChoices() |
1837 | ||
1838 | ||
1839 | self._autofit = self._ctrl_constraints._autofit | |
1840 | self._isNeg = False | |
1841 | ||
1842 | self._isDate = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask) | |
1843 | self._isTime = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask) | |
1844 | if self._isDate: | |
1845 | # Set _dateExtent, used in date validation to locate date in string; | |
1846 | # always set as though year will be 4 digits, even if mask only has | |
1847 | # 2 digits, so we can always properly process the intended year for | |
1848 | # date validation (leap years, etc.) | |
1849 | if self._mask.find('CCC') != -1: self._dateExtent = 11 | |
1850 | else: self._dateExtent = 10 | |
1851 | ||
1852 | self._4digityear = len(self._mask) > 8 and self._mask[9] == '#' | |
1853 | ||
1854 | if self._isDate and self._autoformat: | |
1855 | # Auto-decide datestyle: | |
1856 | if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY' | |
1857 | elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD' | |
1858 | elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD' | |
1859 | elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY' | |
1860 | elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY' | |
1861 | ||
fffd96b7 RD |
1862 | # Give derived controls a chance to react to parameter changes before |
1863 | # potentially changing current value of the control. | |
1864 | self._OnCtrlParametersChanged() | |
d14a1e28 RD |
1865 | |
1866 | if self.controlInitialized: | |
1867 | # Then the base control is available for configuration; | |
1868 | # take action on base control based on new settings, as appropriate. | |
1869 | if kwargs.has_key('useFixedWidthFont'): | |
1870 | # Set control font - fixed width by default | |
1871 | self._setFont() | |
1872 | ||
1873 | if reset_args.has_key('reset_mask'): | |
fffd96b7 | 1874 | ## dbg('reset mask') |
d14a1e28 RD |
1875 | curvalue = self._GetValue() |
1876 | if curvalue.strip(): | |
1877 | try: | |
fffd96b7 | 1878 | ## dbg('attempting to _SetInitialValue(%s)' % self._GetValue()) |
d14a1e28 RD |
1879 | self._SetInitialValue(self._GetValue()) |
1880 | except Exception, e: | |
fffd96b7 RD |
1881 | ## dbg('exception caught:', e) |
1882 | ## dbg("current value doesn't work; attempting to reset to template") | |
d14a1e28 RD |
1883 | self._SetInitialValue() |
1884 | else: | |
fffd96b7 | 1885 | ## dbg('attempting to _SetInitialValue() with template') |
d14a1e28 RD |
1886 | self._SetInitialValue() |
1887 | ||
1888 | elif kwargs.has_key('useParensForNegatives'): | |
1889 | newvalue = self._getSignedValue()[0] | |
1890 | ||
1891 | if newvalue is not None: | |
1892 | # Adjust for new mask: | |
1893 | if len(newvalue) < len(self._mask): | |
1894 | newvalue += ' ' | |
1895 | elif len(newvalue) > len(self._mask): | |
1896 | if newvalue[-1] in (' ', ')'): | |
1897 | newvalue = newvalue[:-1] | |
1898 | ||
fffd96b7 | 1899 | ## dbg('reconfiguring value for parens:"%s"' % newvalue) |
d14a1e28 RD |
1900 | self._SetValue(newvalue) |
1901 | ||
1902 | if self._prevValue != newvalue: | |
1903 | self._prevValue = newvalue # disallow undo of sign type | |
1904 | ||
1905 | if self._autofit: | |
fffd96b7 | 1906 | ## dbg('setting client size to:', self._CalcSize()) |
d14a1e28 RD |
1907 | self.SetClientSize(self._CalcSize()) |
1908 | ||
1909 | # Set value/type-specific formatting | |
1910 | self._applyFormatting() | |
fffd96b7 | 1911 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
1912 | |
1913 | def SetMaskParameters(self, **kwargs): | |
1914 | """ old name for this function """ | |
1915 | return self.SetCtrlParameters(**kwargs) | |
1916 | ||
1917 | ||
1918 | def GetCtrlParameter(self, paramname): | |
1919 | """ | |
1920 | Routine for retrieving the value of any given parameter | |
1921 | """ | |
d4b73b1b | 1922 | if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')): |
d14a1e28 RD |
1923 | return getattr(self, '_' + paramname.replace('Color', 'Colour')) |
1924 | elif Field.valid_params.has_key(paramname): | |
1925 | return self._ctrl_constraints._GetParameter(paramname) | |
1926 | else: | |
1927 | TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname)) | |
1928 | ||
1929 | def GetMaskParameter(self, paramname): | |
1930 | """ old name for this function """ | |
1931 | return self.GetCtrlParameter(paramname) | |
1932 | ||
1933 | ||
fffd96b7 RD |
1934 | ## This idea worked, but Boa was unable to use this solution... |
1935 | ## def _attachMethod(self, func): | |
1936 | ## import new | |
1937 | ## setattr(self, func.__name__, new.instancemethod(func, self, self.__class__)) | |
1938 | ## | |
1939 | ## | |
1940 | ## def _DefinePropertyFunctions(exposed_params): | |
1941 | ## for param in exposed_params: | |
1942 | ## propname = param[0].upper() + param[1:] | |
1943 | ## | |
1944 | ## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
1945 | ## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
1946 | ## self._attachMethod(locals()['Set%s' % propname]) | |
1947 | ## self._attachMethod(locals()['Get%s' % propname]) | |
1948 | ## | |
1949 | ## if param.find('Colour') != -1: | |
1950 | ## # add non-british spellings, for backward-compatibility | |
1951 | ## propname.replace('Colour', 'Color') | |
1952 | ## | |
1953 | ## exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
1954 | ## exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
1955 | ## self._attachMethod(locals()['Set%s' % propname]) | |
1956 | ## self._attachMethod(locals()['Get%s' % propname]) | |
1957 | ## | |
d14a1e28 RD |
1958 | |
1959 | ||
1960 | def SetFieldParameters(self, field_index, **kwargs): | |
1961 | """ | |
1962 | Routine provided to modify the parameters of a given field. | |
1963 | Because changes to fields can affect the overall control, | |
1964 | direct access to the fields is prevented, and the control | |
1965 | is always "reconfigured" after setting a field parameter. | |
1966 | """ | |
1967 | if field_index not in self._field_indices: | |
1968 | raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) | |
1969 | # set parameters as requested: | |
1970 | self._fields[field_index]._SetParameters(**kwargs) | |
1971 | ||
1972 | # Possibly reprogram control template due to resulting changes, and ensure | |
1973 | # control-level params are still propagated to fields: | |
1974 | self._configure(self._previous_mask) | |
1975 | self._fields[field_index]._ValidateParameters(**kwargs) | |
1976 | ||
1977 | if self.controlInitialized: | |
1978 | if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'): | |
1979 | self._SetInitialValue() | |
1980 | ||
1981 | if self._autofit: | |
1982 | self.SetClientSize(self._CalcSize()) | |
1983 | ||
1984 | # Set value/type-specific formatting | |
1985 | self._applyFormatting() | |
1986 | ||
1987 | ||
1988 | def GetFieldParameter(self, field_index, paramname): | |
1989 | """ | |
1990 | Routine provided for getting a parameter of an individual field. | |
1991 | """ | |
1992 | if field_index not in self._field_indices: | |
1993 | raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name)) | |
1994 | elif Field.valid_params.has_key(paramname): | |
1995 | return self._fields[field_index]._GetParameter(paramname) | |
1996 | else: | |
1997 | TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname)) | |
1998 | ||
1999 | ||
2000 | def _SetKeycodeHandler(self, keycode, func): | |
2001 | """ | |
2002 | This function adds and/or replaces key event handling functions | |
2003 | used by the control. <func> should take the event as argument | |
2004 | and return False if no further action on the key is necessary. | |
2005 | """ | |
2006 | self._keyhandlers[keycode] = func | |
2007 | ||
2008 | ||
2009 | def _SetKeyHandler(self, char, func): | |
2010 | """ | |
2011 | This function adds and/or replaces key event handling functions | |
2012 | for ascii characters. <func> should take the event as argument | |
2013 | and return False if no further action on the key is necessary. | |
2014 | """ | |
2015 | self._SetKeycodeHandler(ord(char), func) | |
2016 | ||
2017 | ||
2018 | def _AddNavKeycode(self, keycode, handler=None): | |
2019 | """ | |
2020 | This function allows a derived subclass to augment the list of | |
2021 | keycodes that are considered "navigational" keys. | |
2022 | """ | |
2023 | self._nav.append(keycode) | |
2024 | if handler: | |
2025 | self._keyhandlers[keycode] = handler | |
2026 | ||
2027 | ||
2028 | def _AddNavKey(self, char, handler=None): | |
2029 | """ | |
2030 | This function is a convenience function so you don't have to | |
2031 | remember to call ord() for ascii chars to be used for navigation. | |
2032 | """ | |
2033 | self._AddNavKeycode(ord(char), handler) | |
2034 | ||
2035 | ||
2036 | def _GetNavKeycodes(self): | |
2037 | """ | |
2038 | This function retrieves the current list of navigational keycodes for | |
2039 | the control. | |
2040 | """ | |
2041 | return self._nav | |
2042 | ||
2043 | ||
2044 | def _SetNavKeycodes(self, keycode_func_tuples): | |
2045 | """ | |
2046 | This function allows you to replace the current list of keycode processed | |
2047 | as navigation keys, and bind associated optional keyhandlers. | |
2048 | """ | |
2049 | self._nav = [] | |
2050 | for keycode, func in keycode_func_tuples: | |
2051 | self._nav.append(keycode) | |
2052 | if func: | |
2053 | self._keyhandlers[keycode] = func | |
2054 | ||
2055 | ||
2056 | def _processMask(self, mask): | |
2057 | """ | |
2058 | This subroutine expands {n} syntax in mask strings, and looks for escaped | |
2059 | special characters and returns the expanded mask, and an dictionary | |
2060 | of booleans indicating whether or not a given position in the mask is | |
2061 | a mask character or not. | |
2062 | """ | |
fffd96b7 | 2063 | ## dbg('_processMask: mask', mask, indent=1) |
d14a1e28 RD |
2064 | # regular expression for parsing c{n} syntax: |
2065 | rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}') | |
2066 | s = mask | |
2067 | match = rex.search(s) | |
2068 | while match: # found an(other) occurrence | |
2069 | maskchr = s[match.start(1):match.end(1)] # char to be repeated | |
2070 | repcount = int(s[match.start(2):match.end(2)]) # the number of times | |
2071 | replacement = string.join( maskchr * repcount, "") # the resulting substr | |
2072 | s = s[:match.start(1)] + replacement + s[match.end(2)+1:] #account for trailing '}' | |
2073 | match = rex.search(s) # look for another such entry in mask | |
2074 | ||
2075 | self._decimalChar = self._ctrl_constraints._decimalChar | |
2076 | self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar | |
2077 | ||
2078 | self._isFloat = isFloatingPoint(s) and not self._ctrl_constraints._validRegex | |
2079 | self._isInt = isInteger(s) and not self._ctrl_constraints._validRegex | |
2080 | self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt) | |
2081 | self._useParens = self._ctrl_constraints._useParensForNegatives | |
2082 | self._isNeg = False | |
fffd96b7 RD |
2083 | #### dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens) |
2084 | #### dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s), | |
d14a1e28 RD |
2085 | ## 'ctrl regex:', self._ctrl_constraints._validRegex) |
2086 | ||
2087 | if self._signOk and s[0] != ' ': | |
2088 | s = ' ' + s | |
2089 | if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ': | |
2090 | self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue | |
2091 | self._signpos = 0 | |
2092 | ||
2093 | if self._useParens: | |
2094 | s += ' ' | |
2095 | self._ctrl_constraints._defaultValue += ' ' | |
2096 | ||
2097 | # Now, go build up a dictionary of booleans, indexed by position, | |
2098 | # indicating whether or not a given position is masked or not | |
2099 | ismasked = {} | |
2100 | i = 0 | |
2101 | while i < len(s): | |
2102 | if s[i] == '\\': # if escaped character: | |
2103 | ismasked[i] = False # mark position as not a mask char | |
2104 | if i+1 < len(s): # if another char follows... | |
2105 | s = s[:i] + s[i+1:] # elide the '\' | |
2106 | if i+2 < len(s) and s[i+1] == '\\': | |
2107 | # if next char also a '\', char is a literal '\' | |
2108 | s = s[:i] + s[i+1:] # elide the 2nd '\' as well | |
2109 | else: # else if special char, mark position accordingly | |
2110 | ismasked[i] = s[i] in maskchars | |
fffd96b7 | 2111 | #### dbg('ismasked[%d]:' % i, ismasked[i], s) |
d14a1e28 | 2112 | i += 1 # increment to next char |
fffd96b7 RD |
2113 | #### dbg('ismasked:', ismasked) |
2114 | ## dbg('new mask: "%s"' % s, indent=0) | |
d14a1e28 RD |
2115 | |
2116 | return s, ismasked | |
2117 | ||
2118 | ||
2119 | def _calcFieldExtents(self): | |
2120 | """ | |
2121 | Subroutine responsible for establishing/configuring field instances with | |
2122 | indices and editable extents appropriate to the specified mask, and building | |
2123 | the lookup table mapping each position to the corresponding field. | |
2124 | """ | |
2125 | self._lookupField = {} | |
2126 | if self._mask: | |
2127 | ||
2128 | ## Create dictionary of positions,characters in mask | |
2129 | self.maskdict = {} | |
2130 | for charnum in range( len( self._mask)): | |
2131 | self.maskdict[charnum] = self._mask[charnum:charnum+1] | |
2132 | ||
2133 | # For the current mask, create an ordered list of field extents | |
2134 | # and a dictionary of positions that map to field indices: | |
2135 | ||
2136 | if self._signOk: start = 1 | |
2137 | else: start = 0 | |
2138 | ||
2139 | if self._isFloat: | |
2140 | # Skip field "discovery", and just construct a 2-field control with appropriate | |
2141 | # constraints for a floating-point entry. | |
2142 | ||
2143 | # .setdefault always constructs 2nd argument even if not needed, so we do this | |
2144 | # the old-fashioned way... | |
2145 | if not self._fields.has_key(0): | |
2146 | self._fields[0] = Field() | |
2147 | if not self._fields.has_key(1): | |
2148 | self._fields[1] = Field() | |
2149 | ||
2150 | self._decimalpos = string.find( self._mask, '.') | |
fffd96b7 | 2151 | ## dbg('decimal pos =', self._decimalpos) |
d14a1e28 RD |
2152 | |
2153 | formatcodes = self._fields[0]._GetParameter('formatcodes') | |
2154 | if 'R' not in formatcodes: formatcodes += 'R' | |
2155 | self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos), | |
2156 | mask=self._mask[start:self._decimalpos], formatcodes=formatcodes) | |
2157 | end = len(self._mask) | |
2158 | if self._signOk and self._useParens: | |
2159 | end -= 1 | |
2160 | self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end), | |
2161 | mask=self._mask[self._decimalpos+1:end]) | |
2162 | ||
2163 | for i in range(self._decimalpos+1): | |
2164 | self._lookupField[i] = 0 | |
2165 | ||
2166 | for i in range(self._decimalpos+1, len(self._mask)+1): | |
2167 | self._lookupField[i] = 1 | |
2168 | ||
2169 | elif self._isInt: | |
2170 | # Skip field "discovery", and just construct a 1-field control with appropriate | |
2171 | # constraints for a integer entry. | |
2172 | if not self._fields.has_key(0): | |
2173 | self._fields[0] = Field(index=0) | |
2174 | end = len(self._mask) | |
2175 | if self._signOk and self._useParens: | |
2176 | end -= 1 | |
2177 | self._fields[0]._SetParameters(index=0, extent=(start, end), | |
2178 | mask=self._mask[start:end]) | |
2179 | for i in range(len(self._mask)+1): | |
2180 | self._lookupField[i] = 0 | |
2181 | else: | |
2182 | # generic control; parse mask to figure out where the fields are: | |
2183 | field_index = 0 | |
2184 | pos = 0 | |
2185 | i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point: | |
2186 | if i < len(self._mask): # no editable chars! | |
2187 | for j in range(pos, i+1): | |
2188 | self._lookupField[j] = field_index | |
2189 | pos = i # figure out field for 1st editable space: | |
2190 | ||
2191 | while i <= len(self._mask): | |
fffd96b7 | 2192 | #### dbg('searching: outer field loop: i = ', i) |
d14a1e28 | 2193 | if self._isMaskChar(i): |
fffd96b7 | 2194 | #### dbg('1st char is mask char; recording edit_start=', i) |
d14a1e28 RD |
2195 | edit_start = i |
2196 | # Skip to end of editable part of current field: | |
2197 | while i < len(self._mask) and self._isMaskChar(i): | |
2198 | self._lookupField[i] = field_index | |
2199 | i += 1 | |
fffd96b7 | 2200 | #### dbg('edit_end =', i) |
d14a1e28 RD |
2201 | edit_end = i |
2202 | self._lookupField[i] = field_index | |
fffd96b7 | 2203 | #### dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index)) |
d14a1e28 RD |
2204 | if not self._fields.has_key(field_index): |
2205 | kwargs = Field.valid_params.copy() | |
2206 | kwargs['index'] = field_index | |
2207 | kwargs['extent'] = (edit_start, edit_end) | |
2208 | kwargs['mask'] = self._mask[edit_start:edit_end] | |
2209 | self._fields[field_index] = Field(**kwargs) | |
2210 | else: | |
2211 | self._fields[field_index]._SetParameters( | |
2212 | index=field_index, | |
2213 | extent=(edit_start, edit_end), | |
2214 | mask=self._mask[edit_start:edit_end]) | |
2215 | pos = i | |
2216 | i = self._findNextEntry(pos, adjustInsert=False) # go to next field: | |
2217 | if i > pos: | |
2218 | for j in range(pos, i+1): | |
2219 | self._lookupField[j] = field_index | |
2220 | if i >= len(self._mask): | |
2221 | break # if past end, we're done | |
2222 | else: | |
2223 | field_index += 1 | |
fffd96b7 | 2224 | #### dbg('next field:', field_index) |
d14a1e28 RD |
2225 | |
2226 | indices = self._fields.keys() | |
2227 | indices.sort() | |
2228 | self._field_indices = indices[1:] | |
fffd96b7 | 2229 | #### dbg('lookupField map:', indent=1) |
d14a1e28 | 2230 | ## for i in range(len(self._mask)): |
fffd96b7 RD |
2231 | #### dbg('pos %d:' % i, self._lookupField[i]) |
2232 | #### dbg(indent=0) | |
d14a1e28 RD |
2233 | |
2234 | # Verify that all field indices specified are valid for mask: | |
2235 | for index in self._fields.keys(): | |
2236 | if index not in [-1] + self._lookupField.values(): | |
2237 | raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask)) | |
2238 | ||
2239 | ||
2240 | def _calcTemplate(self, reset_fillchar, reset_default): | |
2241 | """ | |
2242 | Subroutine for processing current fillchars and default values for | |
2243 | whole control and individual fields, constructing the resulting | |
2244 | overall template, and adjusting the current value as necessary. | |
2245 | """ | |
2246 | default_set = False | |
2247 | if self._ctrl_constraints._defaultValue: | |
2248 | default_set = True | |
2249 | else: | |
2250 | for field in self._fields.values(): | |
2251 | if field._defaultValue and not reset_default: | |
2252 | default_set = True | |
fffd96b7 | 2253 | ## dbg('default set?', default_set) |
d14a1e28 RD |
2254 | |
2255 | # Determine overall new template for control, and keep track of previous | |
2256 | # values, so that current control value can be modified as appropriate: | |
2257 | if self.controlInitialized: curvalue = list(self._GetValue()) | |
2258 | else: curvalue = None | |
2259 | ||
2260 | if hasattr(self, '_fillChar'): old_fillchars = self._fillChar | |
2261 | else: old_fillchars = None | |
2262 | ||
2263 | if hasattr(self, '_template'): old_template = self._template | |
2264 | else: old_template = None | |
2265 | ||
2266 | self._template = "" | |
2267 | ||
2268 | self._fillChar = {} | |
2269 | reset_value = False | |
2270 | ||
2271 | for field in self._fields.values(): | |
2272 | field._template = "" | |
2273 | ||
2274 | for pos in range(len(self._mask)): | |
fffd96b7 | 2275 | #### dbg('pos:', pos) |
d14a1e28 | 2276 | field = self._FindField(pos) |
fffd96b7 | 2277 | #### dbg('field:', field._index) |
d14a1e28 RD |
2278 | start, end = field._extent |
2279 | ||
2280 | if pos == 0 and self._signOk: | |
2281 | self._template = ' ' # always make 1st 1st position blank, regardless of fillchar | |
2282 | elif self._isFloat and pos == self._decimalpos: | |
2283 | self._template += self._decimalChar | |
2284 | elif self._isMaskChar(pos): | |
2285 | if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar: | |
2286 | fillChar = field._fillChar | |
2287 | else: | |
2288 | fillChar = self._ctrl_constraints._fillChar | |
2289 | self._fillChar[pos] = fillChar | |
2290 | ||
2291 | # Replace any current old fillchar with new one in current value; | |
2292 | # if action required, set reset_value flag so we can take that action | |
2293 | # after we're all done | |
2294 | if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue: | |
2295 | if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar: | |
2296 | reset_value = True | |
2297 | curvalue[pos] = fillChar | |
2298 | ||
2299 | if not field._defaultValue and not self._ctrl_constraints._defaultValue: | |
fffd96b7 | 2300 | #### dbg('no default value') |
d14a1e28 RD |
2301 | self._template += fillChar |
2302 | field._template += fillChar | |
2303 | ||
2304 | elif field._defaultValue and not reset_default: | |
fffd96b7 RD |
2305 | #### dbg('len(field._defaultValue):', len(field._defaultValue)) |
2306 | #### dbg('pos-start:', pos-start) | |
d14a1e28 | 2307 | if len(field._defaultValue) > pos-start: |
fffd96b7 | 2308 | #### dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start]) |
d14a1e28 RD |
2309 | self._template += field._defaultValue[pos-start] |
2310 | field._template += field._defaultValue[pos-start] | |
2311 | else: | |
fffd96b7 | 2312 | #### dbg('field default not long enough; using fillChar') |
d14a1e28 RD |
2313 | self._template += fillChar |
2314 | field._template += fillChar | |
2315 | else: | |
2316 | if len(self._ctrl_constraints._defaultValue) > pos: | |
fffd96b7 | 2317 | #### dbg('using control default') |
d14a1e28 RD |
2318 | self._template += self._ctrl_constraints._defaultValue[pos] |
2319 | field._template += self._ctrl_constraints._defaultValue[pos] | |
2320 | else: | |
fffd96b7 | 2321 | #### dbg('ctrl default not long enough; using fillChar') |
d14a1e28 RD |
2322 | self._template += fillChar |
2323 | field._template += fillChar | |
fffd96b7 RD |
2324 | #### dbg('field[%d]._template now "%s"' % (field._index, field._template)) |
2325 | #### dbg('self._template now "%s"' % self._template) | |
d14a1e28 RD |
2326 | else: |
2327 | self._template += self._mask[pos] | |
2328 | ||
2329 | self._fields[-1]._template = self._template # (for consistency) | |
2330 | ||
2331 | if curvalue: # had an old value, put new one back together | |
2332 | newvalue = string.join(curvalue, "") | |
2333 | else: | |
2334 | newvalue = None | |
2335 | ||
2336 | if default_set: | |
2337 | self._defaultValue = self._template | |
fffd96b7 | 2338 | ## dbg('self._defaultValue:', self._defaultValue) |
d14a1e28 | 2339 | if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue): |
fffd96b7 | 2340 | #### dbg(indent=0) |
d14a1e28 RD |
2341 | raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name)) |
2342 | ||
2343 | # if no fillchar change, but old value == old template, replace it: | |
2344 | if newvalue == old_template: | |
2345 | newvalue = self._template | |
7722248d | 2346 | reset_value = True |
d14a1e28 RD |
2347 | else: |
2348 | self._defaultValue = None | |
2349 | ||
2350 | if reset_value: | |
fffd96b7 | 2351 | ## dbg('resetting value to: "%s"' % newvalue) |
d14a1e28 RD |
2352 | pos = self._GetInsertionPoint() |
2353 | sel_start, sel_to = self._GetSelection() | |
2354 | self._SetValue(newvalue) | |
2355 | self._SetInsertionPoint(pos) | |
2356 | self._SetSelection(sel_start, sel_to) | |
2357 | ||
2358 | ||
2359 | def _propagateConstraints(self, **reset_args): | |
2360 | """ | |
2361 | Subroutine for propagating changes to control-level constraints and | |
2362 | formatting to the individual fields as appropriate. | |
2363 | """ | |
2364 | parent_codes = self._ctrl_constraints._formatcodes | |
2365 | parent_includes = self._ctrl_constraints._includeChars | |
2366 | parent_excludes = self._ctrl_constraints._excludeChars | |
2367 | for i in self._field_indices: | |
2368 | field = self._fields[i] | |
2369 | inherit_args = {} | |
2370 | if len(self._field_indices) == 1: | |
2371 | inherit_args['formatcodes'] = parent_codes | |
2372 | inherit_args['includeChars'] = parent_includes | |
2373 | inherit_args['excludeChars'] = parent_excludes | |
2374 | else: | |
2375 | field_codes = current_codes = field._GetParameter('formatcodes') | |
2376 | for c in parent_codes: | |
2377 | if c not in field_codes: field_codes += c | |
2378 | if field_codes != current_codes: | |
2379 | inherit_args['formatcodes'] = field_codes | |
2380 | ||
2381 | include_chars = current_includes = field._GetParameter('includeChars') | |
2382 | for c in parent_includes: | |
2383 | if not c in include_chars: include_chars += c | |
2384 | if include_chars != current_includes: | |
2385 | inherit_args['includeChars'] = include_chars | |
2386 | ||
2387 | exclude_chars = current_excludes = field._GetParameter('excludeChars') | |
2388 | for c in parent_excludes: | |
2389 | if not c in exclude_chars: exclude_chars += c | |
2390 | if exclude_chars != current_excludes: | |
2391 | inherit_args['excludeChars'] = exclude_chars | |
2392 | ||
2393 | if reset_args.has_key('defaultValue') and reset_args['defaultValue']: | |
2394 | inherit_args['defaultValue'] = "" # (reset for field) | |
2395 | ||
2396 | for param in Field.propagating_params: | |
fffd96b7 RD |
2397 | #### dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param)) |
2398 | #### dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param]) | |
d14a1e28 RD |
2399 | if reset_args.has_key(param): |
2400 | inherit_args[param] = self.GetCtrlParameter(param) | |
fffd96b7 | 2401 | #### dbg('inherit_args[%s]' % param, inherit_args[param]) |
d14a1e28 RD |
2402 | |
2403 | if inherit_args: | |
2404 | field._SetParameters(**inherit_args) | |
2405 | field._ValidateParameters(**inherit_args) | |
2406 | ||
2407 | ||
2408 | def _validateChoices(self): | |
2409 | """ | |
2410 | Subroutine that validates that all choices for given fields are at | |
2411 | least of the necessary length, and that they all would be valid pastes | |
2412 | if pasted into their respective fields. | |
2413 | """ | |
2414 | for field in self._fields.values(): | |
2415 | if field._choices: | |
2416 | index = field._index | |
2417 | if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices: | |
fffd96b7 | 2418 | ## dbg('skipping (duplicate) choice validation of field 0') |
d14a1e28 | 2419 | continue |
fffd96b7 | 2420 | #### dbg('checking for choices for field', field._index) |
d14a1e28 RD |
2421 | start, end = field._extent |
2422 | field_length = end - start | |
fffd96b7 | 2423 | #### dbg('start, end, length:', start, end, field_length) |
d14a1e28 | 2424 | for choice in field._choices: |
fffd96b7 | 2425 | #### dbg('testing "%s"' % choice) |
d14a1e28 RD |
2426 | valid_paste, ignore, replace_to = self._validatePaste(choice, start, end) |
2427 | if not valid_paste: | |
fffd96b7 | 2428 | #### dbg(indent=0) |
d14a1e28 RD |
2429 | raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name)) |
2430 | elif replace_to > end: | |
fffd96b7 | 2431 | #### dbg(indent=0) |
d14a1e28 | 2432 | raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name)) |
fffd96b7 | 2433 | #### dbg(choice, 'valid in field', index) |
d14a1e28 RD |
2434 | |
2435 | ||
2436 | def _configure(self, mask, **reset_args): | |
2437 | """ | |
2438 | This function sets flags for automatic styling options. It is | |
2439 | called whenever a control or field-level parameter is set/changed. | |
2440 | ||
2441 | This routine does the bulk of the interdependent parameter processing, determining | |
2442 | the field extents of the mask if changed, resetting parameters as appropriate, | |
2443 | determining the overall template value for the control, etc. | |
2444 | ||
2445 | reset_args is supplied if called from control's .SetCtrlParameters() | |
2446 | routine, and indicates which if any parameters which can be | |
2447 | overridden by individual fields have been reset by request for the | |
2448 | whole control. | |
2449 | ||
2450 | """ | |
fffd96b7 RD |
2451 | ## dbg(suspend=1) |
2452 | ## dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1) | |
d14a1e28 RD |
2453 | |
2454 | # Preprocess specified mask to expand {n} syntax, handle escaped | |
2455 | # mask characters, etc and build the resulting positionally keyed | |
2456 | # dictionary for which positions are mask vs. template characters: | |
2457 | self._mask, self.ismasked = self._processMask(mask) | |
2458 | self._masklength = len(self._mask) | |
fffd96b7 | 2459 | #### dbg('processed mask:', self._mask) |
d14a1e28 RD |
2460 | |
2461 | # Preserve original mask specified, for subsequent reprocessing | |
2462 | # if parameters change. | |
fffd96b7 | 2463 | ## dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask) |
d14a1e28 RD |
2464 | self._previous_mask = mask # save unexpanded mask for next time |
2465 | # Set expanded mask and extent of field -1 to width of entire control: | |
2466 | self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength)) | |
2467 | ||
2468 | # Go parse mask to determine where each field is, construct field | |
2469 | # instances as necessary, configure them with those extents, and | |
2470 | # build lookup table mapping each position for control to its corresponding | |
2471 | # field. | |
fffd96b7 | 2472 | #### dbg('calculating field extents') |
d14a1e28 RD |
2473 | |
2474 | self._calcFieldExtents() | |
2475 | ||
2476 | ||
2477 | # Go process defaultValues and fillchars to construct the overall | |
2478 | # template, and adjust the current value as necessary: | |
2479 | reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar'] | |
2480 | reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue'] | |
2481 | ||
fffd96b7 | 2482 | #### dbg('calculating template') |
d14a1e28 RD |
2483 | self._calcTemplate(reset_fillchar, reset_default) |
2484 | ||
2485 | # Propagate control-level formatting and character constraints to each | |
2486 | # field if they don't already have them; if only one field, propagate | |
2487 | # control-level validation constraints to field as well: | |
fffd96b7 | 2488 | #### dbg('propagating constraints') |
d14a1e28 RD |
2489 | self._propagateConstraints(**reset_args) |
2490 | ||
2491 | ||
2492 | if self._isFloat and self._fields[0]._groupChar == self._decimalChar: | |
2493 | raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' % | |
2494 | (self._fields[0]._groupChar, self._decimalChar) ) | |
2495 | ||
fffd96b7 | 2496 | #### dbg('fields:', indent=1) |
d14a1e28 | 2497 | ## for i in [-1] + self._field_indices: |
fffd96b7 RD |
2498 | #### dbg('field %d:' % i, self._fields[i].__dict__) |
2499 | #### dbg(indent=0) | |
d14a1e28 RD |
2500 | |
2501 | # Set up special parameters for numeric control, if appropriate: | |
2502 | if self._signOk: | |
2503 | self._signpos = 0 # assume it starts here, but it will move around on floats | |
2504 | signkeys = ['-', '+', ' '] | |
2505 | if self._useParens: | |
2506 | signkeys += ['(', ')'] | |
2507 | for key in signkeys: | |
2508 | keycode = ord(key) | |
2509 | if not self._keyhandlers.has_key(keycode): | |
2510 | self._SetKeyHandler(key, self._OnChangeSign) | |
2511 | ||
2512 | ||
2513 | ||
2514 | if self._isFloat or self._isInt: | |
2515 | if self.controlInitialized: | |
2516 | value = self._GetValue() | |
fffd96b7 | 2517 | #### dbg('value: "%s"' % value, 'len(value):', len(value), |
d14a1e28 RD |
2518 | ## 'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask)) |
2519 | if len(value) < len(self._ctrl_constraints._mask): | |
2520 | newvalue = value | |
2521 | if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1: | |
2522 | newvalue += ' ' | |
2523 | if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1: | |
2524 | newvalue = ' ' + newvalue | |
2525 | if len(newvalue) < len(self._ctrl_constraints._mask): | |
2526 | if self._ctrl_constraints._alignRight: | |
2527 | newvalue = newvalue.rjust(len(self._ctrl_constraints._mask)) | |
2528 | else: | |
2529 | newvalue = newvalue.ljust(len(self._ctrl_constraints._mask)) | |
fffd96b7 RD |
2530 | ## dbg('old value: "%s"' % value) |
2531 | ## dbg('new value: "%s"' % newvalue) | |
d14a1e28 RD |
2532 | try: |
2533 | self._SetValue(newvalue) | |
2534 | except Exception, e: | |
fffd96b7 | 2535 | ## dbg('exception raised:', e, 'resetting to initial value') |
d14a1e28 RD |
2536 | self._SetInitialValue() |
2537 | ||
2538 | elif len(value) > len(self._ctrl_constraints._mask): | |
2539 | newvalue = value | |
2540 | if not self._useParens and newvalue[-1] == ' ': | |
2541 | newvalue = newvalue[:-1] | |
2542 | if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask): | |
2543 | newvalue = newvalue[1:] | |
2544 | if not self._signOk: | |
2545 | newvalue, signpos, right_signpos = self._getSignedValue(newvalue) | |
2546 | ||
fffd96b7 RD |
2547 | ## dbg('old value: "%s"' % value) |
2548 | ## dbg('new value: "%s"' % newvalue) | |
d14a1e28 RD |
2549 | try: |
2550 | self._SetValue(newvalue) | |
2551 | except Exception, e: | |
fffd96b7 | 2552 | ## dbg('exception raised:', e, 'resetting to initial value') |
d14a1e28 RD |
2553 | self._SetInitialValue() |
2554 | elif not self._signOk and ('(' in value or '-' in value): | |
2555 | newvalue, signpos, right_signpos = self._getSignedValue(value) | |
fffd96b7 RD |
2556 | ## dbg('old value: "%s"' % value) |
2557 | ## dbg('new value: "%s"' % newvalue) | |
d14a1e28 RD |
2558 | try: |
2559 | self._SetValue(newvalue) | |
2560 | except e: | |
fffd96b7 | 2561 | ## dbg('exception raised:', e, 'resetting to initial value') |
d14a1e28 RD |
2562 | self._SetInitialValue() |
2563 | ||
2564 | # Replace up/down arrow default handling: | |
2565 | # make down act like tab, up act like shift-tab: | |
2566 | ||
fffd96b7 | 2567 | #### dbg('Registering numeric navigation and control handlers (if not already set)') |
b881fc78 RD |
2568 | if not self._keyhandlers.has_key(wx.WXK_DOWN): |
2569 | self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField) | |
2570 | if not self._keyhandlers.has_key(wx.WXK_UP): | |
2571 | self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField) | |
d14a1e28 RD |
2572 | |
2573 | # On ., truncate contents right of cursor to decimal point (if any) | |
2574 | # leaves cusor after decimal point if floating point, otherwise at 0. | |
2575 | if not self._keyhandlers.has_key(ord(self._decimalChar)): | |
2576 | self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint) | |
2577 | if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)): | |
2578 | self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards) | |
2579 | ||
2580 | # Allow selective insert of groupchar in numbers: | |
2581 | if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)): | |
2582 | self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar) | |
2583 | ||
fffd96b7 | 2584 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
2585 | |
2586 | ||
2587 | def _SetInitialValue(self, value=""): | |
2588 | """ | |
2589 | fills the control with the generated or supplied default value. | |
2590 | It will also set/reset the font if necessary and apply | |
2591 | formatting to the control at this time. | |
2592 | """ | |
fffd96b7 | 2593 | ## dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1) |
d14a1e28 RD |
2594 | if not value: |
2595 | self._prevValue = self._curValue = self._template | |
2596 | # don't apply external validation rules in this case, as template may | |
2597 | # not coincide with "legal" value... | |
2598 | try: | |
2599 | self._SetValue(self._curValue) # note the use of "raw" ._SetValue()... | |
2600 | except Exception, e: | |
fffd96b7 | 2601 | ## dbg('exception thrown:', e, indent=0) |
d14a1e28 RD |
2602 | raise |
2603 | else: | |
2604 | # Otherwise apply validation as appropriate to passed value: | |
fffd96b7 | 2605 | #### dbg('value = "%s", length:' % value, len(value)) |
d14a1e28 RD |
2606 | self._prevValue = self._curValue = value |
2607 | try: | |
2608 | self.SetValue(value) # use public (validating) .SetValue() | |
2609 | except Exception, e: | |
fffd96b7 | 2610 | ## dbg('exception thrown:', e, indent=0) |
d14a1e28 RD |
2611 | raise |
2612 | ||
2613 | ||
2614 | # Set value/type-specific formatting | |
2615 | self._applyFormatting() | |
fffd96b7 | 2616 | ## dbg(indent=0) |
d14a1e28 RD |
2617 | |
2618 | ||
2619 | def _calcSize(self, size=None): | |
2620 | """ Calculate automatic size if allowed; must be called after the base control is instantiated""" | |
fffd96b7 | 2621 | #### dbg('MaskedEditMixin::_calcSize', indent=1) |
b881fc78 | 2622 | cont = (size is None or size == wx.DefaultSize) |
d14a1e28 RD |
2623 | |
2624 | if cont and self._autofit: | |
2625 | sizing_text = 'M' * self._masklength | |
b881fc78 | 2626 | if wx.Platform != "__WXMSW__": # give it a little extra space |
d14a1e28 | 2627 | sizing_text += 'M' |
b881fc78 | 2628 | if wx.Platform == "__WXMAC__": # give it even a little more... |
d14a1e28 | 2629 | sizing_text += 'M' |
fffd96b7 | 2630 | #### dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text) |
d14a1e28 RD |
2631 | w, h = self.GetTextExtent(sizing_text) |
2632 | size = (w+4, self.GetClientSize().height) | |
fffd96b7 | 2633 | #### dbg('size:', size, indent=0) |
d14a1e28 RD |
2634 | return size |
2635 | ||
2636 | ||
2637 | def _setFont(self): | |
2638 | """ Set the control's font typeface -- pass the font name as str.""" | |
fffd96b7 | 2639 | #### dbg('MaskedEditMixin::_setFont', indent=1) |
d14a1e28 | 2640 | if not self._useFixedWidthFont: |
b881fc78 | 2641 | self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) |
d14a1e28 RD |
2642 | else: |
2643 | font = self.GetFont() # get size, weight, etc from current font | |
2644 | ||
fffd96b7 | 2645 | # Set to teletype font (guaranteed to be mappable to all wxWidgets |
d14a1e28 | 2646 | # platforms: |
b881fc78 | 2647 | self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(), |
d14a1e28 | 2648 | font.GetWeight(), font.GetUnderlined()) |
fffd96b7 | 2649 | #### dbg('font string: "%s"' % font.GetNativeFontInfo().ToString()) |
d14a1e28 RD |
2650 | |
2651 | self.SetFont(self._font) | |
fffd96b7 | 2652 | #### dbg(indent=0) |
d14a1e28 RD |
2653 | |
2654 | ||
2655 | def _OnTextChange(self, event): | |
2656 | """ | |
2657 | Handler for EVT_TEXT event. | |
2658 | self._Change() is provided for subclasses, and may return False to | |
2659 | skip this method logic. This function returns True if the event | |
2660 | detected was a legitimate event, or False if it was a "bogus" | |
2661 | EVT_TEXT event. (NOTE: There is currently an issue with calling | |
2662 | .SetValue from within the EVT_CHAR handler that causes duplicate | |
2663 | EVT_TEXT events for the same change.) | |
2664 | """ | |
2665 | newvalue = self._GetValue() | |
fffd96b7 | 2666 | ## dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1) |
d14a1e28 RD |
2667 | bValid = False |
2668 | if self._ignoreChange: # ie. if an "intermediate text change event" | |
fffd96b7 | 2669 | ## dbg(indent=0) |
d14a1e28 RD |
2670 | return bValid |
2671 | ||
2672 | ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue | |
2673 | ## call is generating two (2) EVT_TEXT events. | |
2674 | ## This is the only mechanism I can find to mask this problem: | |
2675 | if newvalue == self._curValue: | |
fffd96b7 RD |
2676 | ## dbg('ignoring bogus text change event', indent=0) |
2677 | pass | |
d14a1e28 | 2678 | else: |
fffd96b7 | 2679 | ## dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue)) |
d14a1e28 RD |
2680 | if self._Change(): |
2681 | if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1: | |
fffd96b7 | 2682 | ## dbg('clearing self._isNeg') |
d14a1e28 RD |
2683 | self._isNeg = False |
2684 | text, self._signpos, self._right_signpos = self._getSignedValue() | |
2685 | self._CheckValid() # Recolor control as appropriate | |
fffd96b7 | 2686 | ## dbg('calling event.Skip()') |
d14a1e28 RD |
2687 | event.Skip() |
2688 | bValid = True | |
2689 | self._prevValue = self._curValue # save for undo | |
2690 | self._curValue = newvalue # Save last seen value for next iteration | |
fffd96b7 | 2691 | ## dbg(indent=0) |
d14a1e28 RD |
2692 | return bValid |
2693 | ||
2694 | ||
2695 | def _OnKeyDown(self, event): | |
2696 | """ | |
2697 | This function allows the control to capture Ctrl-events like Ctrl-tab, | |
2698 | that are not normally seen by the "cooked" EVT_CHAR routine. | |
2699 | """ | |
2700 | # Get keypress value, adjusted by control options (e.g. convert to upper etc) | |
2701 | key = event.GetKeyCode() | |
2702 | if key in self._nav and event.ControlDown(): | |
2703 | # then this is the only place we will likely see these events; | |
2704 | # process them now: | |
fffd96b7 | 2705 | ## dbg('MaskedEditMixin::OnKeyDown: calling _OnChar') |
d14a1e28 RD |
2706 | self._OnChar(event) |
2707 | return | |
2708 | # else allow regular EVT_CHAR key processing | |
2709 | event.Skip() | |
2710 | ||
2711 | ||
2712 | def _OnChar(self, event): | |
2713 | """ | |
2714 | This is the engine of wxMaskedEdit controls. It examines each keystroke, | |
2715 | decides if it's allowed, where it should go or what action to take. | |
2716 | """ | |
fffd96b7 | 2717 | ## dbg('MaskedEditMixin::_OnChar', indent=1) |
d14a1e28 RD |
2718 | |
2719 | # Get keypress value, adjusted by control options (e.g. convert to upper etc) | |
2720 | key = event.GetKeyCode() | |
2721 | orig_pos = self._GetInsertionPoint() | |
2722 | orig_value = self._GetValue() | |
fffd96b7 RD |
2723 | ## dbg('keycode = ', key) |
2724 | ## dbg('current pos = ', orig_pos) | |
2725 | ## dbg('current selection = ', self._GetSelection()) | |
d14a1e28 RD |
2726 | |
2727 | if not self._Keypress(key): | |
fffd96b7 | 2728 | ## dbg(indent=0) |
d14a1e28 RD |
2729 | return |
2730 | ||
2731 | # If no format string for this control, or the control is marked as "read-only", | |
2732 | # skip the rest of the special processing, and just "do the standard thing:" | |
2733 | if not self._mask or not self._IsEditable(): | |
2734 | event.Skip() | |
fffd96b7 | 2735 | ## dbg(indent=0) |
d14a1e28 RD |
2736 | return |
2737 | ||
2738 | # Process navigation and control keys first, with | |
2739 | # position/selection unadulterated: | |
2740 | if key in self._nav + self._control: | |
2741 | if self._keyhandlers.has_key(key): | |
2742 | keep_processing = self._keyhandlers[key](event) | |
2743 | if self._GetValue() != orig_value: | |
2744 | self.modified = True | |
2745 | if not keep_processing: | |
fffd96b7 | 2746 | ## dbg(indent=0) |
d14a1e28 RD |
2747 | return |
2748 | self._applyFormatting() | |
fffd96b7 | 2749 | ## dbg(indent=0) |
d14a1e28 RD |
2750 | return |
2751 | ||
2752 | # Else... adjust the position as necessary for next input key, | |
2753 | # and determine resulting selection: | |
2754 | pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed | |
2755 | sel_start, sel_to = self._GetSelection() ## check for a range of selected text | |
fffd96b7 | 2756 | ## dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to) |
d14a1e28 RD |
2757 | |
2758 | keep_processing = True | |
2759 | # Capture user past end of format field | |
2760 | if pos > len(self.maskdict): | |
fffd96b7 | 2761 | ## dbg("field length exceeded:",pos) |
d14a1e28 RD |
2762 | keep_processing = False |
2763 | ||
2764 | if keep_processing: | |
2765 | if self._isMaskChar(pos): ## Get string of allowed characters for validation | |
2766 | okchars = self._getAllowedChars(pos) | |
2767 | else: | |
fffd96b7 | 2768 | ## dbg('Not a valid position: pos = ', pos,"chars=",maskchars) |
d14a1e28 RD |
2769 | okchars = "" |
2770 | ||
2771 | key = self._adjustKey(pos, key) # apply formatting constraints to key: | |
2772 | ||
2773 | if self._keyhandlers.has_key(key): | |
2774 | # there's an override for default behavior; use override function instead | |
fffd96b7 | 2775 | ## dbg('using supplied key handler:', self._keyhandlers[key]) |
d14a1e28 RD |
2776 | keep_processing = self._keyhandlers[key](event) |
2777 | if self._GetValue() != orig_value: | |
2778 | self.modified = True | |
2779 | if not keep_processing: | |
fffd96b7 | 2780 | ## dbg(indent=0) |
d14a1e28 RD |
2781 | return |
2782 | # else skip default processing, but do final formatting | |
b881fc78 | 2783 | if key < wx.WXK_SPACE or key > 255: |
fffd96b7 | 2784 | ## dbg('key < WXK_SPACE or key > 255') |
d14a1e28 RD |
2785 | event.Skip() # non alphanumeric |
2786 | keep_processing = False | |
2787 | else: | |
2788 | field = self._FindField(pos) | |
fffd96b7 | 2789 | ## dbg("key ='%s'" % chr(key)) |
d14a1e28 | 2790 | if chr(key) == ' ': |
fffd96b7 RD |
2791 | ## dbg('okSpaces?', field._okSpaces) |
2792 | pass | |
d14a1e28 RD |
2793 | |
2794 | ||
2795 | if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars: | |
2796 | keep_processing = False | |
2797 | ||
2798 | if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ): | |
fffd96b7 | 2799 | ## dbg("key allowed by mask") |
d14a1e28 RD |
2800 | # insert key into candidate new value, but don't change control yet: |
2801 | oldstr = self._GetValue() | |
2802 | newstr, newpos, new_select_to, match_field, match_index = self._insertKey( | |
2803 | chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True) | |
fffd96b7 | 2804 | ## dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr) |
d14a1e28 | 2805 | if self._ctrl_constraints._validRequired and not self.IsValid(newstr): |
fffd96b7 | 2806 | ## dbg('not valid; checking to see if adjusted string is:') |
d14a1e28 RD |
2807 | keep_processing = False |
2808 | if self._isFloat and newstr != self._template: | |
2809 | newstr = self._adjustFloat(newstr) | |
fffd96b7 | 2810 | ## dbg('adjusted str:', newstr) |
d14a1e28 | 2811 | if self.IsValid(newstr): |
fffd96b7 | 2812 | ## dbg("it is!") |
d14a1e28 | 2813 | keep_processing = True |
b881fc78 | 2814 | wx.CallAfter(self._SetInsertionPoint, self._decimalpos) |
d14a1e28 | 2815 | if not keep_processing: |
fffd96b7 | 2816 | ## dbg("key disallowed by validation") |
b881fc78 RD |
2817 | if not wx.Validator_IsSilent() and orig_pos == pos: |
2818 | wx.Bell() | |
d14a1e28 RD |
2819 | |
2820 | if keep_processing: | |
2821 | unadjusted = newstr | |
2822 | ||
2823 | # special case: adjust date value as necessary: | |
2824 | if self._isDate and newstr != self._template: | |
2825 | newstr = self._adjustDate(newstr) | |
fffd96b7 | 2826 | ## dbg('adjusted newstr:', newstr) |
d14a1e28 RD |
2827 | |
2828 | if newstr != orig_value: | |
2829 | self.modified = True | |
2830 | ||
b881fc78 | 2831 | wx.CallAfter(self._SetValue, newstr) |
d14a1e28 RD |
2832 | |
2833 | # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits: | |
2834 | if not self.IsDefault() and self._isDate and self._4digityear: | |
2835 | year2dig = self._dateExtent - 2 | |
2836 | if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]: | |
2837 | newpos = pos+2 | |
2838 | ||
b881fc78 | 2839 | wx.CallAfter(self._SetInsertionPoint, newpos) |
d14a1e28 RD |
2840 | |
2841 | if match_field is not None: | |
fffd96b7 | 2842 | ## dbg('matched field') |
d14a1e28 RD |
2843 | self._OnAutoSelect(match_field, match_index) |
2844 | ||
2845 | if new_select_to != newpos: | |
fffd96b7 | 2846 | ## dbg('queuing selection: (%d, %d)' % (newpos, new_select_to)) |
b881fc78 | 2847 | wx.CallAfter(self._SetSelection, newpos, new_select_to) |
d14a1e28 RD |
2848 | else: |
2849 | newfield = self._FindField(newpos) | |
2850 | if newfield != field and newfield._selectOnFieldEntry: | |
fffd96b7 | 2851 | ## dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1])) |
b881fc78 | 2852 | wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1]) |
7722248d | 2853 | keep_processing = False |
d14a1e28 RD |
2854 | |
2855 | elif keep_processing: | |
fffd96b7 | 2856 | ## dbg('char not allowed') |
d14a1e28 | 2857 | keep_processing = False |
b881fc78 RD |
2858 | if (not wx.Validator_IsSilent()) and orig_pos == pos: |
2859 | wx.Bell() | |
d14a1e28 RD |
2860 | |
2861 | self._applyFormatting() | |
2862 | ||
2863 | # Move to next insertion point | |
2864 | if keep_processing and key not in self._nav: | |
2865 | pos = self._GetInsertionPoint() | |
2866 | next_entry = self._findNextEntry( pos ) | |
2867 | if pos != next_entry: | |
fffd96b7 | 2868 | ## dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals()) |
b881fc78 | 2869 | wx.CallAfter(self._SetInsertionPoint, next_entry ) |
d14a1e28 RD |
2870 | |
2871 | if self._isTemplateChar(pos): | |
2872 | self._AdjustField(pos) | |
fffd96b7 | 2873 | ## dbg(indent=0) |
d14a1e28 RD |
2874 | |
2875 | ||
2876 | def _FindFieldExtent(self, pos=None, getslice=False, value=None): | |
2877 | """ returns editable extent of field corresponding to | |
2878 | position pos, and, optionally, the contents of that field | |
2879 | in the control or the value specified. | |
2880 | Template chars are bound to the preceding field. | |
2881 | For masks beginning with template chars, these chars are ignored | |
2882 | when calculating the current field. | |
2883 | ||
2884 | Eg: with template (###) ###-####, | |
2885 | >>> self._FindFieldExtent(pos=0) | |
2886 | 1, 4 | |
2887 | >>> self._FindFieldExtent(pos=1) | |
2888 | 1, 4 | |
2889 | >>> self._FindFieldExtent(pos=5) | |
2890 | 1, 4 | |
2891 | >>> self._FindFieldExtent(pos=6) | |
2892 | 6, 9 | |
2893 | >>> self._FindFieldExtent(pos=10) | |
2894 | 10, 14 | |
2895 | etc. | |
2896 | """ | |
fffd96b7 | 2897 | ## dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1) |
d14a1e28 RD |
2898 | |
2899 | field = self._FindField(pos) | |
2900 | if not field: | |
2901 | if getslice: | |
2902 | return None, None, "" | |
2903 | else: | |
2904 | return None, None | |
2905 | edit_start, edit_end = field._extent | |
2906 | if getslice: | |
2907 | if value is None: value = self._GetValue() | |
2908 | slice = value[edit_start:edit_end] | |
fffd96b7 RD |
2909 | ## dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice) |
2910 | ## dbg(indent=0) | |
d14a1e28 RD |
2911 | return edit_start, edit_end, slice |
2912 | else: | |
fffd96b7 RD |
2913 | ## dbg('edit_start:', edit_start, 'edit_end:', edit_end) |
2914 | ## dbg(indent=0) | |
d14a1e28 RD |
2915 | return edit_start, edit_end |
2916 | ||
2917 | ||
2918 | def _FindField(self, pos=None): | |
2919 | """ | |
2920 | Returns the field instance in which pos resides. | |
2921 | Template chars are bound to the preceding field. | |
2922 | For masks beginning with template chars, these chars are ignored | |
2923 | when calculating the current field. | |
2924 | ||
2925 | """ | |
fffd96b7 | 2926 | #### dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1) |
d14a1e28 RD |
2927 | if pos is None: pos = self._GetInsertionPoint() |
2928 | elif pos < 0 or pos > self._masklength: | |
2929 | raise IndexError('position %s out of range of control' % str(pos)) | |
2930 | ||
2931 | if len(self._fields) == 0: | |
fffd96b7 | 2932 | ## dbg(indent=0) |
d14a1e28 RD |
2933 | return None |
2934 | ||
2935 | # else... | |
fffd96b7 | 2936 | #### dbg(indent=0) |
d14a1e28 RD |
2937 | return self._fields[self._lookupField[pos]] |
2938 | ||
2939 | ||
2940 | def ClearValue(self): | |
2941 | """ Blanks the current control value by replacing it with the default value.""" | |
fffd96b7 | 2942 | ## dbg("MaskedEditMixin::ClearValue - value reset to default value (template)") |
d14a1e28 RD |
2943 | self._SetValue( self._template ) |
2944 | self._SetInsertionPoint(0) | |
2945 | self.Refresh() | |
2946 | ||
2947 | ||
2948 | def _baseCtrlEventHandler(self, event): | |
2949 | """ | |
2950 | This function is used whenever a key should be handled by the base control. | |
2951 | """ | |
2952 | event.Skip() | |
2953 | return False | |
2954 | ||
2955 | ||
2956 | def _OnUpNumeric(self, event): | |
2957 | """ | |
2958 | Makes up-arrow act like shift-tab should; ie. take you to start of | |
2959 | previous field. | |
2960 | """ | |
fffd96b7 | 2961 | ## dbg('MaskedEditMixin::_OnUpNumeric', indent=1) |
d14a1e28 | 2962 | event.m_shiftDown = 1 |
fffd96b7 | 2963 | ## dbg('event.ShiftDown()?', event.ShiftDown()) |
d14a1e28 | 2964 | self._OnChangeField(event) |
fffd96b7 | 2965 | ## dbg(indent=0) |
d14a1e28 RD |
2966 | |
2967 | ||
2968 | def _OnArrow(self, event): | |
2969 | """ | |
2970 | Used in response to left/right navigation keys; makes these actions skip | |
2971 | over mask template chars. | |
2972 | """ | |
fffd96b7 | 2973 | ## dbg("MaskedEditMixin::_OnArrow", indent=1) |
d14a1e28 RD |
2974 | pos = self._GetInsertionPoint() |
2975 | keycode = event.GetKeyCode() | |
2976 | sel_start, sel_to = self._GetSelection() | |
2977 | entry_end = self._goEnd(getPosOnly=True) | |
b881fc78 | 2978 | if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN): |
d14a1e28 RD |
2979 | if( ( not self._isTemplateChar(pos) and pos+1 > entry_end) |
2980 | or ( self._isTemplateChar(pos) and pos >= entry_end) ): | |
fffd96b7 | 2981 | ## dbg("can't advance", indent=0) |
d14a1e28 RD |
2982 | return False |
2983 | elif self._isTemplateChar(pos): | |
2984 | self._AdjustField(pos) | |
b881fc78 | 2985 | elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1): |
fffd96b7 | 2986 | ## dbg('adjusting field') |
d14a1e28 RD |
2987 | self._AdjustField(pos) |
2988 | ||
2989 | # treat as shifted up/down arrows as tab/reverse tab: | |
b881fc78 | 2990 | if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN): |
d14a1e28 RD |
2991 | # remove "shifting" and treat as (forward) tab: |
2992 | event.m_shiftDown = False | |
2993 | keep_processing = self._OnChangeField(event) | |
2994 | ||
2995 | elif self._FindField(pos)._selectOnFieldEntry: | |
b881fc78 | 2996 | if( keycode in (wx.WXK_UP, wx.WXK_LEFT) |
d14a1e28 RD |
2997 | and sel_start != 0 |
2998 | and self._isTemplateChar(sel_start-1) | |
2999 | and sel_start != self._masklength | |
3000 | and not self._signOk and not self._useParens): | |
3001 | ||
3002 | # call _OnChangeField to handle "ctrl-shifted event" | |
3003 | # (which moves to previous field and selects it.) | |
3004 | event.m_shiftDown = True | |
3005 | event.m_ControlDown = True | |
3006 | keep_processing = self._OnChangeField(event) | |
b881fc78 | 3007 | elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT) |
d14a1e28 RD |
3008 | and sel_to != self._masklength |
3009 | and self._isTemplateChar(sel_to)): | |
3010 | ||
3011 | # when changing field to the right, ensure don't accidentally go left instead | |
3012 | event.m_shiftDown = False | |
3013 | keep_processing = self._OnChangeField(event) | |
3014 | else: | |
3015 | # treat arrows as normal, allowing selection | |
3016 | # as appropriate: | |
fffd96b7 | 3017 | ## dbg('using base ctrl event processing') |
d14a1e28 RD |
3018 | event.Skip() |
3019 | else: | |
b881fc78 RD |
3020 | if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT) |
3021 | or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ): | |
3022 | if not wx.Validator_IsSilent(): | |
3023 | wx.Bell() | |
d14a1e28 RD |
3024 | else: |
3025 | # treat arrows as normal, allowing selection | |
3026 | # as appropriate: | |
fffd96b7 | 3027 | ## dbg('using base event processing') |
d14a1e28 RD |
3028 | event.Skip() |
3029 | ||
3030 | keep_processing = False | |
fffd96b7 | 3031 | ## dbg(indent=0) |
d14a1e28 RD |
3032 | return keep_processing |
3033 | ||
3034 | ||
3035 | def _OnCtrl_S(self, event): | |
3036 | """ Default Ctrl-S handler; prints value information if demo enabled. """ | |
fffd96b7 | 3037 | ## dbg("MaskedEditMixin::_OnCtrl_S") |
d14a1e28 | 3038 | if self._demo: |
d4b73b1b | 3039 | print 'MaskedEditMixin.GetValue() = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue()) |
d14a1e28 RD |
3040 | print "Valid? => " + str(self.IsValid()) |
3041 | print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True)) | |
3042 | return False | |
3043 | ||
3044 | ||
3045 | def _OnCtrl_X(self, event=None): | |
3046 | """ Handles ctrl-x keypress in control and Cut operation on context menu. | |
3047 | Should return False to skip other processing. """ | |
fffd96b7 | 3048 | ## dbg("MaskedEditMixin::_OnCtrl_X", indent=1) |
d14a1e28 | 3049 | self.Cut() |
fffd96b7 | 3050 | ## dbg(indent=0) |
d14a1e28 RD |
3051 | return False |
3052 | ||
3053 | def _OnCtrl_C(self, event=None): | |
3054 | """ Handles ctrl-C keypress in control and Copy operation on context menu. | |
3055 | Uses base control handling. Should return False to skip other processing.""" | |
3056 | self.Copy() | |
3057 | return False | |
3058 | ||
3059 | def _OnCtrl_V(self, event=None): | |
3060 | """ Handles ctrl-V keypress in control and Paste operation on context menu. | |
3061 | Should return False to skip other processing. """ | |
fffd96b7 | 3062 | ## dbg("MaskedEditMixin::_OnCtrl_V", indent=1) |
d14a1e28 | 3063 | self.Paste() |
fffd96b7 | 3064 | ## dbg(indent=0) |
d14a1e28 RD |
3065 | return False |
3066 | ||
3067 | def _OnCtrl_Z(self, event=None): | |
3068 | """ Handles ctrl-Z keypress in control and Undo operation on context menu. | |
7722248d | 3069 | Should return False to skip other processing. """ |
fffd96b7 | 3070 | ## dbg("MaskedEditMixin::_OnCtrl_Z", indent=1) |
d14a1e28 | 3071 | self.Undo() |
fffd96b7 | 3072 | ## dbg(indent=0) |
d14a1e28 RD |
3073 | return False |
3074 | ||
3075 | def _OnCtrl_A(self,event=None): | |
3076 | """ Handles ctrl-a keypress in control. Should return False to skip other processing. """ | |
3077 | end = self._goEnd(getPosOnly=True) | |
3078 | if not event or event.ShiftDown(): | |
b881fc78 RD |
3079 | wx.CallAfter(self._SetInsertionPoint, 0) |
3080 | wx.CallAfter(self._SetSelection, 0, self._masklength) | |
d14a1e28 | 3081 | else: |
b881fc78 RD |
3082 | wx.CallAfter(self._SetInsertionPoint, 0) |
3083 | wx.CallAfter(self._SetSelection, 0, end) | |
d14a1e28 RD |
3084 | return False |
3085 | ||
3086 | ||
3087 | def _OnErase(self, event=None): | |
3088 | """ Handles backspace and delete keypress in control. Should return False to skip other processing.""" | |
fffd96b7 | 3089 | ## dbg("MaskedEditMixin::_OnErase", indent=1) |
d14a1e28 RD |
3090 | sel_start, sel_to = self._GetSelection() ## check for a range of selected text |
3091 | ||
3092 | if event is None: # called as action routine from Cut() operation. | |
b881fc78 | 3093 | key = wx.WXK_DELETE |
d14a1e28 RD |
3094 | else: |
3095 | key = event.GetKeyCode() | |
3096 | ||
3097 | field = self._FindField(sel_to) | |
3098 | start, end = field._extent | |
3099 | value = self._GetValue() | |
3100 | oldstart = sel_start | |
3101 | ||
3102 | # If trying to erase beyond "legal" bounds, disallow operation: | |
b881fc78 RD |
3103 | if( (sel_to == 0 and key == wx.WXK_BACK) |
3104 | or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK) | |
3105 | or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight) | |
d14a1e28 RD |
3106 | or (self._signOk and self._useParens |
3107 | and sel_start == sel_to | |
3108 | and sel_to == self._masklength - 1 | |
b881fc78 RD |
3109 | and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ): |
3110 | if not wx.Validator_IsSilent(): | |
3111 | wx.Bell() | |
fffd96b7 | 3112 | ## dbg(indent=0) |
d14a1e28 RD |
3113 | return False |
3114 | ||
3115 | ||
3116 | if( field._insertRight # an insert-right field | |
3117 | and value[start:end] != self._template[start:end] # and field not empty | |
3118 | and sel_start >= start # and selection starts in field | |
3119 | and ((sel_to == sel_start # and no selection | |
3120 | and sel_to == end # and cursor at right edge | |
b881fc78 | 3121 | and key in (wx.WXK_BACK, wx.WXK_DELETE)) # and either delete or backspace key |
d14a1e28 | 3122 | or # or |
b881fc78 | 3123 | (key == wx.WXK_BACK # backspacing |
d14a1e28 RD |
3124 | and (sel_to == end # and selection ends at right edge |
3125 | or sel_to < end and field._allowInsert)) ) ): # or allow right insert at any point in field | |
3126 | ||
fffd96b7 | 3127 | ## dbg('delete left') |
d14a1e28 | 3128 | # if backspace but left of cursor is empty, adjust cursor right before deleting |
b881fc78 | 3129 | while( key == wx.WXK_BACK |
d14a1e28 RD |
3130 | and sel_start == sel_to |
3131 | and sel_start < end | |
3132 | and value[start:sel_start] == self._template[start:sel_start]): | |
3133 | sel_start += 1 | |
3134 | sel_to = sel_start | |
3135 | ||
fffd96b7 | 3136 | ## dbg('sel_start, start:', sel_start, start) |
d14a1e28 RD |
3137 | |
3138 | if sel_start == sel_to: | |
3139 | keep = sel_start -1 | |
3140 | else: | |
3141 | keep = sel_start | |
3142 | newfield = value[start:keep] + value[sel_to:end] | |
3143 | ||
3144 | # handle sign char moving from outside field into the field: | |
3145 | move_sign_into_field = False | |
3146 | if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('): | |
3147 | signchar = value[0] | |
3148 | newfield = signchar + newfield | |
3149 | move_sign_into_field = True | |
fffd96b7 | 3150 | ## dbg('cut newfield: "%s"' % newfield) |
d14a1e28 RD |
3151 | |
3152 | # handle what should fill in from the left: | |
3153 | left = "" | |
3154 | for i in range(start, end - len(newfield)): | |
3155 | if field._padZero: | |
3156 | left += '0' | |
3157 | elif( self._signOk and self._isNeg and i == 1 | |
3158 | and ((self._useParens and newfield.find('(') == -1) | |
3159 | or (not self._useParens and newfield.find('-') == -1)) ): | |
3160 | left += ' ' | |
3161 | else: | |
3162 | left += self._template[i] # this can produce strange results in combination with default values... | |
3163 | newfield = left + newfield | |
fffd96b7 | 3164 | ## dbg('filled newfield: "%s"' % newfield) |
d14a1e28 RD |
3165 | |
3166 | newstr = value[:start] + newfield + value[end:] | |
3167 | ||
3168 | # (handle sign located in "mask position" in front of field prior to delete) | |
3169 | if move_sign_into_field: | |
3170 | newstr = ' ' + newstr[1:] | |
3171 | pos = sel_to | |
3172 | else: | |
3173 | # handle erasure of (left) sign, moving selection accordingly... | |
3174 | if self._signOk and sel_start == 0: | |
3175 | newstr = value = ' ' + value[1:] | |
3176 | sel_start += 1 | |
3177 | ||
3178 | if field._allowInsert and sel_start >= start: | |
3179 | # selection (if any) falls within current insert-capable field: | |
3180 | select_len = sel_to - sel_start | |
3181 | # determine where cursor should end up: | |
b881fc78 | 3182 | if key == wx.WXK_BACK: |
d14a1e28 RD |
3183 | if select_len == 0: |
3184 | newpos = sel_start -1 | |
3185 | else: | |
3186 | newpos = sel_start | |
3187 | erase_to = sel_to | |
3188 | else: | |
3189 | newpos = sel_start | |
3190 | if sel_to == sel_start: | |
3191 | erase_to = sel_to + 1 | |
3192 | else: | |
3193 | erase_to = sel_to | |
3194 | ||
3195 | if self._isTemplateChar(newpos) and select_len == 0: | |
3196 | if self._signOk: | |
3197 | if value[newpos] in ('(', '-'): | |
3198 | newpos += 1 # don't move cusor | |
3199 | newstr = ' ' + value[newpos:] | |
3200 | elif value[newpos] == ')': | |
3201 | # erase right sign, but don't move cursor; (matching left sign handled later) | |
3202 | newstr = value[:newpos] + ' ' | |
3203 | else: | |
3204 | # no deletion; just move cursor | |
3205 | newstr = value | |
3206 | else: | |
3207 | # no deletion; just move cursor | |
3208 | newstr = value | |
3209 | else: | |
3210 | if erase_to > end: erase_to = end | |
3211 | erase_len = erase_to - newpos | |
3212 | ||
3213 | left = value[start:newpos] | |
fffd96b7 | 3214 | ## dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end]) |
d14a1e28 RD |
3215 | right = value[erase_to:end] + self._template[end-erase_len:end] |
3216 | pos_adjust = 0 | |
3217 | if field._alignRight: | |
3218 | rstripped = right.rstrip() | |
3219 | if rstripped != right: | |
3220 | pos_adjust = len(right) - len(rstripped) | |
3221 | right = rstripped | |
3222 | ||
3223 | if not field._insertRight and value[-1] == ')' and end == self._masklength - 1: | |
3224 | # need to shift ) into the field: | |
3225 | right = right[:-1] + ')' | |
3226 | value = value[:-1] + ' ' | |
3227 | ||
3228 | newfield = left+right | |
3229 | if pos_adjust: | |
3230 | newfield = newfield.rjust(end-start) | |
3231 | newpos += pos_adjust | |
fffd96b7 | 3232 | ## dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield)) |
d14a1e28 RD |
3233 | newstr = value[:start] + newfield + value[end:] |
3234 | ||
3235 | pos = newpos | |
3236 | ||
3237 | else: | |
3238 | if sel_start == sel_to: | |
fffd96b7 | 3239 | ## dbg("current sel_start, sel_to:", sel_start, sel_to) |
b881fc78 | 3240 | if key == wx.WXK_BACK: |
d14a1e28 | 3241 | sel_start, sel_to = sel_to-1, sel_to-1 |
fffd96b7 | 3242 | ## dbg("new sel_start, sel_to:", sel_start, sel_to) |
d14a1e28 RD |
3243 | |
3244 | if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''): | |
3245 | # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0: | |
3246 | newchar = '0' | |
3247 | else: | |
3248 | newchar = self._template[sel_to] ## get an original template character to "clear" the current char | |
fffd96b7 | 3249 | ## dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start])) |
d14a1e28 RD |
3250 | |
3251 | if self._isTemplateChar(sel_to): | |
3252 | if sel_to == 0 and self._signOk and value[sel_to] == '-': # erasing "template" sign char | |
3253 | newstr = ' ' + value[1:] | |
3254 | sel_to += 1 | |
3255 | elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('): | |
3256 | # allow "change sign" by removing both parens: | |
3257 | newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' ' | |
3258 | else: | |
3259 | newstr = value | |
3260 | newpos = sel_to | |
3261 | else: | |
3262 | if field._insertRight and sel_start == sel_to: | |
3263 | # force non-insert-right behavior, by selecting char to be replaced: | |
3264 | sel_to += 1 | |
3265 | newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value) | |
3266 | ||
3267 | else: | |
3268 | # selection made | |
3269 | newstr = self._eraseSelection(value, sel_start, sel_to) | |
3270 | ||
3271 | pos = sel_start # put cursor back at beginning of selection | |
3272 | ||
3273 | if self._signOk and self._useParens: | |
3274 | # account for resultant unbalanced parentheses: | |
3275 | left_signpos = newstr.find('(') | |
3276 | right_signpos = newstr.find(')') | |
3277 | ||
3278 | if left_signpos == -1 and right_signpos != -1: | |
3279 | # erased left-sign marker; get rid of right sign marker: | |
3280 | newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:] | |
3281 | ||
3282 | elif left_signpos != -1 and right_signpos == -1: | |
3283 | # erased right-sign marker; get rid of left-sign marker: | |
3284 | newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:] | |
3285 | ||
fffd96b7 RD |
3286 | ## dbg("oldstr:'%s'" % value, 'oldpos:', oldstart) |
3287 | ## dbg("newstr:'%s'" % newstr, 'pos:', pos) | |
d14a1e28 RD |
3288 | |
3289 | # if erasure results in an invalid field, disallow it: | |
fffd96b7 RD |
3290 | ## dbg('field._validRequired?', field._validRequired) |
3291 | ## dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end])) | |
d14a1e28 | 3292 | if field._validRequired and not field.IsValid(newstr[start:end]): |
b881fc78 RD |
3293 | if not wx.Validator_IsSilent(): |
3294 | wx.Bell() | |
fffd96b7 | 3295 | ## dbg(indent=0) |
d14a1e28 RD |
3296 | return False |
3297 | ||
3298 | # if erasure results in an invalid value, disallow it: | |
3299 | if self._ctrl_constraints._validRequired and not self.IsValid(newstr): | |
b881fc78 RD |
3300 | if not wx.Validator_IsSilent(): |
3301 | wx.Bell() | |
fffd96b7 | 3302 | ## dbg(indent=0) |
d14a1e28 RD |
3303 | return False |
3304 | ||
fffd96b7 | 3305 | ## dbg('setting value (later) to', newstr) |
b881fc78 | 3306 | wx.CallAfter(self._SetValue, newstr) |
fffd96b7 | 3307 | ## dbg('setting insertion point (later) to', pos) |
b881fc78 | 3308 | wx.CallAfter(self._SetInsertionPoint, pos) |
fffd96b7 RD |
3309 | ## dbg(indent=0) |
3310 | if newstr != value: | |
3311 | self.modified = True | |
d14a1e28 RD |
3312 | return False |
3313 | ||
3314 | ||
3315 | def _OnEnd(self,event): | |
3316 | """ Handles End keypress in control. Should return False to skip other processing. """ | |
fffd96b7 | 3317 | ## dbg("MaskedEditMixin::_OnEnd", indent=1) |
d14a1e28 RD |
3318 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) |
3319 | if not event.ControlDown(): | |
3320 | end = self._masklength # go to end of control | |
3321 | if self._signOk and self._useParens: | |
3322 | end = end - 1 # account for reserved char at end | |
3323 | else: | |
3324 | end_of_input = self._goEnd(getPosOnly=True) | |
3325 | sel_start, sel_to = self._GetSelection() | |
3326 | if sel_to < pos: sel_to = pos | |
3327 | field = self._FindField(sel_to) | |
3328 | field_end = self._FindField(end_of_input) | |
3329 | ||
3330 | # pick different end point if either: | |
3331 | # - cursor not in same field | |
3332 | # - or at or past last input already | |
3333 | # - or current selection = end of current field: | |
fffd96b7 RD |
3334 | #### dbg('field != field_end?', field != field_end) |
3335 | #### dbg('sel_to >= end_of_input?', sel_to >= end_of_input) | |
d14a1e28 RD |
3336 | if field != field_end or sel_to >= end_of_input: |
3337 | edit_start, edit_end = field._extent | |
fffd96b7 RD |
3338 | #### dbg('edit_end:', edit_end) |
3339 | #### dbg('sel_to:', sel_to) | |
3340 | #### dbg('sel_to == edit_end?', sel_to == edit_end) | |
3341 | #### dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1]) | |
d14a1e28 RD |
3342 | |
3343 | if sel_to == edit_end and field._index < self._field_indices[-1]: | |
3344 | edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end)) # go to end of next field: | |
3345 | end = edit_end | |
fffd96b7 | 3346 | ## dbg('end moved to', end) |
d14a1e28 RD |
3347 | |
3348 | elif sel_to == edit_end and field._index == self._field_indices[-1]: | |
3349 | # already at edit end of last field; select to end of control: | |
3350 | end = self._masklength | |
fffd96b7 | 3351 | ## dbg('end moved to', end) |
d14a1e28 RD |
3352 | else: |
3353 | end = edit_end # select to end of current field | |
fffd96b7 | 3354 | ## dbg('end moved to ', end) |
d14a1e28 RD |
3355 | else: |
3356 | # select to current end of input | |
3357 | end = end_of_input | |
3358 | ||
3359 | ||
fffd96b7 | 3360 | #### dbg('pos:', pos, 'end:', end) |
d14a1e28 RD |
3361 | |
3362 | if event.ShiftDown(): | |
3363 | if not event.ControlDown(): | |
fffd96b7 RD |
3364 | ## dbg("shift-end; select to end of control") |
3365 | pass | |
d14a1e28 | 3366 | else: |
fffd96b7 RD |
3367 | ## dbg("shift-ctrl-end; select to end of non-whitespace") |
3368 | pass | |
b881fc78 RD |
3369 | wx.CallAfter(self._SetInsertionPoint, pos) |
3370 | wx.CallAfter(self._SetSelection, pos, end) | |
d14a1e28 RD |
3371 | else: |
3372 | if not event.ControlDown(): | |
fffd96b7 RD |
3373 | ## dbg('go to end of control:') |
3374 | pass | |
b881fc78 RD |
3375 | wx.CallAfter(self._SetInsertionPoint, end) |
3376 | wx.CallAfter(self._SetSelection, end, end) | |
d14a1e28 | 3377 | |
fffd96b7 | 3378 | ## dbg(indent=0) |
d14a1e28 RD |
3379 | return False |
3380 | ||
3381 | ||
3382 | def _OnReturn(self, event): | |
3383 | """ | |
3384 | Changes the event to look like a tab event, so we can then call | |
3385 | event.Skip() on it, and have the parent form "do the right thing." | |
3386 | """ | |
fffd96b7 | 3387 | ## dbg('MaskedEditMixin::OnReturn') |
b881fc78 | 3388 | event.m_keyCode = wx.WXK_TAB |
d14a1e28 RD |
3389 | event.Skip() |
3390 | ||
3391 | ||
3392 | def _OnHome(self,event): | |
3393 | """ Handles Home keypress in control. Should return False to skip other processing.""" | |
fffd96b7 | 3394 | ## dbg("MaskedEditMixin::_OnHome", indent=1) |
d14a1e28 RD |
3395 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) |
3396 | sel_start, sel_to = self._GetSelection() | |
3397 | ||
3398 | # There are 5 cases here: | |
3399 | ||
3400 | # 1) shift: select from start of control to end of current | |
3401 | # selection. | |
3402 | if event.ShiftDown() and not event.ControlDown(): | |
fffd96b7 | 3403 | ## dbg("shift-home; select to start of control") |
d14a1e28 RD |
3404 | start = 0 |
3405 | end = sel_start | |
3406 | ||
3407 | # 2) no shift, no control: move cursor to beginning of control. | |
3408 | elif not event.ControlDown(): | |
fffd96b7 | 3409 | ## dbg("home; move to start of control") |
d14a1e28 RD |
3410 | start = 0 |
3411 | end = 0 | |
3412 | ||
3413 | # 3) No shift, control: move cursor back to beginning of field; if | |
3414 | # there already, go to beginning of previous field. | |
3415 | # 4) shift, control, start of selection not at beginning of control: | |
3416 | # move sel_start back to start of field; if already there, go to | |
3417 | # start of previous field. | |
3418 | elif( event.ControlDown() | |
3419 | and (not event.ShiftDown() | |
3420 | or (event.ShiftDown() and sel_start > 0) ) ): | |
3421 | if len(self._field_indices) > 1: | |
3422 | field = self._FindField(sel_start) | |
3423 | start, ignore = field._extent | |
3424 | if sel_start == start and field._index != self._field_indices[0]: # go to start of previous field: | |
3425 | start, ignore = self._FindFieldExtent(sel_start-1) | |
3426 | elif sel_start == start: | |
3427 | start = 0 # go to literal beginning if edit start | |
3428 | # not at that point | |
3429 | end_of_field = True | |
3430 | ||
3431 | else: | |
3432 | start = 0 | |
3433 | ||
3434 | if not event.ShiftDown(): | |
fffd96b7 | 3435 | ## dbg("ctrl-home; move to beginning of field") |
d14a1e28 RD |
3436 | end = start |
3437 | else: | |
fffd96b7 | 3438 | ## dbg("shift-ctrl-home; select to beginning of field") |
d14a1e28 RD |
3439 | end = sel_to |
3440 | ||
3441 | else: | |
3442 | # 5) shift, control, start of selection at beginning of control: | |
3443 | # unselect by moving sel_to backward to beginning of current field; | |
3444 | # if already there, move to start of previous field. | |
3445 | start = sel_start | |
3446 | if len(self._field_indices) > 1: | |
3447 | # find end of previous field: | |
3448 | field = self._FindField(sel_to) | |
3449 | if sel_to > start and field._index != self._field_indices[0]: | |
3450 | ignore, end = self._FindFieldExtent(field._extent[0]-1) | |
3451 | else: | |
3452 | end = start | |
3453 | end_of_field = True | |
3454 | else: | |
3455 | end = start | |
3456 | end_of_field = False | |
fffd96b7 | 3457 | ## dbg("shift-ctrl-home; unselect to beginning of field") |
d14a1e28 | 3458 | |
fffd96b7 | 3459 | ## dbg('queuing new sel_start, sel_to:', (start, end)) |
b881fc78 RD |
3460 | wx.CallAfter(self._SetInsertionPoint, start) |
3461 | wx.CallAfter(self._SetSelection, start, end) | |
fffd96b7 | 3462 | ## dbg(indent=0) |
d14a1e28 RD |
3463 | return False |
3464 | ||
3465 | ||
3466 | def _OnChangeField(self, event): | |
3467 | """ | |
3468 | Primarily handles TAB events, but can be used for any key that | |
3469 | designer wants to change fields within a masked edit control. | |
3470 | NOTE: at the moment, although coded to handle shift-TAB and | |
3471 | control-shift-TAB, these events are not sent to the controls | |
3472 | by the framework. | |
3473 | """ | |
fffd96b7 | 3474 | ## dbg('MaskedEditMixin::_OnChangeField', indent = 1) |
d14a1e28 RD |
3475 | # determine end of current field: |
3476 | pos = self._GetInsertionPoint() | |
fffd96b7 | 3477 | ## dbg('current pos:', pos) |
d14a1e28 RD |
3478 | sel_start, sel_to = self._GetSelection() |
3479 | ||
3480 | if self._masklength < 0: # no fields; process tab normally | |
3481 | self._AdjustField(pos) | |
b881fc78 | 3482 | if event.GetKeyCode() == wx.WXK_TAB: |
fffd96b7 | 3483 | ## dbg('tab to next ctrl') |
d14a1e28 RD |
3484 | event.Skip() |
3485 | #else: do nothing | |
fffd96b7 | 3486 | ## dbg(indent=0) |
d14a1e28 RD |
3487 | return False |
3488 | ||
3489 | ||
3490 | if event.ShiftDown(): | |
3491 | ||
3492 | # "Go backward" | |
3493 | ||
3494 | # NOTE: doesn't yet work with SHIFT-tab under wx; the control | |
3495 | # never sees this event! (But I've coded for it should it ever work, | |
d4b73b1b | 3496 | # and it *does* work for '.' in IpAddrCtrl.) |
d14a1e28 RD |
3497 | field = self._FindField(pos) |
3498 | index = field._index | |
3499 | field_start = field._extent[0] | |
3500 | if pos < field_start: | |
fffd96b7 | 3501 | ## dbg('cursor before 1st field; cannot change to a previous field') |
b881fc78 RD |
3502 | if not wx.Validator_IsSilent(): |
3503 | wx.Bell() | |
7722248d | 3504 | return False |
d14a1e28 RD |
3505 | |
3506 | if event.ControlDown(): | |
fffd96b7 | 3507 | ## dbg('queuing select to beginning of field:', field_start, pos) |
b881fc78 RD |
3508 | wx.CallAfter(self._SetInsertionPoint, field_start) |
3509 | wx.CallAfter(self._SetSelection, field_start, pos) | |
fffd96b7 | 3510 | ## dbg(indent=0) |
d14a1e28 RD |
3511 | return False |
3512 | ||
3513 | elif index == 0: | |
3514 | # We're already in the 1st field; process shift-tab normally: | |
3515 | self._AdjustField(pos) | |
b881fc78 | 3516 | if event.GetKeyCode() == wx.WXK_TAB: |
fffd96b7 | 3517 | ## dbg('tab to previous ctrl') |
d14a1e28 RD |
3518 | event.Skip() |
3519 | else: | |
fffd96b7 | 3520 | ## dbg('position at beginning') |
b881fc78 | 3521 | wx.CallAfter(self._SetInsertionPoint, field_start) |
fffd96b7 | 3522 | ## dbg(indent=0) |
d14a1e28 RD |
3523 | return False |
3524 | else: | |
3525 | # find beginning of previous field: | |
3526 | begin_prev = self._FindField(field_start-1)._extent[0] | |
3527 | self._AdjustField(pos) | |
fffd96b7 | 3528 | ## dbg('repositioning to', begin_prev) |
b881fc78 | 3529 | wx.CallAfter(self._SetInsertionPoint, begin_prev) |
d14a1e28 RD |
3530 | if self._FindField(begin_prev)._selectOnFieldEntry: |
3531 | edit_start, edit_end = self._FindFieldExtent(begin_prev) | |
fffd96b7 | 3532 | ## dbg('queuing selection to (%d, %d)' % (edit_start, edit_end)) |
b881fc78 RD |
3533 | wx.CallAfter(self._SetInsertionPoint, edit_start) |
3534 | wx.CallAfter(self._SetSelection, edit_start, edit_end) | |
fffd96b7 | 3535 | ## dbg(indent=0) |
d14a1e28 RD |
3536 | return False |
3537 | ||
3538 | else: | |
3539 | # "Go forward" | |
3540 | field = self._FindField(sel_to) | |
3541 | field_start, field_end = field._extent | |
3542 | if event.ControlDown(): | |
fffd96b7 | 3543 | ## dbg('queuing select to end of field:', pos, field_end) |
b881fc78 RD |
3544 | wx.CallAfter(self._SetInsertionPoint, pos) |
3545 | wx.CallAfter(self._SetSelection, pos, field_end) | |
fffd96b7 | 3546 | ## dbg(indent=0) |
d14a1e28 RD |
3547 | return False |
3548 | else: | |
3549 | if pos < field_start: | |
fffd96b7 | 3550 | ## dbg('cursor before 1st field; go to start of field') |
b881fc78 | 3551 | wx.CallAfter(self._SetInsertionPoint, field_start) |
d14a1e28 | 3552 | if field._selectOnFieldEntry: |
b881fc78 | 3553 | wx.CallAfter(self._SetSelection, field_start, field_end) |
d14a1e28 | 3554 | else: |
b881fc78 | 3555 | wx.CallAfter(self._SetSelection, field_start, field_start) |
d14a1e28 RD |
3556 | return False |
3557 | # else... | |
fffd96b7 RD |
3558 | ## dbg('end of current field:', field_end) |
3559 | ## dbg('go to next field') | |
d14a1e28 RD |
3560 | if field_end == self._fields[self._field_indices[-1]]._extent[1]: |
3561 | self._AdjustField(pos) | |
b881fc78 | 3562 | if event.GetKeyCode() == wx.WXK_TAB: |
fffd96b7 | 3563 | ## dbg('tab to next ctrl') |
d14a1e28 RD |
3564 | event.Skip() |
3565 | else: | |
fffd96b7 | 3566 | ## dbg('position at end') |
b881fc78 | 3567 | wx.CallAfter(self._SetInsertionPoint, field_end) |
fffd96b7 | 3568 | ## dbg(indent=0) |
d14a1e28 RD |
3569 | return False |
3570 | else: | |
3571 | # we have to find the start of the next field | |
3572 | next_pos = self._findNextEntry(field_end) | |
3573 | if next_pos == field_end: | |
fffd96b7 | 3574 | ## dbg('already in last field') |
d14a1e28 | 3575 | self._AdjustField(pos) |
b881fc78 | 3576 | if event.GetKeyCode() == wx.WXK_TAB: |
fffd96b7 | 3577 | ## dbg('tab to next ctrl') |
d14a1e28 RD |
3578 | event.Skip() |
3579 | #else: do nothing | |
fffd96b7 | 3580 | ## dbg(indent=0) |
d14a1e28 RD |
3581 | return False |
3582 | else: | |
3583 | self._AdjustField( pos ) | |
3584 | ||
3585 | # move cursor to appropriate point in the next field and select as necessary: | |
3586 | field = self._FindField(next_pos) | |
3587 | edit_start, edit_end = field._extent | |
3588 | if field._selectOnFieldEntry: | |
fffd96b7 | 3589 | ## dbg('move to ', next_pos) |
b881fc78 | 3590 | wx.CallAfter(self._SetInsertionPoint, next_pos) |
d14a1e28 | 3591 | edit_start, edit_end = self._FindFieldExtent(next_pos) |
fffd96b7 | 3592 | ## dbg('queuing select', edit_start, edit_end) |
b881fc78 | 3593 | wx.CallAfter(self._SetSelection, edit_start, edit_end) |
d14a1e28 RD |
3594 | else: |
3595 | if field._insertRight: | |
3596 | next_pos = field._extent[1] | |
fffd96b7 | 3597 | ## dbg('move to ', next_pos) |
b881fc78 | 3598 | wx.CallAfter(self._SetInsertionPoint, next_pos) |
fffd96b7 | 3599 | ## dbg(indent=0) |
d14a1e28 RD |
3600 | return False |
3601 | ||
3602 | ||
3603 | def _OnDecimalPoint(self, event): | |
fffd96b7 | 3604 | ## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1) |
d14a1e28 RD |
3605 | |
3606 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) | |
3607 | ||
3608 | if self._isFloat: ## handle float value, move to decimal place | |
fffd96b7 | 3609 | ## dbg('key == Decimal tab; decimal pos:', self._decimalpos) |
d14a1e28 RD |
3610 | value = self._GetValue() |
3611 | if pos < self._decimalpos: | |
3612 | clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:] | |
fffd96b7 | 3613 | ## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) |
d14a1e28 RD |
3614 | newstr = self._adjustFloat(clipped_text) |
3615 | else: | |
3616 | newstr = self._adjustFloat(value) | |
b881fc78 | 3617 | wx.CallAfter(self._SetValue, newstr) |
d14a1e28 RD |
3618 | fraction = self._fields[1] |
3619 | start, end = fraction._extent | |
b881fc78 | 3620 | wx.CallAfter(self._SetInsertionPoint, start) |
d14a1e28 | 3621 | if fraction._selectOnFieldEntry: |
fffd96b7 | 3622 | ## dbg('queuing selection after decimal point to:', (start, end)) |
b881fc78 | 3623 | wx.CallAfter(self._SetSelection, start, end) |
d14a1e28 RD |
3624 | keep_processing = False |
3625 | ||
3626 | if self._isInt: ## handle integer value, truncate from current position | |
fffd96b7 | 3627 | ## dbg('key == Integer decimal event') |
d14a1e28 RD |
3628 | value = self._GetValue() |
3629 | clipped_text = value[0:pos] | |
fffd96b7 | 3630 | ## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text) |
d14a1e28 | 3631 | newstr = self._adjustInt(clipped_text) |
fffd96b7 | 3632 | ## dbg('newstr: "%s"' % newstr) |
b881fc78 | 3633 | wx.CallAfter(self._SetValue, newstr) |
d14a1e28 RD |
3634 | newpos = len(newstr.rstrip()) |
3635 | if newstr.find(')') != -1: | |
3636 | newpos -= 1 # (don't move past right paren) | |
b881fc78 | 3637 | wx.CallAfter(self._SetInsertionPoint, newpos) |
d14a1e28 | 3638 | keep_processing = False |
fffd96b7 | 3639 | ## dbg(indent=0) |
d14a1e28 RD |
3640 | |
3641 | ||
3642 | def _OnChangeSign(self, event): | |
fffd96b7 | 3643 | ## dbg('MaskedEditMixin::_OnChangeSign', indent=1) |
d14a1e28 RD |
3644 | key = event.GetKeyCode() |
3645 | pos = self._adjustPos(self._GetInsertionPoint(), key) | |
3646 | value = self._eraseSelection() | |
3647 | integer = self._fields[0] | |
3648 | start, end = integer._extent | |
3649 | ||
fffd96b7 | 3650 | #### dbg('adjusted pos:', pos) |
d14a1e28 RD |
3651 | if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos): |
3652 | cursign = self._isNeg | |
fffd96b7 | 3653 | ## dbg('cursign:', cursign) |
d14a1e28 RD |
3654 | if chr(key) in ('-','(', ')'): |
3655 | self._isNeg = (not self._isNeg) ## flip value | |
3656 | else: | |
3657 | self._isNeg = False | |
fffd96b7 | 3658 | ## dbg('isNeg?', self._isNeg) |
d14a1e28 RD |
3659 | |
3660 | text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value) | |
fffd96b7 | 3661 | ## dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos) |
d14a1e28 RD |
3662 | if text is None: |
3663 | text = value | |
3664 | ||
3665 | if self._isNeg and self._signpos is not None and self._signpos != -1: | |
3666 | if self._useParens and self._right_signpos is not None: | |
3667 | text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:] | |
3668 | else: | |
3669 | text = text[:self._signpos] + '-' + text[self._signpos+1:] | |
3670 | else: | |
fffd96b7 | 3671 | #### dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text)) |
d14a1e28 RD |
3672 | if self._useParens: |
3673 | text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:] | |
3674 | else: | |
3675 | text = text[:self._signpos] + ' ' + text[self._signpos+1:] | |
fffd96b7 | 3676 | ## dbg('clearing self._isNeg') |
d14a1e28 RD |
3677 | self._isNeg = False |
3678 | ||
b881fc78 RD |
3679 | wx.CallAfter(self._SetValue, text) |
3680 | wx.CallAfter(self._applyFormatting) | |
fffd96b7 | 3681 | ## dbg('pos:', pos, 'signpos:', self._signpos) |
d14a1e28 | 3682 | if pos == self._signpos or integer.IsEmpty(text[start:end]): |
b881fc78 | 3683 | wx.CallAfter(self._SetInsertionPoint, self._signpos+1) |
d14a1e28 | 3684 | else: |
b881fc78 | 3685 | wx.CallAfter(self._SetInsertionPoint, pos) |
d14a1e28 RD |
3686 | |
3687 | keep_processing = False | |
3688 | else: | |
3689 | keep_processing = True | |
fffd96b7 | 3690 | ## dbg(indent=0) |
d14a1e28 RD |
3691 | return keep_processing |
3692 | ||
3693 | ||
3694 | def _OnGroupChar(self, event): | |
3695 | """ | |
3696 | This handler is only registered if the mask is a numeric mask. | |
3697 | It allows the insertion of ',' or '.' if appropriate. | |
3698 | """ | |
fffd96b7 | 3699 | ## dbg('MaskedEditMixin::_OnGroupChar', indent=1) |
d14a1e28 RD |
3700 | keep_processing = True |
3701 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) | |
3702 | sel_start, sel_to = self._GetSelection() | |
3703 | groupchar = self._fields[0]._groupChar | |
3704 | if not self._isCharAllowed(groupchar, pos, checkRegex=True): | |
3705 | keep_processing = False | |
b881fc78 RD |
3706 | if not wx.Validator_IsSilent(): |
3707 | wx.Bell() | |
d14a1e28 RD |
3708 | |
3709 | if keep_processing: | |
3710 | newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() ) | |
fffd96b7 | 3711 | ## dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr) |
d14a1e28 RD |
3712 | if self._ctrl_constraints._validRequired and not self.IsValid(newstr): |
3713 | keep_processing = False | |
b881fc78 RD |
3714 | if not wx.Validator_IsSilent(): |
3715 | wx.Bell() | |
d14a1e28 RD |
3716 | |
3717 | if keep_processing: | |
b881fc78 RD |
3718 | wx.CallAfter(self._SetValue, newstr) |
3719 | wx.CallAfter(self._SetInsertionPoint, newpos) | |
d14a1e28 | 3720 | keep_processing = False |
fffd96b7 | 3721 | ## dbg(indent=0) |
d14a1e28 RD |
3722 | return keep_processing |
3723 | ||
3724 | ||
3725 | def _findNextEntry(self,pos, adjustInsert=True): | |
3726 | """ Find the insertion point for the next valid entry character position.""" | |
3727 | if self._isTemplateChar(pos): # if changing fields, pay attn to flag | |
3728 | adjustInsert = adjustInsert | |
3729 | else: # else within a field; flag not relevant | |
3730 | adjustInsert = False | |
3731 | ||
3732 | while self._isTemplateChar(pos) and pos < self._masklength: | |
3733 | pos += 1 | |
3734 | ||
3735 | # if changing fields, and we've been told to adjust insert point, | |
3736 | # look at new field; if empty and right-insert field, | |
3737 | # adjust to right edge: | |
3738 | if adjustInsert and pos < self._masklength: | |
3739 | field = self._FindField(pos) | |
3740 | start, end = field._extent | |
3741 | slice = self._GetValue()[start:end] | |
3742 | if field._insertRight and field.IsEmpty(slice): | |
3743 | pos = end | |
3744 | return pos | |
3745 | ||
3746 | ||
3747 | def _findNextTemplateChar(self, pos): | |
3748 | """ Find the position of the next non-editable character in the mask.""" | |
3749 | while not self._isTemplateChar(pos) and pos < self._masklength: | |
3750 | pos += 1 | |
3751 | return pos | |
3752 | ||
3753 | ||
3754 | def _OnAutoCompleteField(self, event): | |
fffd96b7 | 3755 | ## dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1) |
d14a1e28 RD |
3756 | pos = self._GetInsertionPoint() |
3757 | field = self._FindField(pos) | |
3758 | edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) | |
3759 | ||
3760 | match_index = None | |
3761 | keycode = event.GetKeyCode() | |
3762 | ||
3763 | if field._fillChar != ' ': | |
3764 | text = slice.replace(field._fillChar, '') | |
3765 | else: | |
3766 | text = slice | |
3767 | text = text.strip() | |
3768 | keep_processing = True # (assume True to start) | |
fffd96b7 | 3769 | ## dbg('field._hasList?', field._hasList) |
d14a1e28 | 3770 | if field._hasList: |
fffd96b7 RD |
3771 | ## dbg('choices:', field._choices) |
3772 | ## dbg('compareChoices:', field._compareChoices) | |
d14a1e28 | 3773 | choices, choice_required = field._compareChoices, field._choiceRequired |
b881fc78 | 3774 | if keycode in (wx.WXK_PRIOR, wx.WXK_UP): |
d14a1e28 RD |
3775 | direction = -1 |
3776 | else: | |
3777 | direction = 1 | |
3778 | match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex) | |
3779 | if( match_index is None | |
b881fc78 RD |
3780 | and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] |
3781 | or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ): | |
d14a1e28 RD |
3782 | # Select the 1st thing from the list: |
3783 | match_index = 0 | |
3784 | ||
3785 | if( match_index is not None | |
b881fc78 RD |
3786 | and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT] |
3787 | or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown()) | |
3788 | or (keycode == wx.WXK_DOWN and partial_match) ) ): | |
d14a1e28 RD |
3789 | |
3790 | # We're allowed to auto-complete: | |
fffd96b7 | 3791 | ## dbg('match found') |
d14a1e28 RD |
3792 | value = self._GetValue() |
3793 | newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:] | |
fffd96b7 | 3794 | ## dbg('setting value to "%s"' % newvalue) |
d14a1e28 RD |
3795 | self._SetValue(newvalue) |
3796 | self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip()))) | |
3797 | self._OnAutoSelect(field, match_index) | |
3798 | self._CheckValid() # recolor as appopriate | |
3799 | ||
3800 | ||
b881fc78 | 3801 | if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT): |
d14a1e28 RD |
3802 | # treat as left right arrow if unshifted, tab/shift tab if shifted. |
3803 | if event.ShiftDown(): | |
b881fc78 | 3804 | if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT): |
d14a1e28 RD |
3805 | # remove "shifting" and treat as (forward) tab: |
3806 | event.m_shiftDown = False | |
3807 | keep_processing = self._OnChangeField(event) | |
3808 | else: | |
3809 | keep_processing = self._OnArrow(event) | |
3810 | # else some other key; keep processing the key | |
3811 | ||
fffd96b7 | 3812 | ## dbg('keep processing?', keep_processing, indent=0) |
d14a1e28 RD |
3813 | return keep_processing |
3814 | ||
3815 | ||
3816 | def _OnAutoSelect(self, field, match_index = None): | |
3817 | """ | |
3818 | Function called if autoselect feature is enabled and entire control | |
3819 | is selected: | |
3820 | """ | |
fffd96b7 | 3821 | ## dbg('MaskedEditMixin::OnAutoSelect', field._index) |
d14a1e28 RD |
3822 | if match_index is not None: |
3823 | field._autoCompleteIndex = match_index | |
3824 | ||
3825 | ||
3826 | def _autoComplete(self, direction, choices, value, compareNoCase, current_index): | |
3827 | """ | |
3828 | This function gets called in response to Auto-complete events. | |
3829 | It attempts to find a match to the specified value against the | |
3830 | list of choices; if exact match, the index of then next | |
3831 | appropriate value in the list, based on the given direction. | |
3832 | If not an exact match, it will return the index of the 1st value from | |
3833 | the choice list for which the partial value can be extended to match. | |
3834 | If no match found, it will return None. | |
3835 | The function returns a 2-tuple, with the 2nd element being a boolean | |
3836 | that indicates if partial match was necessary. | |
3837 | """ | |
fffd96b7 | 3838 | ## dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1) |
d14a1e28 | 3839 | if value is None: |
fffd96b7 | 3840 | ## dbg('nothing to match against', indent=0) |
d14a1e28 RD |
3841 | return (None, False) |
3842 | ||
3843 | partial_match = False | |
3844 | ||
3845 | if compareNoCase: | |
3846 | value = value.lower() | |
3847 | ||
3848 | last_index = len(choices) - 1 | |
3849 | if value in choices: | |
fffd96b7 | 3850 | ## dbg('"%s" in', choices) |
d14a1e28 RD |
3851 | if current_index is not None and choices[current_index] == value: |
3852 | index = current_index | |
3853 | else: | |
3854 | index = choices.index(value) | |
3855 | ||
fffd96b7 | 3856 | ## dbg('matched "%s" (%d)' % (choices[index], index)) |
d14a1e28 | 3857 | if direction == -1: |
fffd96b7 | 3858 | ## dbg('going to previous') |
d14a1e28 RD |
3859 | if index == 0: index = len(choices) - 1 |
3860 | else: index -= 1 | |
3861 | else: | |
3862 | if index == len(choices) - 1: index = 0 | |
3863 | else: index += 1 | |
fffd96b7 | 3864 | ## dbg('change value to "%s" (%d)' % (choices[index], index)) |
d14a1e28 RD |
3865 | match = index |
3866 | else: | |
3867 | partial_match = True | |
3868 | value = value.strip() | |
fffd96b7 | 3869 | ## dbg('no match; try to auto-complete:') |
d14a1e28 | 3870 | match = None |
fffd96b7 | 3871 | ## dbg('searching for "%s"' % value) |
d14a1e28 RD |
3872 | if current_index is None: |
3873 | indices = range(len(choices)) | |
3874 | if direction == -1: | |
3875 | indices.reverse() | |
3876 | else: | |
3877 | if direction == 1: | |
3878 | indices = range(current_index +1, len(choices)) + range(current_index+1) | |
fffd96b7 | 3879 | ## dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices) |
d14a1e28 RD |
3880 | else: |
3881 | indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1) | |
fffd96b7 RD |
3882 | ## 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) |
3883 | #### dbg('indices:', indices) | |
d14a1e28 RD |
3884 | for index in indices: |
3885 | choice = choices[index] | |
3886 | if choice.find(value, 0) == 0: | |
fffd96b7 | 3887 | ## dbg('match found:', choice) |
d14a1e28 RD |
3888 | match = index |
3889 | break | |
3890 | else: dbg('choice: "%s" - no match' % choice) | |
3891 | if match is not None: | |
fffd96b7 RD |
3892 | ## dbg('matched', match) |
3893 | pass | |
d14a1e28 | 3894 | else: |
fffd96b7 RD |
3895 | ## dbg('no match found') |
3896 | pass | |
3897 | ## dbg(indent=0) | |
d14a1e28 RD |
3898 | return (match, partial_match) |
3899 | ||
3900 | ||
3901 | def _AdjustField(self, pos): | |
3902 | """ | |
3903 | This function gets called by default whenever the cursor leaves a field. | |
3904 | The pos argument given is the char position before leaving that field. | |
3905 | By default, floating point, integer and date values are adjusted to be | |
3906 | legal in this function. Derived classes may override this function | |
3907 | to modify the value of the control in a different way when changing fields. | |
3908 | ||
3909 | NOTE: these change the value immediately, and restore the cursor to | |
3910 | the passed location, so that any subsequent code can then move it | |
3911 | based on the operation being performed. | |
3912 | """ | |
3913 | newvalue = value = self._GetValue() | |
3914 | field = self._FindField(pos) | |
3915 | start, end, slice = self._FindFieldExtent(getslice=True) | |
3916 | newfield = field._AdjustField(slice) | |
3917 | newvalue = value[:start] + newfield + value[end:] | |
3918 | ||
3919 | if self._isFloat and newvalue != self._template: | |
3920 | newvalue = self._adjustFloat(newvalue) | |
3921 | ||
3922 | if self._ctrl_constraints._isInt and value != self._template: | |
3923 | newvalue = self._adjustInt(value) | |
3924 | ||
3925 | if self._isDate and value != self._template: | |
3926 | newvalue = self._adjustDate(value, fixcentury=True) | |
3927 | if self._4digityear: | |
3928 | year2dig = self._dateExtent - 2 | |
3929 | if pos == year2dig and value[year2dig] != newvalue[year2dig]: | |
3930 | pos = pos+2 | |
3931 | ||
3932 | if newvalue != value: | |
3933 | self._SetValue(newvalue) | |
3934 | self._SetInsertionPoint(pos) | |
3935 | ||
3936 | ||
3937 | def _adjustKey(self, pos, key): | |
3938 | """ Apply control formatting to the key (e.g. convert to upper etc). """ | |
3939 | field = self._FindField(pos) | |
3940 | if field._forceupper and key in range(97,123): | |
3941 | key = ord( chr(key).upper()) | |
3942 | ||
3943 | if field._forcelower and key in range(97,123): | |
3944 | key = ord( chr(key).lower()) | |
3945 | ||
3946 | return key | |
3947 | ||
3948 | ||
3949 | def _adjustPos(self, pos, key): | |
3950 | """ | |
3951 | Checks the current insertion point position and adjusts it if | |
3952 | necessary to skip over non-editable characters. | |
3953 | """ | |
fffd96b7 | 3954 | ## dbg('_adjustPos', pos, key, indent=1) |
d14a1e28 RD |
3955 | sel_start, sel_to = self._GetSelection() |
3956 | # If a numeric or decimal mask, and negatives allowed, reserve the | |
3957 | # first space for sign, and last one if using parens. | |
3958 | if( self._signOk | |
3959 | and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) ) | |
3960 | or self._useParens and pos == self._masklength -1)): | |
fffd96b7 | 3961 | ## dbg('adjusted pos:', pos, indent=0) |
d14a1e28 RD |
3962 | return pos |
3963 | ||
3964 | if key not in self._nav: | |
3965 | field = self._FindField(pos) | |
3966 | ||
fffd96b7 | 3967 | ## dbg('field._insertRight?', field._insertRight) |
d14a1e28 RD |
3968 | if field._insertRight: # if allow right-insert |
3969 | start, end = field._extent | |
3970 | slice = self._GetValue()[start:end].strip() | |
3971 | field_len = end - start | |
3972 | if pos == end: # if cursor at right edge of field | |
3973 | # if not filled or supposed to stay in field, keep current position | |
fffd96b7 RD |
3974 | #### dbg('pos==end') |
3975 | #### dbg('len (slice):', len(slice)) | |
3976 | #### dbg('field_len?', field_len) | |
3977 | #### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len) | |
3978 | #### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull) | |
d14a1e28 RD |
3979 | if len(slice) == field_len and field._moveOnFieldFull: |
3980 | # move cursor to next field: | |
3981 | pos = self._findNextEntry(pos) | |
3982 | self._SetInsertionPoint(pos) | |
3983 | if pos < sel_to: | |
3984 | self._SetSelection(pos, sel_to) # restore selection | |
3985 | else: | |
3986 | self._SetSelection(pos, pos) # remove selection | |
3987 | else: # leave cursor alone | |
3988 | pass | |
3989 | else: | |
3990 | # if at start of control, move to right edge | |
3991 | if sel_to == sel_start and self._isTemplateChar(pos) and pos != end: | |
3992 | pos = end # move to right edge | |
3993 | ## elif sel_start <= start and sel_to == end: | |
3994 | ## # select to right edge of field - 1 (to replace char) | |
3995 | ## pos = end - 1 | |
3996 | ## self._SetInsertionPoint(pos) | |
3997 | ## # restore selection | |
3998 | ## self._SetSelection(sel_start, pos) | |
3999 | ||
4000 | elif self._signOk and sel_start == 0: # if selected to beginning and signed, | |
4001 | # adjust to past reserved sign position: | |
4002 | pos = self._fields[0]._extent[0] | |
4003 | self._SetInsertionPoint(pos) | |
4004 | # restore selection | |
4005 | self._SetSelection(pos, sel_to) | |
4006 | else: | |
4007 | pass # leave position/selection alone | |
4008 | ||
4009 | # else make sure the user is not trying to type over a template character | |
4010 | # If they are, move them to the next valid entry position | |
4011 | elif self._isTemplateChar(pos): | |
4012 | if( not field._moveOnFieldFull | |
4013 | and (not self._signOk | |
4014 | or (self._signOk | |
4015 | and field._index == 0 | |
4016 | and pos > 0) ) ): # don't move to next field without explicit cursor movement | |
4017 | pass | |
4018 | else: | |
4019 | # find next valid position | |
4020 | pos = self._findNextEntry(pos) | |
4021 | self._SetInsertionPoint(pos) | |
4022 | if pos < sel_to: # restore selection | |
4023 | self._SetSelection(pos, sel_to) | |
fffd96b7 | 4024 | ## dbg('adjusted pos:', pos, indent=0) |
d14a1e28 RD |
4025 | return pos |
4026 | ||
4027 | ||
4028 | def _adjustFloat(self, candidate=None): | |
4029 | """ | |
4030 | 'Fixes' an floating point control. Collapses spaces, right-justifies, etc. | |
4031 | """ | |
fffd96b7 | 4032 | ## dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1) |
d14a1e28 RD |
4033 | lenInt,lenFraction = [len(s) for s in self._mask.split('.')] ## Get integer, fraction lengths |
4034 | ||
4035 | if candidate is None: value = self._GetValue() | |
4036 | else: value = candidate | |
fffd96b7 | 4037 | ## dbg('value = "%(value)s"' % locals(), 'len(value):', len(value)) |
d14a1e28 RD |
4038 | intStr, fracStr = value.split(self._decimalChar) |
4039 | ||
4040 | intStr = self._fields[0]._AdjustField(intStr) | |
fffd96b7 | 4041 | ## dbg('adjusted intStr: "%s"' % intStr) |
d14a1e28 RD |
4042 | lenInt = len(intStr) |
4043 | fracStr = fracStr + ('0'*(lenFraction-len(fracStr))) # add trailing spaces to decimal | |
4044 | ||
fffd96b7 RD |
4045 | ## dbg('intStr "%(intStr)s"' % locals()) |
4046 | ## dbg('lenInt:', lenInt) | |
d14a1e28 RD |
4047 | |
4048 | intStr = string.rjust( intStr[-lenInt:], lenInt) | |
fffd96b7 | 4049 | ## dbg('right-justifed intStr = "%(intStr)s"' % locals()) |
d14a1e28 RD |
4050 | newvalue = intStr + self._decimalChar + fracStr |
4051 | ||
4052 | if self._signOk: | |
4053 | if len(newvalue) < self._masklength: | |
4054 | newvalue = ' ' + newvalue | |
4055 | signedvalue = self._getSignedValue(newvalue)[0] | |
4056 | if signedvalue is not None: newvalue = signedvalue | |
4057 | ||
4058 | # Finally, align string with decimal position, left-padding with | |
4059 | # fillChar: | |
4060 | newdecpos = newvalue.find(self._decimalChar) | |
4061 | if newdecpos < self._decimalpos: | |
4062 | padlen = self._decimalpos - newdecpos | |
4063 | newvalue = string.join([' ' * padlen] + [newvalue] ,'') | |
4064 | ||
4065 | if self._signOk and self._useParens: | |
4066 | if newvalue.find('(') != -1: | |
4067 | newvalue = newvalue[:-1] + ')' | |
4068 | else: | |
4069 | newvalue = newvalue[:-1] + ' ' | |
4070 | ||
fffd96b7 | 4071 | ## dbg('newvalue = "%s"' % newvalue) |
d14a1e28 | 4072 | if candidate is None: |
b881fc78 | 4073 | wx.CallAfter(self._SetValue, newvalue) |
fffd96b7 | 4074 | ## dbg(indent=0) |
d14a1e28 RD |
4075 | return newvalue |
4076 | ||
4077 | ||
4078 | def _adjustInt(self, candidate=None): | |
4079 | """ 'Fixes' an integer control. Collapses spaces, right or left-justifies.""" | |
fffd96b7 | 4080 | ## dbg("MaskedEditMixin::_adjustInt", candidate) |
d14a1e28 RD |
4081 | lenInt = self._masklength |
4082 | if candidate is None: value = self._GetValue() | |
4083 | else: value = candidate | |
4084 | ||
4085 | intStr = self._fields[0]._AdjustField(value) | |
4086 | intStr = intStr.strip() # drop extra spaces | |
fffd96b7 | 4087 | ## dbg('adjusted field: "%s"' % intStr) |
d14a1e28 RD |
4088 | |
4089 | if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1: | |
4090 | if self._useParens: | |
4091 | intStr = '(' + intStr + ')' | |
4092 | else: | |
4093 | intStr = '-' + intStr | |
4094 | elif self._isNeg and intStr.find('-') != -1 and self._useParens: | |
4095 | intStr = intStr.replace('-', '(') | |
4096 | ||
4097 | if( self._signOk and ((self._useParens and intStr.find('(') == -1) | |
4098 | or (not self._useParens and intStr.find('-') == -1))): | |
4099 | intStr = ' ' + intStr | |
4100 | if self._useParens: | |
4101 | intStr += ' ' # space for right paren position | |
4102 | ||
4103 | elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1: | |
4104 | # ensure closing right paren: | |
4105 | intStr += ')' | |
4106 | ||
4107 | if self._fields[0]._alignRight: ## Only if right-alignment is enabled | |
4108 | intStr = intStr.rjust( lenInt ) | |
4109 | else: | |
4110 | intStr = intStr.ljust( lenInt ) | |
4111 | ||
4112 | if candidate is None: | |
b881fc78 | 4113 | wx.CallAfter(self._SetValue, intStr ) |
d14a1e28 RD |
4114 | return intStr |
4115 | ||
4116 | ||
4117 | def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False): | |
4118 | """ | |
4119 | 'Fixes' a date control, expanding the year if it can. | |
4120 | Applies various self-formatting options. | |
4121 | """ | |
fffd96b7 | 4122 | ## dbg("MaskedEditMixin::_adjustDate", indent=1) |
d14a1e28 RD |
4123 | if candidate is None: text = self._GetValue() |
4124 | else: text = candidate | |
fffd96b7 | 4125 | ## dbg('text=', text) |
d14a1e28 RD |
4126 | if self._datestyle == "YMD": |
4127 | year_field = 0 | |
4128 | else: | |
4129 | year_field = 2 | |
4130 | ||
fffd96b7 | 4131 | ## dbg('getYear: "%s"' % getYear(text, self._datestyle)) |
d14a1e28 RD |
4132 | year = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars |
4133 | month = getMonth( text, self._datestyle) | |
4134 | day = getDay( text, self._datestyle) | |
fffd96b7 | 4135 | ## dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day) |
d14a1e28 RD |
4136 | |
4137 | yearVal = None | |
4138 | yearstart = self._dateExtent - 4 | |
4139 | if( len(year) < 4 | |
4140 | and (fixcentury | |
4141 | or force4digit_year | |
4142 | or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ') | |
4143 | or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ): | |
4144 | ## user entered less than four digits and changing fields or past point where we could | |
4145 | ## enter another digit: | |
4146 | try: | |
4147 | yearVal = int(year) | |
4148 | except: | |
fffd96b7 | 4149 | ## dbg('bad year=', year) |
d14a1e28 RD |
4150 | year = text[yearstart:self._dateExtent] |
4151 | ||
4152 | if len(year) < 4 and yearVal: | |
4153 | if len(year) == 2: | |
4154 | # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the | |
4155 | # years pass... | |
b881fc78 | 4156 | now = wx.DateTime_Now() |
d14a1e28 RD |
4157 | century = (now.GetYear() /100) * 100 # "this century" |
4158 | twodig_year = now.GetYear() - century # "this year" (2 digits) | |
4159 | # if separation between today's 2-digit year and typed value > 50, | |
4160 | # assume last century, | |
4161 | # else assume this century. | |
4162 | # | |
4163 | # Eg: if 2003 and yearVal == 30, => 2030 | |
4164 | # if 2055 and yearVal == 80, => 2080 | |
4165 | # if 2010 and yearVal == 96, => 1996 | |
4166 | # | |
4167 | if abs(yearVal - twodig_year) > 50: | |
4168 | yearVal = (century - 100) + yearVal | |
4169 | else: | |
4170 | yearVal = century + yearVal | |
4171 | year = str( yearVal ) | |
4172 | else: # pad with 0's to make a 4-digit year | |
4173 | year = "%04d" % yearVal | |
4174 | if self._4digityear or force4digit_year: | |
4175 | text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:] | |
fffd96b7 | 4176 | ## dbg('newdate: "%s"' % text, indent=0) |
d14a1e28 RD |
4177 | return text |
4178 | ||
4179 | ||
4180 | def _goEnd(self, getPosOnly=False): | |
4181 | """ Moves the insertion point to the end of user-entry """ | |
fffd96b7 | 4182 | ## dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1) |
d14a1e28 | 4183 | text = self._GetValue() |
fffd96b7 | 4184 | #### dbg('text: "%s"' % text) |
d14a1e28 RD |
4185 | i = 0 |
4186 | if len(text.rstrip()): | |
4187 | for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1): | |
fffd96b7 | 4188 | #### dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i)) |
d14a1e28 RD |
4189 | if self._isMaskChar(i): |
4190 | char = text[i] | |
fffd96b7 | 4191 | #### dbg("text[%d]: '%s'" % (i, char)) |
d14a1e28 RD |
4192 | if char != ' ': |
4193 | i += 1 | |
4194 | break | |
4195 | ||
4196 | if i == 0: | |
4197 | pos = self._goHome(getPosOnly=True) | |
4198 | else: | |
4199 | pos = min(i,self._masklength) | |
4200 | ||
4201 | field = self._FindField(pos) | |
4202 | start, end = field._extent | |
4203 | if field._insertRight and pos < end: | |
4204 | pos = end | |
fffd96b7 RD |
4205 | ## dbg('next pos:', pos) |
4206 | ## dbg(indent=0) | |
d14a1e28 RD |
4207 | if getPosOnly: |
4208 | return pos | |
4209 | else: | |
4210 | self._SetInsertionPoint(pos) | |
4211 | ||
4212 | ||
4213 | def _goHome(self, getPosOnly=False): | |
4214 | """ Moves the insertion point to the beginning of user-entry """ | |
fffd96b7 | 4215 | ## dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1) |
d14a1e28 RD |
4216 | text = self._GetValue() |
4217 | for i in range(self._masklength): | |
4218 | if self._isMaskChar(i): | |
4219 | break | |
4220 | pos = max(i, 0) | |
fffd96b7 | 4221 | ## dbg(indent=0) |
d14a1e28 RD |
4222 | if getPosOnly: |
4223 | return pos | |
4224 | else: | |
4225 | self._SetInsertionPoint(max(i,0)) | |
4226 | ||
4227 | ||
4228 | ||
4229 | def _getAllowedChars(self, pos): | |
4230 | """ Returns a string of all allowed user input characters for the provided | |
4231 | mask character plus control options | |
4232 | """ | |
4233 | maskChar = self.maskdict[pos] | |
4234 | okchars = self.maskchardict[maskChar] ## entry, get mask approved characters | |
4235 | field = self._FindField(pos) | |
4236 | if okchars and field._okSpaces: ## Allow spaces? | |
4237 | okchars += " " | |
4238 | if okchars and field._includeChars: ## any additional included characters? | |
4239 | okchars += field._includeChars | |
fffd96b7 | 4240 | #### dbg('okchars[%d]:' % pos, okchars) |
d14a1e28 RD |
4241 | return okchars |
4242 | ||
4243 | ||
4244 | def _isMaskChar(self, pos): | |
4245 | """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#) | |
4246 | """ | |
4247 | if pos < self._masklength: | |
4248 | return self.ismasked[pos] | |
4249 | else: | |
4250 | return False | |
4251 | ||
4252 | ||
4253 | def _isTemplateChar(self,Pos): | |
4254 | """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#) | |
4255 | """ | |
4256 | if Pos < self._masklength: | |
4257 | return not self._isMaskChar(Pos) | |
4258 | else: | |
4259 | return False | |
4260 | ||
4261 | ||
4262 | def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False): | |
4263 | """ Returns True if character is allowed at the specific position, otherwise False.""" | |
fffd96b7 | 4264 | ## dbg('_isCharAllowed', char, pos, checkRegex, indent=1) |
d14a1e28 RD |
4265 | field = self._FindField(pos) |
4266 | right_insert = False | |
4267 | ||
4268 | if self.controlInitialized: | |
4269 | sel_start, sel_to = self._GetSelection() | |
4270 | else: | |
4271 | sel_start, sel_to = pos, pos | |
4272 | ||
4273 | if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight: | |
4274 | start, end = field._extent | |
4275 | field_len = end - start | |
4276 | if self.controlInitialized: | |
4277 | value = self._GetValue() | |
4278 | fstr = value[start:end].strip() | |
4279 | if field._padZero: | |
4280 | while fstr and fstr[0] == '0': | |
4281 | fstr = fstr[1:] | |
4282 | input_len = len(fstr) | |
4283 | if self._signOk and '-' in fstr or '(' in fstr: | |
4284 | input_len -= 1 # sign can move out of field, so don't consider it in length | |
4285 | else: | |
4286 | value = self._template | |
4287 | input_len = 0 # can't get the current "value", so use 0 | |
4288 | ||
4289 | ||
4290 | # if entire field is selected or position is at end and field is not full, | |
4291 | # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar: | |
4292 | if( (sel_start, sel_to) == field._extent | |
4293 | or (pos == end and input_len < field_len)): | |
4294 | pos = end - 1 | |
fffd96b7 | 4295 | ## dbg('pos = end - 1 = ', pos, 'right_insert? 1') |
d14a1e28 RD |
4296 | right_insert = True |
4297 | elif( field._allowInsert and sel_start == sel_to | |
4298 | and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar)) | |
4299 | and input_len < field_len ): | |
4300 | pos = sel_to - 1 # where character will go | |
fffd96b7 | 4301 | ## dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1') |
d14a1e28 RD |
4302 | right_insert = True |
4303 | # else leave pos alone... | |
4304 | else: | |
fffd96b7 RD |
4305 | ## dbg('pos stays ', pos, 'right_insert? 0') |
4306 | pass | |
d14a1e28 RD |
4307 | |
4308 | if self._isTemplateChar( pos ): ## if a template character, return empty | |
fffd96b7 | 4309 | ## dbg('%d is a template character; returning False' % pos, indent=0) |
d14a1e28 RD |
4310 | return False |
4311 | ||
4312 | if self._isMaskChar( pos ): | |
4313 | okChars = self._getAllowedChars(pos) | |
4314 | ||
4315 | if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)): | |
4316 | okChars += self._fields[0]._groupChar | |
4317 | ||
4318 | if self._signOk: | |
4319 | if self._isInt or (self._isFloat and pos < self._decimalpos): | |
4320 | okChars += '-' | |
4321 | if self._useParens: | |
4322 | okChars += '(' | |
4323 | elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)): | |
4324 | okChars += ')' | |
4325 | ||
fffd96b7 | 4326 | #### dbg('%s in %s?' % (char, okChars), char in okChars) |
d14a1e28 RD |
4327 | approved = char in okChars |
4328 | ||
4329 | if approved and checkRegex: | |
fffd96b7 | 4330 | ## dbg("checking appropriate regex's") |
d14a1e28 RD |
4331 | value = self._eraseSelection(self._GetValue()) |
4332 | if right_insert: | |
4333 | at = pos+1 | |
4334 | else: | |
4335 | at = pos | |
4336 | if allowAutoSelect: | |
4337 | newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True) | |
4338 | else: | |
4339 | newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value) | |
fffd96b7 | 4340 | ## dbg('newvalue: "%s"' % newvalue) |
d14a1e28 RD |
4341 | |
4342 | fields = [self._FindField(pos)] + [self._ctrl_constraints] | |
4343 | for field in fields: # includes fields[-1] == "ctrl_constraints" | |
4344 | if field._regexMask and field._filter: | |
fffd96b7 | 4345 | ## dbg('checking vs. regex') |
d14a1e28 RD |
4346 | start, end = field._extent |
4347 | slice = newvalue[start:end] | |
4348 | approved = (re.match( field._filter, slice) is not None) | |
fffd96b7 | 4349 | ## dbg('approved?', approved) |
d14a1e28 | 4350 | if not approved: break |
fffd96b7 | 4351 | ## dbg(indent=0) |
d14a1e28 RD |
4352 | return approved |
4353 | else: | |
fffd96b7 | 4354 | ## dbg('%d is a !???! character; returning False', indent=0) |
d14a1e28 RD |
4355 | return False |
4356 | ||
4357 | ||
4358 | def _applyFormatting(self): | |
4359 | """ Apply formatting depending on the control's state. | |
4360 | Need to find a way to call this whenever the value changes, in case the control's | |
4361 | value has been changed or set programatically. | |
4362 | """ | |
fffd96b7 RD |
4363 | ## dbg(suspend=1) |
4364 | ## dbg('MaskedEditMixin::_applyFormatting', indent=1) | |
d14a1e28 RD |
4365 | |
4366 | # Handle negative numbers | |
4367 | if self._signOk: | |
4368 | text, signpos, right_signpos = self._getSignedValue() | |
fffd96b7 | 4369 | ## dbg('text: "%s", signpos:' % text, signpos) |
d14a1e28 RD |
4370 | if not text or text[signpos] not in ('-','('): |
4371 | self._isNeg = False | |
fffd96b7 | 4372 | ## dbg('no valid sign found; new sign:', self._isNeg) |
d14a1e28 RD |
4373 | if text and signpos != self._signpos: |
4374 | self._signpos = signpos | |
4375 | elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('): | |
fffd96b7 | 4376 | ## dbg('setting _isNeg to True') |
d14a1e28 | 4377 | self._isNeg = True |
fffd96b7 | 4378 | ## dbg('self._isNeg:', self._isNeg) |
d14a1e28 RD |
4379 | |
4380 | if self._signOk and self._isNeg: | |
4381 | fc = self._signedForegroundColour | |
4382 | else: | |
4383 | fc = self._foregroundColour | |
4384 | ||
4385 | if hasattr(fc, '_name'): | |
4386 | c =fc._name | |
4387 | else: | |
4388 | c = fc | |
fffd96b7 | 4389 | ## dbg('setting foreground to', c) |
d14a1e28 RD |
4390 | self.SetForegroundColour(fc) |
4391 | ||
4392 | if self._valid: | |
fffd96b7 | 4393 | ## dbg('valid') |
d14a1e28 RD |
4394 | if self.IsEmpty(): |
4395 | bc = self._emptyBackgroundColour | |
4396 | else: | |
4397 | bc = self._validBackgroundColour | |
4398 | else: | |
fffd96b7 | 4399 | ## dbg('invalid') |
d14a1e28 RD |
4400 | bc = self._invalidBackgroundColour |
4401 | if hasattr(bc, '_name'): | |
4402 | c =bc._name | |
4403 | else: | |
4404 | c = bc | |
fffd96b7 | 4405 | ## dbg('setting background to', c) |
d14a1e28 RD |
4406 | self.SetBackgroundColour(bc) |
4407 | self._Refresh() | |
fffd96b7 | 4408 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
4409 | |
4410 | ||
4411 | def _getAbsValue(self, candidate=None): | |
4412 | """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s). | |
4413 | """ | |
fffd96b7 | 4414 | ## dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1) |
d14a1e28 RD |
4415 | if candidate is None: text = self._GetValue() |
4416 | else: text = candidate | |
4417 | right_signpos = text.find(')') | |
4418 | ||
4419 | if self._isInt: | |
4420 | if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ': | |
4421 | signpos = text.find('-') | |
4422 | if signpos == -1: | |
fffd96b7 | 4423 | ## dbg('no - found; searching for (') |
d14a1e28 RD |
4424 | signpos = text.find('(') |
4425 | elif signpos != -1: | |
fffd96b7 RD |
4426 | ## dbg('- found at', signpos) |
4427 | pass | |
d14a1e28 RD |
4428 | |
4429 | if signpos == -1: | |
fffd96b7 RD |
4430 | ## dbg('signpos still -1') |
4431 | ## dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength) | |
d14a1e28 RD |
4432 | if len(text) < self._masklength: |
4433 | text = ' ' + text | |
4434 | if len(text) < self._masklength: | |
4435 | text += ' ' | |
4436 | if len(text) > self._masklength and text[-1] in (')', ' '): | |
4437 | text = text[:-1] | |
4438 | else: | |
fffd96b7 RD |
4439 | ## dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength)) |
4440 | ## dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1)) | |
d14a1e28 RD |
4441 | signpos = len(text) - (len(text.lstrip()) + 1) |
4442 | ||
4443 | if self._useParens and not text.strip(): | |
4444 | signpos -= 1 # empty value; use penultimate space | |
fffd96b7 | 4445 | ## dbg('signpos:', signpos) |
d14a1e28 RD |
4446 | if signpos >= 0: |
4447 | text = text[:signpos] + ' ' + text[signpos+1:] | |
4448 | ||
4449 | else: | |
4450 | if self._signOk: | |
4451 | signpos = 0 | |
4452 | text = self._template[0] + text[1:] | |
4453 | else: | |
4454 | signpos = -1 | |
4455 | ||
4456 | if right_signpos != -1: | |
4457 | if self._signOk: | |
4458 | text = text[:right_signpos] + ' ' + text[right_signpos+1:] | |
4459 | elif len(text) > self._masklength: | |
4460 | text = text[:right_signpos] + text[right_signpos+1:] | |
4461 | right_signpos = -1 | |
4462 | ||
4463 | ||
4464 | elif self._useParens and self._signOk: | |
4465 | # figure out where it ought to go: | |
4466 | right_signpos = self._masklength - 1 # initial guess | |
4467 | if not self._ctrl_constraints._alignRight: | |
fffd96b7 | 4468 | ## dbg('not right-aligned') |
d14a1e28 RD |
4469 | if len(text.strip()) == 0: |
4470 | right_signpos = signpos + 1 | |
4471 | elif len(text.strip()) < self._masklength: | |
4472 | right_signpos = len(text.rstrip()) | |
fffd96b7 | 4473 | ## dbg('right_signpos:', right_signpos) |
d14a1e28 RD |
4474 | |
4475 | groupchar = self._fields[0]._groupChar | |
4476 | try: | |
4477 | value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', '')) | |
4478 | except: | |
fffd96b7 | 4479 | ## dbg('invalid number', indent=0) |
d14a1e28 RD |
4480 | return None, signpos, right_signpos |
4481 | ||
4482 | else: # float value | |
4483 | try: | |
4484 | groupchar = self._fields[0]._groupChar | |
4485 | value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', '')) | |
fffd96b7 | 4486 | ## dbg('value:', value) |
d14a1e28 RD |
4487 | except: |
4488 | value = None | |
4489 | ||
4490 | if value < 0 and value is not None: | |
4491 | signpos = text.find('-') | |
4492 | if signpos == -1: | |
4493 | signpos = text.find('(') | |
4494 | ||
4495 | text = text[:signpos] + self._template[signpos] + text[signpos+1:] | |
4496 | else: | |
4497 | # look forwards up to the decimal point for the 1st non-digit | |
fffd96b7 RD |
4498 | ## dbg('decimal pos:', self._decimalpos) |
4499 | ## dbg('text: "%s"' % text) | |
d14a1e28 RD |
4500 | if self._signOk: |
4501 | signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1) | |
fffd96b7 | 4502 | # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET |
50940aad | 4503 | if len(text) >= signpos+1 and text[signpos+1] in ('-','('): |
d14a1e28 RD |
4504 | signpos += 1 |
4505 | else: | |
4506 | signpos = -1 | |
fffd96b7 | 4507 | ## dbg('signpos:', signpos) |
d14a1e28 RD |
4508 | |
4509 | if self._useParens: | |
4510 | if self._signOk: | |
4511 | right_signpos = self._masklength - 1 | |
4512 | text = text[:right_signpos] + ' ' | |
4513 | if text[signpos] == '(': | |
4514 | text = text[:signpos] + ' ' + text[signpos+1:] | |
4515 | else: | |
4516 | right_signpos = text.find(')') | |
4517 | if right_signpos != -1: | |
4518 | text = text[:-1] | |
4519 | right_signpos = -1 | |
4520 | ||
4521 | if value is None: | |
fffd96b7 | 4522 | ## dbg('invalid number') |
d14a1e28 RD |
4523 | text = None |
4524 | ||
fffd96b7 RD |
4525 | ## dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos) |
4526 | ## dbg(indent=0) | |
d14a1e28 RD |
4527 | return text, signpos, right_signpos |
4528 | ||
4529 | ||
4530 | def _getSignedValue(self, candidate=None): | |
4531 | """ Return a signed value by adding a "-" prefix if the value | |
4532 | is set to negative, or a space if positive. | |
4533 | """ | |
fffd96b7 | 4534 | ## dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1) |
d14a1e28 RD |
4535 | if candidate is None: text = self._GetValue() |
4536 | else: text = candidate | |
4537 | ||
4538 | ||
4539 | abstext, signpos, right_signpos = self._getAbsValue(text) | |
4540 | if self._signOk: | |
4541 | if abstext is None: | |
fffd96b7 | 4542 | ## dbg(indent=0) |
d14a1e28 RD |
4543 | return abstext, signpos, right_signpos |
4544 | ||
4545 | if self._isNeg or text[signpos] in ('-', '('): | |
4546 | if self._useParens: | |
4547 | sign = '(' | |
4548 | else: | |
4549 | sign = '-' | |
4550 | else: | |
4551 | sign = ' ' | |
4552 | if abstext[signpos] not in string.digits: | |
4553 | text = abstext[:signpos] + sign + abstext[signpos+1:] | |
4554 | else: | |
4555 | # this can happen if value passed is too big; sign assumed to be | |
4556 | # in position 0, but if already filled with a digit, prepend sign... | |
4557 | text = sign + abstext | |
4558 | if self._useParens and text.find('(') != -1: | |
4559 | text = text[:right_signpos] + ')' + text[right_signpos+1:] | |
4560 | else: | |
4561 | text = abstext | |
fffd96b7 RD |
4562 | ## dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos) |
4563 | ## dbg(indent=0) | |
d14a1e28 RD |
4564 | return text, signpos, right_signpos |
4565 | ||
4566 | ||
4567 | def GetPlainValue(self, candidate=None): | |
4568 | """ Returns control's value stripped of the template text. | |
d4b73b1b | 4569 | plainvalue = MaskedEditMixin.GetPlainValue() |
d14a1e28 | 4570 | """ |
fffd96b7 | 4571 | ## dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1) |
d14a1e28 RD |
4572 | |
4573 | if candidate is None: text = self._GetValue() | |
4574 | else: text = candidate | |
4575 | ||
4576 | if self.IsEmpty(): | |
fffd96b7 | 4577 | ## dbg('returned ""', indent=0) |
d14a1e28 RD |
4578 | return "" |
4579 | else: | |
4580 | plain = "" | |
4581 | for idx in range( min(len(self._template), len(text)) ): | |
4582 | if self._mask[idx] in maskchars: | |
4583 | plain += text[idx] | |
4584 | ||
4585 | if self._isFloat or self._isInt: | |
fffd96b7 | 4586 | ## dbg('plain so far: "%s"' % plain) |
d14a1e28 | 4587 | plain = plain.replace('(', '-').replace(')', ' ') |
fffd96b7 | 4588 | ## dbg('plain after sign regularization: "%s"' % plain) |
d14a1e28 RD |
4589 | |
4590 | if self._signOk and self._isNeg and plain.count('-') == 0: | |
4591 | # must be in reserved position; add to "plain value" | |
4592 | plain = '-' + plain.strip() | |
4593 | ||
4594 | if self._fields[0]._alignRight: | |
4595 | lpad = plain.count(',') | |
4596 | plain = ' ' * lpad + plain.replace(',','') | |
4597 | else: | |
4598 | plain = plain.replace(',','') | |
fffd96b7 | 4599 | ## dbg('plain after pad and group:"%s"' % plain) |
d14a1e28 | 4600 | |
fffd96b7 | 4601 | ## dbg('returned "%s"' % plain.rstrip(), indent=0) |
d14a1e28 RD |
4602 | return plain.rstrip() |
4603 | ||
4604 | ||
4605 | def IsEmpty(self, value=None): | |
4606 | """ | |
4607 | Returns True if control is equal to an empty value. | |
4608 | (Empty means all editable positions in the template == fillChar.) | |
4609 | """ | |
4610 | if value is None: value = self._GetValue() | |
4611 | if value == self._template and not self._defaultValue: | |
fffd96b7 | 4612 | #### dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)") |
d14a1e28 RD |
4613 | return True # (all mask chars == fillChar by defn) |
4614 | elif value == self._template: | |
4615 | empty = True | |
4616 | for pos in range(len(self._template)): | |
fffd96b7 RD |
4617 | #### dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos)) |
4618 | #### dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos]) | |
d14a1e28 RD |
4619 | if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]): |
4620 | empty = False | |
fffd96b7 | 4621 | #### dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals()) |
d14a1e28 RD |
4622 | return empty |
4623 | else: | |
fffd96b7 | 4624 | #### dbg("IsEmpty? 0 (value doesn't match template)") |
d14a1e28 RD |
4625 | return False |
4626 | ||
4627 | ||
4628 | def IsDefault(self, value=None): | |
4629 | """ | |
4630 | Returns True if the value specified (or the value of the control if not specified) | |
4631 | is equal to the default value. | |
4632 | """ | |
4633 | if value is None: value = self._GetValue() | |
4634 | return value == self._template | |
4635 | ||
4636 | ||
4637 | def IsValid(self, value=None): | |
4638 | """ Indicates whether the value specified (or the current value of the control | |
4639 | if not specified) is considered valid.""" | |
fffd96b7 | 4640 | #### dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1) |
d14a1e28 RD |
4641 | if value is None: value = self._GetValue() |
4642 | ret = self._CheckValid(value) | |
fffd96b7 | 4643 | #### dbg(indent=0) |
d14a1e28 RD |
4644 | return ret |
4645 | ||
4646 | ||
4647 | def _eraseSelection(self, value=None, sel_start=None, sel_to=None): | |
4648 | """ Used to blank the selection when inserting a new character. """ | |
fffd96b7 | 4649 | ## dbg("MaskedEditMixin::_eraseSelection", indent=1) |
d14a1e28 RD |
4650 | if value is None: value = self._GetValue() |
4651 | if sel_start is None or sel_to is None: | |
4652 | sel_start, sel_to = self._GetSelection() ## check for a range of selected text | |
fffd96b7 RD |
4653 | ## dbg('value: "%s"' % value) |
4654 | ## dbg("current sel_start, sel_to:", sel_start, sel_to) | |
d14a1e28 RD |
4655 | |
4656 | newvalue = list(value) | |
4657 | for i in range(sel_start, sel_to): | |
4658 | if self._signOk and newvalue[i] in ('-', '(', ')'): | |
fffd96b7 | 4659 | ## dbg('found sign (%s) at' % newvalue[i], i) |
d14a1e28 RD |
4660 | |
4661 | # balance parentheses: | |
4662 | if newvalue[i] == '(': | |
4663 | right_signpos = value.find(')') | |
4664 | if right_signpos != -1: | |
4665 | newvalue[right_signpos] = ' ' | |
4666 | ||
4667 | elif newvalue[i] == ')': | |
4668 | left_signpos = value.find('(') | |
4669 | if left_signpos != -1: | |
4670 | newvalue[left_signpos] = ' ' | |
4671 | ||
4672 | newvalue[i] = ' ' | |
4673 | ||
4674 | elif self._isMaskChar(i): | |
4675 | field = self._FindField(i) | |
4676 | if field._padZero: | |
4677 | newvalue[i] = '0' | |
4678 | else: | |
4679 | newvalue[i] = self._template[i] | |
4680 | ||
4681 | value = string.join(newvalue,"") | |
fffd96b7 RD |
4682 | ## dbg('new value: "%s"' % value) |
4683 | ## dbg(indent=0) | |
d14a1e28 RD |
4684 | return value |
4685 | ||
4686 | ||
4687 | def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False): | |
4688 | """ Handles replacement of the character at the current insertion point.""" | |
fffd96b7 | 4689 | ## dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1) |
d14a1e28 RD |
4690 | |
4691 | text = self._eraseSelection(value) | |
4692 | field = self._FindField(pos) | |
4693 | start, end = field._extent | |
4694 | newtext = "" | |
4695 | newpos = pos | |
4696 | ||
4697 | if pos != sel_start and sel_start == sel_to: | |
4698 | # adjustpos must have moved the position; make selection match: | |
4699 | sel_start = sel_to = pos | |
4700 | ||
fffd96b7 | 4701 | ## dbg('field._insertRight?', field._insertRight) |
d14a1e28 RD |
4702 | if( field._insertRight # field allows right insert |
4703 | and ((sel_start, sel_to) == field._extent # and whole field selected | |
4704 | or (sel_start == sel_to # or nothing selected | |
4705 | and (sel_start == end # and cursor at right edge | |
4706 | or (field._allowInsert # or field allows right-insert | |
4707 | and sel_start < end # next to other char in field: | |
4708 | and text[sel_start] != field._fillChar) ) ) ) ): | |
fffd96b7 | 4709 | ## dbg('insertRight') |
d14a1e28 RD |
4710 | fstr = text[start:end] |
4711 | erasable_chars = [field._fillChar, ' '] | |
4712 | ||
4713 | if field._padZero: | |
4714 | erasable_chars.append('0') | |
4715 | ||
4716 | erased = '' | |
fffd96b7 RD |
4717 | #### dbg("fstr[0]:'%s'" % fstr[0]) |
4718 | #### dbg('field_index:', field._index) | |
4719 | #### dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars) | |
4720 | #### dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?", | |
d14a1e28 RD |
4721 | ## self._signOk and field._index == 0 and fstr[0] in ('-','(')) |
4722 | if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')): | |
4723 | erased = fstr[0] | |
fffd96b7 RD |
4724 | #### dbg('value: "%s"' % text) |
4725 | #### dbg('fstr: "%s"' % fstr) | |
4726 | #### dbg("erased: '%s'" % erased) | |
d14a1e28 RD |
4727 | field_sel_start = sel_start - start |
4728 | field_sel_to = sel_to - start | |
fffd96b7 RD |
4729 | ## dbg('left fstr: "%s"' % fstr[1:field_sel_start]) |
4730 | ## dbg('right fstr: "%s"' % fstr[field_sel_to:end]) | |
d14a1e28 RD |
4731 | fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end] |
4732 | if field._alignRight and sel_start != sel_to: | |
4733 | field_len = end - start | |
4734 | ## pos += (field_len - len(fstr)) # move cursor right by deleted amount | |
4735 | pos = sel_to | |
fffd96b7 | 4736 | ## dbg('setting pos to:', pos) |
d14a1e28 RD |
4737 | if field._padZero: |
4738 | fstr = '0' * (field_len - len(fstr)) + fstr | |
4739 | else: | |
4740 | fstr = fstr.rjust(field_len) # adjust the field accordingly | |
fffd96b7 | 4741 | ## dbg('field str: "%s"' % fstr) |
d14a1e28 RD |
4742 | |
4743 | newtext = text[:start] + fstr + text[end:] | |
4744 | if erased in ('-', '(') and self._signOk: | |
4745 | newtext = erased + newtext[1:] | |
fffd96b7 | 4746 | ## dbg('newtext: "%s"' % newtext) |
d14a1e28 RD |
4747 | |
4748 | if self._signOk and field._index == 0: | |
4749 | start -= 1 # account for sign position | |
4750 | ||
fffd96b7 RD |
4751 | #### dbg('field._moveOnFieldFull?', field._moveOnFieldFull) |
4752 | #### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start) | |
d14a1e28 RD |
4753 | if( field._moveOnFieldFull and pos == end |
4754 | and len(fstr.lstrip()) == end-start): # if field now full | |
4755 | newpos = self._findNextEntry(end) # go to next field | |
4756 | else: | |
4757 | newpos = pos # else keep cursor at current position | |
4758 | ||
4759 | if not newtext: | |
fffd96b7 | 4760 | ## dbg('not newtext') |
d14a1e28 | 4761 | if newpos != pos: |
fffd96b7 RD |
4762 | ## dbg('newpos:', newpos) |
4763 | pass | |
d14a1e28 RD |
4764 | if self._signOk and self._useParens: |
4765 | old_right_signpos = text.find(')') | |
4766 | ||
4767 | if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start: | |
4768 | # inserting within a left-insert-capable field | |
4769 | field_len = end - start | |
4770 | before = text[start:sel_start] | |
4771 | after = text[sel_to:end].strip() | |
fffd96b7 RD |
4772 | #### dbg("current field:'%s'" % text[start:end]) |
4773 | #### dbg("before:'%s'" % before, "after:'%s'" % after) | |
d14a1e28 | 4774 | new_len = len(before) + len(after) + 1 # (for inserted char) |
fffd96b7 | 4775 | #### dbg('new_len:', new_len) |
d14a1e28 RD |
4776 | |
4777 | if new_len < field_len: | |
4778 | retained = after + self._template[end-(field_len-new_len):end] | |
4779 | elif new_len > end-start: | |
4780 | retained = after[1:] | |
4781 | else: | |
4782 | retained = after | |
4783 | ||
4784 | left = text[0:start] + before | |
fffd96b7 | 4785 | #### dbg("left:'%s'" % left, "retained:'%s'" % retained) |
d14a1e28 RD |
4786 | right = retained + text[end:] |
4787 | else: | |
4788 | left = text[0:pos] | |
4789 | right = text[pos+1:] | |
4790 | ||
4791 | newtext = left + char + right | |
4792 | ||
4793 | if self._signOk and self._useParens: | |
4794 | # Balance parentheses: | |
4795 | left_signpos = newtext.find('(') | |
4796 | ||
4797 | if left_signpos == -1: # erased '('; remove ')' | |
4798 | right_signpos = newtext.find(')') | |
4799 | if right_signpos != -1: | |
4800 | newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:] | |
4801 | ||
4802 | elif old_right_signpos != -1: | |
4803 | right_signpos = newtext.find(')') | |
4804 | ||
4805 | if right_signpos == -1: # just replaced right-paren | |
4806 | if newtext[pos] == ' ': # we just erased '); erase '(' | |
4807 | newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:] | |
4808 | else: # replaced with digit; move ') over | |
4809 | if self._ctrl_constraints._alignRight or self._isFloat: | |
4810 | newtext = newtext[:-1] + ')' | |
4811 | else: | |
4812 | rstripped_text = newtext.rstrip() | |
4813 | right_signpos = len(rstripped_text) | |
fffd96b7 | 4814 | ## dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos) |
d14a1e28 RD |
4815 | newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:] |
4816 | ||
4817 | if( field._insertRight # if insert-right field (but we didn't start at right edge) | |
4818 | and field._moveOnFieldFull # and should move cursor when full | |
4819 | and len(newtext[start:end].strip()) == end-start): # and field now full | |
4820 | newpos = self._findNextEntry(end) # go to next field | |
fffd96b7 | 4821 | ## dbg('newpos = nextentry =', newpos) |
d14a1e28 | 4822 | else: |
fffd96b7 | 4823 | ## dbg('pos:', pos, 'newpos:', pos+1) |
d14a1e28 RD |
4824 | newpos = pos+1 |
4825 | ||
4826 | ||
4827 | if allowAutoSelect: | |
4828 | new_select_to = newpos # (default return values) | |
4829 | match_field = None | |
4830 | match_index = None | |
4831 | ||
4832 | if field._autoSelect: | |
4833 | match_index, partial_match = self._autoComplete(1, # (always forward) | |
4834 | field._compareChoices, | |
4835 | newtext[start:end], | |
4836 | compareNoCase=field._compareNoCase, | |
4837 | current_index = field._autoCompleteIndex-1) | |
4838 | if match_index is not None and partial_match: | |
4839 | matched_str = newtext[start:end] | |
4840 | newtext = newtext[:start] + field._choices[match_index] + newtext[end:] | |
4841 | new_select_to = end | |
4842 | match_field = field | |
4843 | if field._insertRight: | |
4844 | # adjust position to just after partial match in field | |
4845 | newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip())) | |
4846 | ||
4847 | elif self._ctrl_constraints._autoSelect: | |
4848 | match_index, partial_match = self._autoComplete( | |
4849 | 1, # (always forward) | |
4850 | self._ctrl_constraints._compareChoices, | |
4851 | newtext, | |
4852 | self._ctrl_constraints._compareNoCase, | |
4853 | current_index = self._ctrl_constraints._autoCompleteIndex - 1) | |
4854 | if match_index is not None and partial_match: | |
4855 | matched_str = newtext | |
4856 | newtext = self._ctrl_constraints._choices[match_index] | |
4857 | new_select_to = self._ctrl_constraints._extent[1] | |
4858 | match_field = self._ctrl_constraints | |
4859 | if self._ctrl_constraints._insertRight: | |
4860 | # adjust position to just after partial match in control: | |
4861 | newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip())) | |
4862 | ||
fffd96b7 RD |
4863 | ## dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to) |
4864 | ## dbg(indent=0) | |
d14a1e28 RD |
4865 | return newtext, newpos, new_select_to, match_field, match_index |
4866 | else: | |
fffd96b7 RD |
4867 | ## dbg('newtext: "%s"' % newtext, 'newpos:', newpos) |
4868 | ## dbg(indent=0) | |
d14a1e28 RD |
4869 | return newtext, newpos |
4870 | ||
4871 | ||
4872 | def _OnFocus(self,event): | |
4873 | """ | |
4874 | This event handler is currently necessary to work around new default | |
4875 | behavior as of wxPython2.3.3; | |
4876 | The TAB key auto selects the entire contents of the wxTextCtrl *after* | |
4877 | the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection | |
4878 | *here*, because it hasn't happened yet. So to prevent this behavior, and | |
4879 | preserve the correct selection when the focus event is not due to tab, | |
4880 | we need to pull the following trick: | |
4881 | """ | |
fffd96b7 | 4882 | ## dbg('MaskedEditMixin::_OnFocus') |
b881fc78 | 4883 | wx.CallAfter(self._fixSelection) |
d14a1e28 RD |
4884 | event.Skip() |
4885 | self.Refresh() | |
4886 | ||
4887 | ||
4888 | def _CheckValid(self, candidate=None): | |
4889 | """ | |
4890 | This is the default validation checking routine; It verifies that the | |
4891 | current value of the control is a "valid value," and has the side | |
4892 | effect of coloring the control appropriately. | |
4893 | """ | |
fffd96b7 RD |
4894 | ## dbg(suspend=1) |
4895 | ## dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1) | |
d14a1e28 RD |
4896 | oldValid = self._valid |
4897 | if candidate is None: value = self._GetValue() | |
4898 | else: value = candidate | |
fffd96b7 | 4899 | ## dbg('value: "%s"' % value) |
d14a1e28 RD |
4900 | oldvalue = value |
4901 | valid = True # assume True | |
4902 | ||
4903 | if not self.IsDefault(value) and self._isDate: ## Date type validation | |
4904 | valid = self._validateDate(value) | |
fffd96b7 | 4905 | ## dbg("valid date?", valid) |
d14a1e28 RD |
4906 | |
4907 | elif not self.IsDefault(value) and self._isTime: | |
4908 | valid = self._validateTime(value) | |
fffd96b7 | 4909 | ## dbg("valid time?", valid) |
d14a1e28 RD |
4910 | |
4911 | elif not self.IsDefault(value) and (self._isInt or self._isFloat): ## Numeric type | |
4912 | valid = self._validateNumeric(value) | |
fffd96b7 | 4913 | ## dbg("valid Number?", valid) |
d14a1e28 RD |
4914 | |
4915 | if valid: # and not self.IsDefault(value): ## generic validation accounts for IsDefault() | |
4916 | ## valid so far; ensure also allowed by any list or regex provided: | |
4917 | valid = self._validateGeneric(value) | |
fffd96b7 | 4918 | ## dbg("valid value?", valid) |
d14a1e28 | 4919 | |
fffd96b7 | 4920 | ## dbg('valid?', valid) |
d14a1e28 RD |
4921 | |
4922 | if not candidate: | |
4923 | self._valid = valid | |
4924 | self._applyFormatting() | |
4925 | if self._valid != oldValid: | |
fffd96b7 RD |
4926 | ## dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid) |
4927 | ## dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue()) | |
4928 | pass | |
4929 | ## dbg(indent=0, suspend=0) | |
d14a1e28 RD |
4930 | return valid |
4931 | ||
4932 | ||
4933 | def _validateGeneric(self, candidate=None): | |
4934 | """ Validate the current value using the provided list or Regex filter (if any). | |
4935 | """ | |
4936 | if candidate is None: | |
4937 | text = self._GetValue() | |
4938 | else: | |
4939 | text = candidate | |
4940 | ||
7722248d | 4941 | valid = True # assume True |
d14a1e28 RD |
4942 | for i in [-1] + self._field_indices: # process global constraints first: |
4943 | field = self._fields[i] | |
4944 | start, end = field._extent | |
4945 | slice = text[start:end] | |
4946 | valid = field.IsValid(slice) | |
4947 | if not valid: | |
4948 | break | |
4949 | ||
4950 | return valid | |
4951 | ||
4952 | ||
4953 | def _validateNumeric(self, candidate=None): | |
4954 | """ Validate that the value is within the specified range (if specified.)""" | |
4955 | if candidate is None: value = self._GetValue() | |
4956 | else: value = candidate | |
4957 | try: | |
4958 | groupchar = self._fields[0]._groupChar | |
4959 | if self._isFloat: | |
4960 | number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', '')) | |
4961 | else: | |
4962 | number = long( value.replace(groupchar, '').replace('(', '-').replace(')', '')) | |
4963 | if value.strip(): | |
4964 | if self._fields[0]._alignRight: | |
4965 | require_digit_at = self._fields[0]._extent[1]-1 | |
4966 | else: | |
4967 | require_digit_at = self._fields[0]._extent[0] | |
fffd96b7 RD |
4968 | ## dbg('require_digit_at:', require_digit_at) |
4969 | ## dbg("value[rda]: '%s'" % value[require_digit_at]) | |
d14a1e28 RD |
4970 | if value[require_digit_at] not in list(string.digits): |
4971 | valid = False | |
4972 | return valid | |
4973 | # else... | |
fffd96b7 | 4974 | ## dbg('number:', number) |
d14a1e28 RD |
4975 | if self._ctrl_constraints._hasRange: |
4976 | valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh | |
4977 | else: | |
4978 | valid = True | |
4979 | groupcharpos = value.rfind(groupchar) | |
4980 | if groupcharpos != -1: # group char present | |
fffd96b7 | 4981 | ## dbg('groupchar found at', groupcharpos) |
d14a1e28 RD |
4982 | if self._isFloat and groupcharpos > self._decimalpos: |
4983 | # 1st one found on right-hand side is past decimal point | |
fffd96b7 | 4984 | ## dbg('groupchar in fraction; illegal') |
d14a1e28 RD |
4985 | valid = False |
4986 | elif self._isFloat: | |
4987 | integer = value[:self._decimalpos].strip() | |
4988 | else: | |
4989 | integer = value.strip() | |
fffd96b7 | 4990 | ## dbg("integer:'%s'" % integer) |
d14a1e28 RD |
4991 | if integer[0] in ('-', '('): |
4992 | integer = integer[1:] | |
4993 | if integer[-1] == ')': | |
4994 | integer = integer[:-1] | |
4995 | ||
4996 | parts = integer.split(groupchar) | |
fffd96b7 | 4997 | ## dbg('parts:', parts) |
d14a1e28 RD |
4998 | for i in range(len(parts)): |
4999 | if i == 0 and abs(int(parts[0])) > 999: | |
fffd96b7 | 5000 | ## dbg('group 0 too long; illegal') |
d14a1e28 RD |
5001 | valid = False |
5002 | break | |
5003 | elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]): | |
fffd96b7 | 5004 | ## dbg('group %i (%s) not right size; illegal' % (i, parts[i])) |
d14a1e28 RD |
5005 | valid = False |
5006 | break | |
5007 | except ValueError: | |
fffd96b7 | 5008 | ## dbg('value not a valid number') |
d14a1e28 RD |
5009 | valid = False |
5010 | return valid | |
5011 | ||
5012 | ||
5013 | def _validateDate(self, candidate=None): | |
5014 | """ Validate the current date value using the provided Regex filter. | |
5015 | Generally used for character types.BufferType | |
5016 | """ | |
fffd96b7 | 5017 | ## dbg('MaskedEditMixin::_validateDate', indent=1) |
d14a1e28 RD |
5018 | if candidate is None: value = self._GetValue() |
5019 | else: value = candidate | |
fffd96b7 | 5020 | ## dbg('value = "%s"' % value) |
d14a1e28 | 5021 | text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it |
fffd96b7 | 5022 | ## dbg('text =', text) |
d14a1e28 RD |
5023 | valid = True # assume True until proven otherwise |
5024 | ||
5025 | try: | |
5026 | # replace fillChar in each field with space: | |
5027 | datestr = text[0:self._dateExtent] | |
5028 | for i in range(3): | |
5029 | field = self._fields[i] | |
5030 | start, end = field._extent | |
5031 | fstr = datestr[start:end] | |
5032 | fstr.replace(field._fillChar, ' ') | |
5033 | datestr = datestr[:start] + fstr + datestr[end:] | |
5034 | ||
5035 | year, month, day = getDateParts( datestr, self._datestyle) | |
5036 | year = int(year) | |
fffd96b7 | 5037 | ## dbg('self._dateExtent:', self._dateExtent) |
d14a1e28 RD |
5038 | if self._dateExtent == 11: |
5039 | month = charmonths_dict[month.lower()] | |
5040 | else: | |
5041 | month = int(month) | |
5042 | day = int(day) | |
fffd96b7 | 5043 | ## dbg('year, month, day:', year, month, day) |
d14a1e28 RD |
5044 | |
5045 | except ValueError: | |
fffd96b7 | 5046 | ## dbg('cannot convert string to integer parts') |
d14a1e28 RD |
5047 | valid = False |
5048 | except KeyError: | |
fffd96b7 | 5049 | ## dbg('cannot convert string to integer month') |
d14a1e28 RD |
5050 | valid = False |
5051 | ||
5052 | if valid: | |
5053 | # use wxDateTime to unambiguously try to parse the date: | |
5054 | # ### Note: because wxDateTime is *brain-dead* and expects months 0-11, | |
5055 | # rather than 1-12, so handle accordingly: | |
5056 | if month > 12: | |
5057 | valid = False | |
5058 | else: | |
5059 | month -= 1 | |
5060 | try: | |
fffd96b7 | 5061 | ## dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year)) |
b881fc78 | 5062 | dateHandler = wx.DateTimeFromDMY(day,month,year) |
fffd96b7 | 5063 | ## dbg("succeeded") |
d14a1e28 RD |
5064 | dateOk = True |
5065 | except: | |
fffd96b7 | 5066 | ## dbg('cannot convert string to valid date') |
d14a1e28 RD |
5067 | dateOk = False |
5068 | if not dateOk: | |
5069 | valid = False | |
5070 | ||
5071 | if valid: | |
5072 | # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, | |
5073 | # so we eliminate them here: | |
5074 | timeStr = text[self._dateExtent+1:].strip() ## time portion of the string | |
5075 | if timeStr: | |
fffd96b7 | 5076 | ## dbg('timeStr: "%s"' % timeStr) |
d14a1e28 RD |
5077 | try: |
5078 | checkTime = dateHandler.ParseTime(timeStr) | |
5079 | valid = checkTime == len(timeStr) | |
5080 | except: | |
5081 | valid = False | |
5082 | if not valid: | |
fffd96b7 RD |
5083 | ## dbg('cannot convert string to valid time') |
5084 | pass | |
d14a1e28 | 5085 | if valid: dbg('valid date') |
fffd96b7 | 5086 | ## dbg(indent=0) |
d14a1e28 RD |
5087 | return valid |
5088 | ||
5089 | ||
5090 | def _validateTime(self, candidate=None): | |
5091 | """ Validate the current time value using the provided Regex filter. | |
5092 | Generally used for character types.BufferType | |
5093 | """ | |
fffd96b7 | 5094 | ## dbg('MaskedEditMixin::_validateTime', indent=1) |
d14a1e28 RD |
5095 | # wxDateTime doesn't take kindly to leading/trailing spaces when parsing, |
5096 | # so we eliminate them here: | |
5097 | if candidate is None: value = self._GetValue().strip() | |
5098 | else: value = candidate.strip() | |
fffd96b7 | 5099 | ## dbg('value = "%s"' % value) |
d14a1e28 RD |
5100 | valid = True # assume True until proven otherwise |
5101 | ||
b881fc78 | 5102 | dateHandler = wx.DateTime_Today() |
d14a1e28 RD |
5103 | try: |
5104 | checkTime = dateHandler.ParseTime(value) | |
fffd96b7 | 5105 | ## dbg('checkTime:', checkTime, 'len(value)', len(value)) |
d14a1e28 RD |
5106 | valid = checkTime == len(value) |
5107 | except: | |
5108 | valid = False | |
5109 | ||
5110 | if not valid: | |
fffd96b7 RD |
5111 | ## dbg('cannot convert string to valid time') |
5112 | pass | |
d14a1e28 | 5113 | if valid: dbg('valid time') |
fffd96b7 | 5114 | ## dbg(indent=0) |
d14a1e28 RD |
5115 | return valid |
5116 | ||
5117 | ||
5118 | def _OnKillFocus(self,event): | |
5119 | """ Handler for EVT_KILL_FOCUS event. | |
5120 | """ | |
fffd96b7 | 5121 | ## dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1) |
d14a1e28 RD |
5122 | if self._mask and self._IsEditable(): |
5123 | self._AdjustField(self._GetInsertionPoint()) | |
5124 | self._CheckValid() ## Call valid handler | |
5125 | ||
5126 | self._LostFocus() ## Provided for subclass use | |
5127 | event.Skip() | |
fffd96b7 | 5128 | ## dbg(indent=0) |
d14a1e28 RD |
5129 | |
5130 | ||
5131 | def _fixSelection(self): | |
5132 | """ | |
5133 | This gets called after the TAB traversal selection is made, if the | |
5134 | focus event was due to this, but before the EVT_LEFT_* events if | |
5135 | the focus shift was due to a mouse event. | |
5136 | ||
5137 | The trouble is that, a priori, there's no explicit notification of | |
5138 | why the focus event we received. However, the whole reason we need to | |
5139 | do this is because the default behavior on TAB traveral in a wxTextCtrl is | |
5140 | now to select the entire contents of the window, something we don't want. | |
5141 | So we can *now* test the selection range, and if it's "the whole text" | |
5142 | we can assume the cause, change the insertion point to the start of | |
5143 | the control, and deselect. | |
5144 | """ | |
fffd96b7 | 5145 | ## dbg('MaskedEditMixin::_fixSelection', indent=1) |
d14a1e28 | 5146 | if not self._mask or not self._IsEditable(): |
fffd96b7 | 5147 | ## dbg(indent=0) |
d14a1e28 RD |
5148 | return |
5149 | ||
5150 | sel_start, sel_to = self._GetSelection() | |
fffd96b7 | 5151 | ## dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty()) |
d14a1e28 RD |
5152 | |
5153 | if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space) | |
5154 | and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ): | |
5155 | # This isn't normally allowed, and so assume we got here by the new | |
5156 | # "tab traversal" behavior, so we need to reset the selection | |
5157 | # and insertion point: | |
fffd96b7 | 5158 | ## dbg('entire text selected; resetting selection to start of control') |
d14a1e28 RD |
5159 | self._goHome() |
5160 | field = self._FindField(self._GetInsertionPoint()) | |
5161 | edit_start, edit_end = field._extent | |
5162 | if field._selectOnFieldEntry: | |
5163 | self._SetInsertionPoint(edit_start) | |
5164 | self._SetSelection(edit_start, edit_end) | |
5165 | ||
5166 | elif field._insertRight: | |
5167 | self._SetInsertionPoint(edit_end) | |
5168 | self._SetSelection(edit_end, edit_end) | |
5169 | ||
5170 | elif (self._isFloat or self._isInt): | |
5171 | ||
5172 | text, signpos, right_signpos = self._getAbsValue() | |
5173 | if text is None or text == self._template: | |
5174 | integer = self._fields[0] | |
5175 | edit_start, edit_end = integer._extent | |
5176 | ||
5177 | if integer._selectOnFieldEntry: | |
fffd96b7 | 5178 | ## dbg('select on field entry:') |
d14a1e28 RD |
5179 | self._SetInsertionPoint(edit_start) |
5180 | self._SetSelection(edit_start, edit_end) | |
5181 | ||
5182 | elif integer._insertRight: | |
fffd96b7 | 5183 | ## dbg('moving insertion point to end') |
d14a1e28 RD |
5184 | self._SetInsertionPoint(edit_end) |
5185 | self._SetSelection(edit_end, edit_end) | |
5186 | else: | |
fffd96b7 | 5187 | ## dbg('numeric ctrl is empty; start at beginning after sign') |
d14a1e28 RD |
5188 | self._SetInsertionPoint(signpos+1) ## Move past minus sign space if signed |
5189 | self._SetSelection(signpos+1, signpos+1) | |
5190 | ||
5191 | elif sel_start > self._goEnd(getPosOnly=True): | |
fffd96b7 | 5192 | ## dbg('cursor beyond the end of the user input; go to end of it') |
d14a1e28 RD |
5193 | self._goEnd() |
5194 | else: | |
fffd96b7 RD |
5195 | ## dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength) |
5196 | pass | |
5197 | ## dbg(indent=0) | |
d14a1e28 RD |
5198 | |
5199 | ||
5200 | def _Keypress(self,key): | |
5201 | """ Method provided to override OnChar routine. Return False to force | |
5202 | a skip of the 'normal' OnChar process. Called before class OnChar. | |
5203 | """ | |
5204 | return True | |
5205 | ||
5206 | ||
5207 | def _LostFocus(self): | |
5208 | """ Method provided for subclasses. _LostFocus() is called after | |
5209 | the class processes its EVT_KILL_FOCUS event code. | |
5210 | """ | |
5211 | pass | |
5212 | ||
5213 | ||
5214 | def _OnDoubleClick(self, event): | |
5215 | """ selects field under cursor on dclick.""" | |
5216 | pos = self._GetInsertionPoint() | |
5217 | field = self._FindField(pos) | |
5218 | start, end = field._extent | |
5219 | self._SetInsertionPoint(start) | |
5220 | self._SetSelection(start, end) | |
5221 | ||
5222 | ||
5223 | def _Change(self): | |
5224 | """ Method provided for subclasses. Called by internal EVT_TEXT | |
5225 | handler. Return False to override the class handler, True otherwise. | |
5226 | """ | |
5227 | return True | |
5228 | ||
5229 | ||
5230 | def _Cut(self): | |
5231 | """ | |
5232 | Used to override the default Cut() method in base controls, instead | |
5233 | copying the selection to the clipboard and then blanking the selection, | |
5234 | leaving only the mask in the selected area behind. | |
5235 | Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the | |
5236 | derived control because the mixin functions can't override a method of | |
5237 | a sibling class. | |
5238 | """ | |
fffd96b7 | 5239 | ## dbg("MaskedEditMixin::_Cut", indent=1) |
d14a1e28 | 5240 | value = self._GetValue() |
fffd96b7 | 5241 | ## dbg('current value: "%s"' % value) |
d14a1e28 | 5242 | sel_start, sel_to = self._GetSelection() ## check for a range of selected text |
fffd96b7 | 5243 | ## dbg('selected text: "%s"' % value[sel_start:sel_to].strip()) |
d14a1e28 RD |
5244 | do = wxTextDataObject() |
5245 | do.SetText(value[sel_start:sel_to].strip()) | |
5246 | wxTheClipboard.Open() | |
5247 | wxTheClipboard.SetData(do) | |
5248 | wxTheClipboard.Close() | |
5249 | ||
5250 | if sel_to - sel_start != 0: | |
5251 | self._OnErase() | |
fffd96b7 | 5252 | ## dbg(indent=0) |
d14a1e28 RD |
5253 | |
5254 | ||
5255 | # WS Note: overriding Copy is no longer necessary given that you | |
5256 | # can no longer select beyond the last non-empty char in the control. | |
5257 | # | |
5258 | ## def _Copy( self ): | |
5259 | ## """ | |
5260 | ## Override the wxTextCtrl's .Copy function, with our own | |
5261 | ## that does validation. Need to strip trailing spaces. | |
5262 | ## """ | |
5263 | ## sel_start, sel_to = self._GetSelection() | |
5264 | ## select_len = sel_to - sel_start | |
5265 | ## textval = wxTextCtrl._GetValue(self) | |
5266 | ## | |
5267 | ## do = wxTextDataObject() | |
5268 | ## do.SetText(textval[sel_start:sel_to].strip()) | |
5269 | ## wxTheClipboard.Open() | |
5270 | ## wxTheClipboard.SetData(do) | |
5271 | ## wxTheClipboard.Close() | |
5272 | ||
5273 | ||
5274 | def _getClipboardContents( self ): | |
5275 | """ Subroutine for getting the current contents of the clipboard. | |
5276 | """ | |
5277 | do = wxTextDataObject() | |
5278 | wxTheClipboard.Open() | |
5279 | success = wxTheClipboard.GetData(do) | |
5280 | wxTheClipboard.Close() | |
5281 | ||
5282 | if not success: | |
5283 | return None | |
5284 | else: | |
5285 | # Remove leading and trailing spaces before evaluating contents | |
5286 | return do.GetText().strip() | |
5287 | ||
5288 | ||
5289 | def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False): | |
5290 | """ | |
5291 | Used by paste routine and field choice validation to see | |
5292 | if a given slice of paste text is legal for the area in question: | |
5293 | returns validity, replacement text, and extent of paste in | |
5294 | template. | |
5295 | """ | |
fffd96b7 RD |
5296 | ## dbg(suspend=1) |
5297 | ## dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1) | |
d14a1e28 RD |
5298 | select_length = sel_to - sel_start |
5299 | maxlength = select_length | |
fffd96b7 | 5300 | ## dbg('sel_to - sel_start:', maxlength) |
d14a1e28 RD |
5301 | if maxlength == 0: |
5302 | maxlength = self._masklength - sel_start | |
5303 | item = 'control' | |
5304 | else: | |
5305 | item = 'selection' | |
fffd96b7 | 5306 | ## dbg('maxlength:', maxlength) |
d14a1e28 RD |
5307 | length_considered = len(paste_text) |
5308 | if length_considered > maxlength: | |
fffd96b7 | 5309 | ## dbg('paste text will not fit into the %s:' % item, indent=0) |
d14a1e28 | 5310 | if raise_on_invalid: |
fffd96b7 | 5311 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
5312 | if item == 'control': |
5313 | raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) | |
5314 | else: | |
5315 | raise ValueError('"%s" will not fit into the selection' % paste_text) | |
5316 | else: | |
fffd96b7 | 5317 | ## dbg(indent=0, suspend=0) |
d14a1e28 RD |
5318 | return False, None, None |
5319 | ||
5320 | text = self._template | |
fffd96b7 | 5321 | ## dbg('length_considered:', length_considered) |
d14a1e28 RD |
5322 | |
5323 | valid_paste = True | |
5324 | replacement_text = "" | |
5325 | replace_to = sel_start | |
5326 | i = 0 | |
5327 | while valid_paste and i < length_considered and replace_to < self._masklength: | |
5328 | if paste_text[i:] == self._template[replace_to:length_considered]: | |
5329 | # remainder of paste matches template; skip char-by-char analysis | |
fffd96b7 | 5330 | ## dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered)) |
d14a1e28 RD |
5331 | replacement_text += paste_text[i:] |
5332 | replace_to = i = length_considered | |
5333 | continue | |
5334 | # else: | |
5335 | char = paste_text[i] | |
5336 | field = self._FindField(replace_to) | |
5337 | if not field._compareNoCase: | |
5338 | if field._forceupper: char = char.upper() | |
5339 | elif field._forcelower: char = char.lower() | |
5340 | ||
fffd96b7 RD |
5341 | ## dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to) |
5342 | ## dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to)) | |
d14a1e28 RD |
5343 | if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True): |
5344 | replacement_text += char | |
fffd96b7 RD |
5345 | ## dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals()) |
5346 | ## dbg("replacement_text:", '"'+replacement_text+'"') | |
d14a1e28 RD |
5347 | i += 1 |
5348 | replace_to += 1 | |
5349 | elif( char == self._template[replace_to] | |
5350 | or (self._signOk and | |
5351 | ( (i == 0 and (char == '-' or (self._useParens and char == '('))) | |
5352 | or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ): | |
5353 | replacement_text += char | |
fffd96b7 RD |
5354 | ## dbg("'%(char)s' == template(%(replace_to)d)" % locals()) |
5355 | ## dbg("replacement_text:", '"'+replacement_text+'"') | |
d14a1e28 RD |
5356 | i += 1 |
5357 | replace_to += 1 | |
5358 | else: | |
5359 | next_entry = self._findNextEntry(replace_to, adjustInsert=False) | |
5360 | if next_entry == replace_to: | |
5361 | valid_paste = False | |
5362 | else: | |
5363 | replacement_text += self._template[replace_to:next_entry] | |
fffd96b7 RD |
5364 | ## dbg("skipping template; next_entry =", next_entry) |
5365 | ## dbg("replacement_text:", '"'+replacement_text+'"') | |
d14a1e28 RD |
5366 | replace_to = next_entry # so next_entry will be considered on next loop |
5367 | ||
5368 | if not valid_paste and raise_on_invalid: | |
fffd96b7 | 5369 | ## dbg('raising exception', indent=0, suspend=0) |
d14a1e28 RD |
5370 | raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name)) |
5371 | ||
5372 | elif i < len(paste_text): | |
5373 | valid_paste = False | |
5374 | if raise_on_invalid: | |
fffd96b7 | 5375 | ## dbg('raising exception', indent=0, suspend=0) |
d14a1e28 RD |
5376 | raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name)) |
5377 | ||
fffd96b7 | 5378 | ## dbg('valid_paste?', valid_paste) |
d14a1e28 | 5379 | if valid_paste: |
fffd96b7 RD |
5380 | ## dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to) |
5381 | pass | |
5382 | ## dbg(indent=0, suspend=0) | |
d14a1e28 RD |
5383 | return valid_paste, replacement_text, replace_to |
5384 | ||
5385 | ||
5386 | def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): | |
5387 | """ | |
5388 | Used to override the base control's .Paste() function, | |
5389 | with our own that does validation. | |
5390 | Note: _Paste must be called from a Paste() override in the | |
5391 | derived control because the mixin functions can't override a | |
5392 | method of a sibling class. | |
5393 | """ | |
fffd96b7 | 5394 | ## dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1) |
d14a1e28 RD |
5395 | if value is None: |
5396 | paste_text = self._getClipboardContents() | |
5397 | else: | |
5398 | paste_text = value | |
5399 | ||
5400 | if paste_text is not None: | |
fffd96b7 | 5401 | ## dbg('paste text: "%s"' % paste_text) |
d14a1e28 RD |
5402 | # (conversion will raise ValueError if paste isn't legal) |
5403 | sel_start, sel_to = self._GetSelection() | |
fffd96b7 | 5404 | ## dbg('selection:', (sel_start, sel_to)) |
d14a1e28 RD |
5405 | |
5406 | # special case: handle allowInsert fields properly | |
5407 | field = self._FindField(sel_start) | |
5408 | edit_start, edit_end = field._extent | |
5409 | new_pos = None | |
5410 | if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end: | |
5411 | new_pos = sel_start + len(paste_text) # store for subsequent positioning | |
5412 | paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip() | |
fffd96b7 | 5413 | ## dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end) |
d14a1e28 RD |
5414 | sel_to = sel_start + len(paste_text) |
5415 | ||
5416 | # Another special case: paste won't fit, but it's a right-insert field where entire | |
5417 | # non-empty value is selected, and there's room if the selection is expanded leftward: | |
5418 | if( len(paste_text) > sel_to - sel_start | |
5419 | and field._insertRight | |
5420 | and sel_start > edit_start | |
5421 | and sel_to >= edit_end | |
5422 | and not self._GetValue()[edit_start:sel_start].strip() ): | |
5423 | # text won't fit within selection, but left of selection is empty; | |
5424 | # check to see if we can expand selection to accomodate the value: | |
5425 | empty_space = sel_start - edit_start | |
5426 | amount_needed = len(paste_text) - (sel_to - sel_start) | |
5427 | if amount_needed <= empty_space: | |
5428 | sel_start -= amount_needed | |
fffd96b7 | 5429 | ## dbg('expanded selection to:', (sel_start, sel_to)) |
d14a1e28 RD |
5430 | |
5431 | ||
5432 | # another special case: deal with signed values properly: | |
5433 | if self._signOk: | |
5434 | signedvalue, signpos, right_signpos = self._getSignedValue() | |
5435 | paste_signpos = paste_text.find('-') | |
5436 | if paste_signpos == -1: | |
5437 | paste_signpos = paste_text.find('(') | |
5438 | ||
5439 | # if paste text will result in signed value: | |
fffd96b7 RD |
5440 | #### dbg('paste_signpos != -1?', paste_signpos != -1) |
5441 | #### dbg('sel_start:', sel_start, 'signpos:', signpos) | |
5442 | #### dbg('field._insertRight?', field._insertRight) | |
5443 | #### dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos) | |
d14a1e28 RD |
5444 | if paste_signpos != -1 and (sel_start <= signpos |
5445 | or (field._insertRight and sel_start - len(paste_text) <= signpos)): | |
5446 | signed = True | |
5447 | else: | |
5448 | signed = False | |
5449 | # remove "sign" from paste text, so we can auto-adjust for sign type after paste: | |
5450 | paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','') | |
fffd96b7 | 5451 | ## dbg('unsigned paste text: "%s"' % paste_text) |
d14a1e28 RD |
5452 | else: |
5453 | signed = False | |
5454 | ||
5455 | # another special case: deal with insert-right fields when selection is empty and | |
5456 | # cursor is at end of field: | |
fffd96b7 RD |
5457 | #### dbg('field._insertRight?', field._insertRight) |
5458 | #### dbg('sel_start == edit_end?', sel_start == edit_end) | |
5459 | #### dbg('sel_start', sel_start, 'sel_to', sel_to) | |
d14a1e28 RD |
5460 | if field._insertRight and sel_start == edit_end and sel_start == sel_to: |
5461 | sel_start -= len(paste_text) | |
5462 | if sel_start < 0: | |
5463 | sel_start = 0 | |
fffd96b7 | 5464 | ## dbg('adjusted selection:', (sel_start, sel_to)) |
d14a1e28 RD |
5465 | |
5466 | try: | |
5467 | valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid) | |
5468 | except: | |
fffd96b7 | 5469 | ## dbg('exception thrown', indent=0) |
d14a1e28 RD |
5470 | raise |
5471 | ||
5472 | if not valid_paste: | |
fffd96b7 | 5473 | ## dbg('paste text not legal for the selection or portion of the control following the cursor;') |
b881fc78 RD |
5474 | if not wx.Validator_IsSilent(): |
5475 | wx.Bell() | |
fffd96b7 | 5476 | ## dbg(indent=0) |
d14a1e28 RD |
5477 | return False |
5478 | # else... | |
5479 | text = self._eraseSelection() | |
5480 | ||
5481 | new_text = text[:sel_start] + replacement_text + text[replace_to:] | |
5482 | if new_text: | |
5483 | new_text = string.ljust(new_text,self._masklength) | |
5484 | if signed: | |
5485 | new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text) | |
5486 | if new_text: | |
5487 | if self._useParens: | |
5488 | new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:] | |
5489 | else: | |
5490 | new_text = new_text[:signpos] + '-' + new_text[signpos+1:] | |
5491 | if not self._isNeg: | |
5492 | self._isNeg = 1 | |
5493 | ||
fffd96b7 | 5494 | ## dbg("new_text:", '"'+new_text+'"') |
d14a1e28 RD |
5495 | |
5496 | if not just_return_value: | |
fffd96b7 RD |
5497 | if new_text != self._GetValue(): |
5498 | self.modified = True | |
d14a1e28 RD |
5499 | if new_text == '': |
5500 | self.ClearValue() | |
5501 | else: | |
b881fc78 | 5502 | wx.CallAfter(self._SetValue, new_text) |
d14a1e28 RD |
5503 | if new_pos is None: |
5504 | new_pos = sel_start + len(replacement_text) | |
b881fc78 | 5505 | wx.CallAfter(self._SetInsertionPoint, new_pos) |
d14a1e28 | 5506 | else: |
fffd96b7 | 5507 | ## dbg(indent=0) |
d14a1e28 RD |
5508 | return new_text |
5509 | elif just_return_value: | |
fffd96b7 | 5510 | ## dbg(indent=0) |
d14a1e28 | 5511 | return self._GetValue() |
fffd96b7 | 5512 | ## dbg(indent=0) |
d14a1e28 RD |
5513 | |
5514 | def _Undo(self): | |
5515 | """ Provides an Undo() method in base controls. """ | |
fffd96b7 | 5516 | ## dbg("MaskedEditMixin::_Undo", indent=1) |
d14a1e28 RD |
5517 | value = self._GetValue() |
5518 | prev = self._prevValue | |
fffd96b7 RD |
5519 | ## dbg('current value: "%s"' % value) |
5520 | ## dbg('previous value: "%s"' % prev) | |
d14a1e28 | 5521 | if prev is None: |
fffd96b7 | 5522 | ## dbg('no previous value', indent=0) |
d14a1e28 RD |
5523 | return |
5524 | ||
5525 | elif value != prev: | |
5526 | # Determine what to select: (relies on fixed-length strings) | |
5527 | # (This is a lot harder than it would first appear, because | |
5528 | # of mask chars that stay fixed, and so break up the "diff"...) | |
5529 | ||
5530 | # Determine where they start to differ: | |
5531 | i = 0 | |
5532 | length = len(value) # (both are same length in masked control) | |
5533 | ||
5534 | while( value[:i] == prev[:i] ): | |
5535 | i += 1 | |
5536 | sel_start = i - 1 | |
5537 | ||
5538 | ||
5539 | # handle signed values carefully, so undo from signed to unsigned or vice-versa | |
5540 | # works properly: | |
5541 | if self._signOk: | |
5542 | text, signpos, right_signpos = self._getSignedValue(candidate=prev) | |
5543 | if self._useParens: | |
5544 | if prev[signpos] == '(' and prev[right_signpos] == ')': | |
5545 | self._isNeg = True | |
5546 | else: | |
5547 | self._isNeg = False | |
5548 | # eliminate source of "far-end" undo difference if using balanced parens: | |
5549 | value = value.replace(')', ' ') | |
5550 | prev = prev.replace(')', ' ') | |
5551 | elif prev[signpos] == '-': | |
5552 | self._isNeg = True | |
5553 | else: | |
5554 | self._isNeg = False | |
5555 | ||
5556 | # Determine where they stop differing in "undo" result: | |
5557 | sm = difflib.SequenceMatcher(None, a=value, b=prev) | |
5558 | i, j, k = sm.find_longest_match(sel_start, length, sel_start, length) | |
fffd96b7 | 5559 | ## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) |
d14a1e28 RD |
5560 | |
5561 | if k == 0: # no match found; select to end | |
5562 | sel_to = length | |
5563 | else: | |
5564 | code_5tuples = sm.get_opcodes() | |
5565 | for op, i1, i2, j1, j2 in code_5tuples: | |
fffd96b7 RD |
5566 | ## dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2])) |
5567 | pass | |
d14a1e28 RD |
5568 | |
5569 | diff_found = False | |
5570 | # look backward through operations needed to produce "previous" value; | |
5571 | # first change wins: | |
5572 | for next_op in range(len(code_5tuples)-1, -1, -1): | |
5573 | op, i1, i2, j1, j2 = code_5tuples[next_op] | |
fffd96b7 | 5574 | ## dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2]) |
d14a1e28 | 5575 | if op == 'insert' and prev[j1:j2] != self._template[j1:j2]: |
fffd96b7 | 5576 | ## dbg('insert found: selection =>', (j1, j2)) |
d14a1e28 RD |
5577 | sel_start = j1 |
5578 | sel_to = j2 | |
5579 | diff_found = True | |
5580 | break | |
5581 | elif op == 'delete' and value[i1:i2] != self._template[i1:i2]: | |
5582 | field = self._FindField(i2) | |
5583 | edit_start, edit_end = field._extent | |
5584 | if field._insertRight and i2 == edit_end: | |
5585 | sel_start = i2 | |
5586 | sel_to = i2 | |
5587 | else: | |
5588 | sel_start = i1 | |
5589 | sel_to = j1 | |
fffd96b7 | 5590 | ## dbg('delete found: selection =>', (sel_start, sel_to)) |
d14a1e28 RD |
5591 | diff_found = True |
5592 | break | |
5593 | elif op == 'replace': | |
fffd96b7 | 5594 | ## dbg('replace found: selection =>', (j1, j2)) |
d14a1e28 RD |
5595 | sel_start = j1 |
5596 | sel_to = j2 | |
5597 | diff_found = True | |
5598 | break | |
5599 | ||
5600 | ||
5601 | if diff_found: | |
5602 | # now go forwards, looking for earlier changes: | |
5603 | for next_op in range(len(code_5tuples)): | |
5604 | op, i1, i2, j1, j2 = code_5tuples[next_op] | |
5605 | field = self._FindField(i1) | |
5606 | if op == 'equal': | |
5607 | continue | |
5608 | elif op == 'replace': | |
fffd96b7 | 5609 | ## dbg('setting sel_start to', i1) |
d14a1e28 RD |
5610 | sel_start = i1 |
5611 | break | |
5612 | elif op == 'insert' and not value[i1:i2]: | |
fffd96b7 | 5613 | ## dbg('forward %s found' % op) |
d14a1e28 | 5614 | if prev[j1:j2].strip(): |
fffd96b7 | 5615 | ## dbg('item to insert non-empty; setting sel_start to', j1) |
d14a1e28 RD |
5616 | sel_start = j1 |
5617 | break | |
5618 | elif not field._insertRight: | |
fffd96b7 | 5619 | ## dbg('setting sel_start to inserted space:', j1) |
d14a1e28 RD |
5620 | sel_start = j1 |
5621 | break | |
5622 | elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip(): | |
5623 | continue | |
5624 | else: | |
5625 | # we've got what we need | |
5626 | break | |
5627 | ||
5628 | ||
5629 | if not diff_found: | |
fffd96b7 | 5630 | ## dbg('no insert,delete or replace found (!)') |
d14a1e28 RD |
5631 | # do "left-insert"-centric processing of difference based on l.c.s.: |
5632 | if i == j and j != sel_start: # match starts after start of selection | |
5633 | sel_to = sel_start + (j-sel_start) # select to start of match | |
5634 | else: | |
5635 | sel_to = j # (change ends at j) | |
5636 | ||
5637 | ||
5638 | # There are several situations where the calculated difference is | |
5639 | # not what we want to select. If changing sign, or just adding | |
5640 | # group characters, we really don't want to highlight the characters | |
5641 | # changed, but instead leave the cursor where it is. | |
5642 | # Also, there a situations in which the difference can be ambiguous; | |
5643 | # Consider: | |
5644 | # | |
5645 | # current value: 11234 | |
5646 | # previous value: 1111234 | |
5647 | # | |
5648 | # Where did the cursor actually lie and which 1s were selected on the delete | |
5649 | # operation? | |
5650 | # | |
5651 | # Also, difflib can "get it wrong;" Consider: | |
5652 | # | |
5653 | # current value: " 128.66" | |
5654 | # previous value: " 121.86" | |
5655 | # | |
5656 | # difflib produces the following opcodes, which are sub-optimal: | |
5657 | # equal value[0:9] ( 12) prev[0:9] ( 12) | |
5658 | # insert value[9:9] () prev[9:11] (1.) | |
5659 | # equal value[9:10] (8) prev[11:12] (8) | |
5660 | # delete value[10:11] (.) prev[12:12] () | |
5661 | # equal value[11:12] (6) prev[12:13] (6) | |
5662 | # delete value[12:13] (6) prev[13:13] () | |
5663 | # | |
5664 | # This should have been: | |
5665 | # equal value[0:9] ( 12) prev[0:9] ( 12) | |
5666 | # replace value[9:11] (8.6) prev[9:11] (1.8) | |
5667 | # equal value[12:13] (6) prev[12:13] (6) | |
5668 | # | |
5669 | # But it didn't figure this out! | |
5670 | # | |
5671 | # To get all this right, we use the previous selection recorded to help us... | |
5672 | ||
5673 | if (sel_start, sel_to) != self._prevSelection: | |
fffd96b7 | 5674 | ## dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection) |
d14a1e28 RD |
5675 | |
5676 | prev_sel_start, prev_sel_to = self._prevSelection | |
5677 | field = self._FindField(sel_start) | |
5678 | ||
5679 | if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')') | |
5680 | or self._curValue[sel_start] in ('-', '(', ')')): | |
5681 | # change of sign; leave cursor alone... | |
5682 | sel_start, sel_to = self._prevSelection | |
5683 | ||
5684 | elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar | |
5685 | or self._prevValue[sel_start:sel_to] == field._groupChar): | |
5686 | # do not highlight grouping changes | |
5687 | sel_start, sel_to = self._prevSelection | |
5688 | ||
5689 | else: | |
5690 | calc_select_len = sel_to - sel_start | |
5691 | prev_select_len = prev_sel_to - prev_sel_start | |
5692 | ||
fffd96b7 RD |
5693 | ## dbg('sel_start == prev_sel_start', sel_start == prev_sel_start) |
5694 | ## dbg('sel_to > prev_sel_to', sel_to > prev_sel_to) | |
d14a1e28 RD |
5695 | |
5696 | if prev_select_len >= calc_select_len: | |
5697 | # old selection was bigger; trust it: | |
5698 | sel_start, sel_to = self._prevSelection | |
5699 | ||
5700 | elif( sel_to > prev_sel_to # calculated select past last selection | |
5701 | and prev_sel_to < len(self._template) # and prev_sel_to not at end of control | |
5702 | and sel_to == len(self._template) ): # and calculated selection goes to end of control | |
5703 | ||
5704 | i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length) | |
fffd96b7 | 5705 | ## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] ) |
d14a1e28 RD |
5706 | if k > 0: |
5707 | # difflib must not have optimized opcodes properly; | |
5708 | sel_to = j | |
5709 | ||
5710 | else: | |
5711 | # look for possible ambiguous diff: | |
5712 | ||
5713 | # if last change resulted in no selection, test from resulting cursor position: | |
5714 | if prev_sel_start == prev_sel_to: | |
5715 | calc_select_len = sel_to - sel_start | |
5716 | field = self._FindField(prev_sel_start) | |
5717 | ||
5718 | # determine which way to search from last cursor position for ambiguous change: | |
5719 | if field._insertRight: | |
5720 | test_sel_start = prev_sel_start | |
5721 | test_sel_to = prev_sel_start + calc_select_len | |
5722 | else: | |
5723 | test_sel_start = prev_sel_start - calc_select_len | |
5724 | test_sel_to = prev_sel_start | |
5725 | else: | |
5726 | test_sel_start, test_sel_to = prev_sel_start, prev_sel_to | |
5727 | ||
fffd96b7 RD |
5728 | ## dbg('test selection:', (test_sel_start, test_sel_to)) |
5729 | ## dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to]) | |
5730 | ## dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to]) | |
d14a1e28 RD |
5731 | |
5732 | # if calculated selection spans characters, and same characters | |
5733 | # "before" the previous insertion point are present there as well, | |
5734 | # select the ones related to the last known selection instead. | |
5735 | if( sel_start != sel_to | |
5736 | and test_sel_to < len(self._template) | |
5737 | and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ): | |
5738 | ||
5739 | sel_start, sel_to = test_sel_start, test_sel_to | |
5740 | ||
fffd96b7 RD |
5741 | ## dbg('sel_start, sel_to:', sel_start, sel_to) |
5742 | ## dbg('previous value: "%s"' % self._prevValue) | |
d14a1e28 RD |
5743 | self._SetValue(self._prevValue) |
5744 | self._SetInsertionPoint(sel_start) | |
5745 | self._SetSelection(sel_start, sel_to) | |
5746 | else: | |
fffd96b7 RD |
5747 | ## dbg('no difference between previous value') |
5748 | pass | |
5749 | ## dbg(indent=0) | |
d14a1e28 RD |
5750 | |
5751 | ||
5752 | def _OnClear(self, event): | |
5753 | """ Provides an action for context menu delete operation """ | |
5754 | self.ClearValue() | |
5755 | ||
5756 | ||
5757 | def _OnContextMenu(self, event): | |
fffd96b7 | 5758 | ## dbg('MaskedEditMixin::OnContextMenu()', indent=1) |
d14a1e28 RD |
5759 | menu = wxMenu() |
5760 | menu.Append(wxID_UNDO, "Undo", "") | |
5761 | menu.AppendSeparator() | |
5762 | menu.Append(wxID_CUT, "Cut", "") | |
5763 | menu.Append(wxID_COPY, "Copy", "") | |
5764 | menu.Append(wxID_PASTE, "Paste", "") | |
5765 | menu.Append(wxID_CLEAR, "Delete", "") | |
5766 | menu.AppendSeparator() | |
5767 | menu.Append(wxID_SELECTALL, "Select All", "") | |
5768 | ||
5769 | EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z) | |
5770 | EVT_MENU(menu, wxID_CUT, self._OnCtrl_X) | |
5771 | EVT_MENU(menu, wxID_COPY, self._OnCtrl_C) | |
5772 | EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V) | |
5773 | EVT_MENU(menu, wxID_CLEAR, self._OnClear) | |
5774 | EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A) | |
5775 | ||
5776 | # ## WSS: The base control apparently handles | |
5777 | # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE | |
5778 | # and wxID_CLEAR menu items even if the menu is one | |
5779 | # we created. However, it doesn't do undo properly, | |
5780 | # so we're keeping track of previous values ourselves. | |
5781 | # Therefore, we have to override the default update for | |
5782 | # that item on the menu: | |
5783 | EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI) | |
5784 | self._contextMenu = menu | |
5785 | ||
5786 | self.PopupMenu(menu, event.GetPosition()) | |
5787 | menu.Destroy() | |
5788 | self._contextMenu = None | |
fffd96b7 | 5789 | ## dbg(indent=0) |
d14a1e28 RD |
5790 | |
5791 | def _UndoUpdateUI(self, event): | |
5792 | if self._prevValue is None or self._prevValue == self._curValue: | |
5793 | self._contextMenu.Enable(wxID_UNDO, False) | |
5794 | else: | |
5795 | self._contextMenu.Enable(wxID_UNDO, True) | |
5796 | ||
5797 | ||
fffd96b7 RD |
5798 | def _OnCtrlParametersChanged(self): |
5799 | """ | |
5800 | Overridable function to allow derived classes to take action as a | |
5801 | result of parameter changes prior to possibly changing the value | |
5802 | of the control. | |
5803 | """ | |
5804 | pass | |
5805 | ||
5806 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
5807 | # ## TRICKY BIT: to avoid a ton of boiler-plate, and to | |
5808 | # ## automate the getter/setter generation for each valid | |
5809 | # ## control parameter so we never forget to add the | |
5810 | # ## functions when adding parameters, this loop | |
5811 | # ## programmatically adds them to the class: | |
5812 | # ## (This makes it easier for Designers like Boa to | |
5813 | # ## deal with masked controls.) | |
5814 | # | |
5815 | # ## To further complicate matters, this is done with an | |
5816 | # ## extra level of inheritance, so that "general" classes like | |
5817 | # ## MaskedTextCtrl can have all possible attributes, | |
5818 | # ## while derived classes, like TimeCtrl and MaskedNumCtrl | |
5819 | # ## can prevent exposure of those optional attributes of their base | |
5820 | # ## class that do not make sense for their derivation. Therefore, | |
5821 | # ## we define | |
5822 | # ## BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin) | |
5823 | # ## and | |
5824 | # ## MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin). | |
5825 | # ## | |
5826 | # ## This allows us to then derive: | |
5827 | # ## MaskedNumCtrl( BaseMaskedTextCtrl ) | |
5828 | # ## | |
5829 | # ## and not have to expose all the same accessor functions for the | |
5830 | # ## derived control when they don't all make sense for it. | |
5831 | # ## | |
5832 | class MaskedEditAccessorsMixin: | |
5833 | ||
5834 | # Define the default set of attributes exposed by the most generic masked controls: | |
5835 | exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys() | |
5836 | exposed_basectrl_params.remove('index') | |
5837 | exposed_basectrl_params.remove('extent') | |
5838 | exposed_basectrl_params.remove('foregroundColour') # (base class already has this) | |
5839 | ||
5840 | for param in exposed_basectrl_params: | |
5841 | propname = param[0].upper() + param[1:] | |
5842 | exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
5843 | exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
5844 | ||
5845 | if param.find('Colour') != -1: | |
5846 | # add non-british spellings, for backward-compatibility | |
5847 | propname.replace('Colour', 'Color') | |
5848 | ||
5849 | exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
5850 | exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
5851 | ||
d14a1e28 | 5852 | |
fffd96b7 RD |
5853 | |
5854 | ||
5855 | class BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ): | |
d14a1e28 | 5856 | """ |
d4b73b1b | 5857 | This is the primary derivation from MaskedEditMixin. It provides |
d14a1e28 | 5858 | a general masked text control that can be configured with different |
fffd96b7 RD |
5859 | masks. It's actually a "base masked textCtrl", so that the |
5860 | MaskedTextCtrl class can be derived from it, and add those | |
5861 | accessor functions to it that are appropriate to the general class, | |
5862 | whilst other classes can derive from BaseMaskedTextCtrl, and | |
5863 | only define those accessor functions that are appropriate for | |
5864 | those derivations. | |
d14a1e28 RD |
5865 | """ |
5866 | ||
5867 | def __init__( self, parent, id=-1, value = '', | |
b881fc78 RD |
5868 | pos = wx.DefaultPosition, |
5869 | size = wx.DefaultSize, | |
5870 | style = wx.TE_PROCESS_TAB, | |
5871 | validator=wx.DefaultValidator, ## placeholder provided for data-transfer logic | |
d14a1e28 RD |
5872 | name = 'maskedTextCtrl', |
5873 | setupEventHandling = True, ## setup event handling by default | |
5874 | **kwargs): | |
5875 | ||
b881fc78 | 5876 | wx.TextCtrl.__init__(self, parent, id, value='', |
d14a1e28 RD |
5877 | pos=pos, size = size, |
5878 | style=style, validator=validator, | |
5879 | name=name) | |
5880 | ||
5881 | self.controlInitialized = True | |
d4b73b1b | 5882 | MaskedEditMixin.__init__( self, name, **kwargs ) |
fffd96b7 | 5883 | |
d14a1e28 RD |
5884 | self._SetInitialValue(value) |
5885 | ||
5886 | if setupEventHandling: | |
5887 | ## Setup event handlers | |
b881fc78 RD |
5888 | self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection |
5889 | self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator | |
5890 | self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick | |
5891 | self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu | |
5892 | self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. | |
5893 | self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress | |
5894 | self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep | |
d14a1e28 RD |
5895 | ## track of previous value for undo |
5896 | ||
5897 | ||
5898 | def __repr__(self): | |
fffd96b7 | 5899 | return "<BaseMaskedTextCtrl: %s>" % self.GetValue() |
d14a1e28 RD |
5900 | |
5901 | ||
5902 | def _GetSelection(self): | |
5903 | """ | |
5904 | Allow mixin to get the text selection of this control. | |
d4b73b1b | 5905 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
5906 | """ |
5907 | return self.GetSelection() | |
5908 | ||
5909 | def _SetSelection(self, sel_start, sel_to): | |
5910 | """ | |
5911 | Allow mixin to set the text selection of this control. | |
d4b73b1b | 5912 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 | 5913 | """ |
fffd96b7 | 5914 | #### dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) |
d14a1e28 RD |
5915 | return self.SetSelection( sel_start, sel_to ) |
5916 | ||
5917 | def SetSelection(self, sel_start, sel_to): | |
5918 | """ | |
5919 | This is just for debugging... | |
5920 | """ | |
fffd96b7 | 5921 | ## dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals()) |
b881fc78 | 5922 | wx.TextCtrl.SetSelection(self, sel_start, sel_to) |
d14a1e28 RD |
5923 | |
5924 | ||
5925 | def _GetInsertionPoint(self): | |
5926 | return self.GetInsertionPoint() | |
5927 | ||
5928 | def _SetInsertionPoint(self, pos): | |
fffd96b7 | 5929 | #### dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals()) |
d14a1e28 RD |
5930 | self.SetInsertionPoint(pos) |
5931 | ||
5932 | def SetInsertionPoint(self, pos): | |
5933 | """ | |
5934 | This is just for debugging... | |
5935 | """ | |
fffd96b7 | 5936 | ## dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals()) |
b881fc78 | 5937 | wx.TextCtrl.SetInsertionPoint(self, pos) |
d14a1e28 RD |
5938 | |
5939 | ||
5940 | def _GetValue(self): | |
5941 | """ | |
5942 | Allow mixin to get the raw value of the control with this function. | |
d4b73b1b | 5943 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
5944 | """ |
5945 | return self.GetValue() | |
5946 | ||
5947 | def _SetValue(self, value): | |
5948 | """ | |
5949 | Allow mixin to set the raw value of the control with this function. | |
d4b73b1b | 5950 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 | 5951 | """ |
fffd96b7 | 5952 | ## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1) |
d14a1e28 RD |
5953 | # Record current selection and insertion point, for undo |
5954 | self._prevSelection = self._GetSelection() | |
5955 | self._prevInsertionPoint = self._GetInsertionPoint() | |
b881fc78 | 5956 | wx.TextCtrl.SetValue(self, value) |
fffd96b7 | 5957 | ## dbg(indent=0) |
d14a1e28 RD |
5958 | |
5959 | def SetValue(self, value): | |
5960 | """ | |
5961 | This function redefines the externally accessible .SetValue to be | |
5962 | a smart "paste" of the text in question, so as not to corrupt the | |
5963 | masked control. NOTE: this must be done in the class derived | |
5964 | from the base wx control. | |
5965 | """ | |
fffd96b7 | 5966 | ## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1) |
d14a1e28 RD |
5967 | |
5968 | if not self._mask: | |
b881fc78 | 5969 | wx.TextCtrl.SetValue(self, value) # revert to base control behavior |
d14a1e28 RD |
5970 | return |
5971 | ||
5972 | # empty previous contents, replacing entire value: | |
5973 | self._SetInsertionPoint(0) | |
5974 | self._SetSelection(0, self._masklength) | |
5975 | if self._signOk and self._useParens: | |
5976 | signpos = value.find('-') | |
5977 | if signpos != -1: | |
5978 | value = value[:signpos] + '(' + value[signpos+1:].strip() + ')' | |
5979 | elif value.find(')') == -1 and len(value) < self._masklength: | |
5980 | value += ' ' # add place holder for reserved space for right paren | |
5981 | ||
5982 | if( len(value) < self._masklength # value shorter than control | |
5983 | and (self._isFloat or self._isInt) # and it's a numeric control | |
5984 | and self._ctrl_constraints._alignRight ): # and it's a right-aligned control | |
5985 | ||
fffd96b7 | 5986 | ## dbg('len(value)', len(value), ' < self._masklength', self._masklength) |
d14a1e28 RD |
5987 | # try to intelligently "pad out" the value to the right size: |
5988 | value = self._template[0:self._masklength - len(value)] + value | |
5989 | if self._isFloat and value.find('.') == -1: | |
5990 | value = value[1:] | |
fffd96b7 | 5991 | ## dbg('padded value = "%s"' % value) |
d14a1e28 RD |
5992 | |
5993 | # make SetValue behave the same as if you had typed the value in: | |
5994 | try: | |
5995 | value = self._Paste(value, raise_on_invalid=True, just_return_value=True) | |
5996 | if self._isFloat: | |
5997 | self._isNeg = False # (clear current assumptions) | |
5998 | value = self._adjustFloat(value) | |
5999 | elif self._isInt: | |
6000 | self._isNeg = False # (clear current assumptions) | |
6001 | value = self._adjustInt(value) | |
6002 | elif self._isDate and not self.IsValid(value) and self._4digityear: | |
7722248d | 6003 | value = self._adjustDate(value, fixcentury=True) |
d14a1e28 RD |
6004 | except ValueError: |
6005 | # If date, year might be 2 digits vs. 4; try adjusting it: | |
6006 | if self._isDate and self._4digityear: | |
6007 | dateparts = value.split(' ') | |
7722248d | 6008 | dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) |
d14a1e28 | 6009 | value = string.join(dateparts, ' ') |
fffd96b7 | 6010 | ## dbg('adjusted value: "%s"' % value) |
d14a1e28 RD |
6011 | value = self._Paste(value, raise_on_invalid=True, just_return_value=True) |
6012 | else: | |
fffd96b7 | 6013 | ## dbg('exception thrown', indent=0) |
d14a1e28 RD |
6014 | raise |
6015 | ||
fffd96b7 RD |
6016 | self._SetValue(value) # note: to preserve similar capability, .SetValue() |
6017 | # does not change IsModified() | |
6018 | #### dbg('queuing insertion after .SetValue', self._masklength) | |
b881fc78 RD |
6019 | wx.CallAfter(self._SetInsertionPoint, self._masklength) |
6020 | wx.CallAfter(self._SetSelection, self._masklength, self._masklength) | |
fffd96b7 | 6021 | ## dbg(indent=0) |
d14a1e28 RD |
6022 | |
6023 | ||
6024 | def Clear(self): | |
6025 | """ Blanks the current control value by replacing it with the default value.""" | |
fffd96b7 | 6026 | ## dbg("MaskedTextCtrl::Clear - value reset to default value (template)") |
d14a1e28 RD |
6027 | if self._mask: |
6028 | self.ClearValue() | |
6029 | else: | |
b881fc78 | 6030 | wx.TextCtrl.Clear(self) # else revert to base control behavior |
d14a1e28 RD |
6031 | |
6032 | ||
6033 | def _Refresh(self): | |
6034 | """ | |
6035 | Allow mixin to refresh the base control with this function. | |
d4b73b1b | 6036 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 | 6037 | """ |
fffd96b7 | 6038 | ## dbg('MaskedTextCtrl::_Refresh', indent=1) |
b881fc78 | 6039 | wx.TextCtrl.Refresh(self) |
fffd96b7 | 6040 | ## dbg(indent=0) |
d14a1e28 RD |
6041 | |
6042 | ||
6043 | def Refresh(self): | |
6044 | """ | |
6045 | This function redefines the externally accessible .Refresh() to | |
6046 | validate the contents of the masked control as it refreshes. | |
6047 | NOTE: this must be done in the class derived from the base wx control. | |
6048 | """ | |
fffd96b7 | 6049 | ## dbg('MaskedTextCtrl::Refresh', indent=1) |
d14a1e28 RD |
6050 | self._CheckValid() |
6051 | self._Refresh() | |
fffd96b7 | 6052 | ## dbg(indent=0) |
d14a1e28 RD |
6053 | |
6054 | ||
6055 | def _IsEditable(self): | |
6056 | """ | |
6057 | Allow mixin to determine if the base control is editable with this function. | |
d4b73b1b | 6058 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 | 6059 | """ |
b881fc78 | 6060 | return wx.TextCtrl.IsEditable(self) |
d14a1e28 RD |
6061 | |
6062 | ||
6063 | def Cut(self): | |
6064 | """ | |
6065 | This function redefines the externally accessible .Cut to be | |
6066 | a smart "erase" of the text in question, so as not to corrupt the | |
6067 | masked control. NOTE: this must be done in the class derived | |
6068 | from the base wx control. | |
6069 | """ | |
6070 | if self._mask: | |
6071 | self._Cut() # call the mixin's Cut method | |
6072 | else: | |
b881fc78 | 6073 | wx.TextCtrl.Cut(self) # else revert to base control behavior |
d14a1e28 RD |
6074 | |
6075 | ||
6076 | def Paste(self): | |
6077 | """ | |
6078 | This function redefines the externally accessible .Paste to be | |
6079 | a smart "paste" of the text in question, so as not to corrupt the | |
6080 | masked control. NOTE: this must be done in the class derived | |
6081 | from the base wx control. | |
6082 | """ | |
6083 | if self._mask: | |
6084 | self._Paste() # call the mixin's Paste method | |
6085 | else: | |
b881fc78 | 6086 | wx.TextCtrl.Paste(self, value) # else revert to base control behavior |
d14a1e28 RD |
6087 | |
6088 | ||
6089 | def Undo(self): | |
6090 | """ | |
6091 | This function defines the undo operation for the control. (The default | |
6092 | undo is 1-deep.) | |
6093 | """ | |
6094 | if self._mask: | |
6095 | self._Undo() | |
6096 | else: | |
b881fc78 | 6097 | wx.TextCtrl.Undo(self) # else revert to base control behavior |
d14a1e28 RD |
6098 | |
6099 | ||
6100 | def IsModified(self): | |
6101 | """ | |
6102 | This function overrides the raw wxTextCtrl method, because the | |
6103 | masked edit mixin uses SetValue to change the value, which doesn't | |
6104 | modify the state of this attribute. So, we keep track on each | |
6105 | keystroke to see if the value changes, and if so, it's been | |
6106 | modified. | |
6107 | """ | |
b881fc78 | 6108 | return wx.TextCtrl.IsModified(self) or self.modified |
d14a1e28 RD |
6109 | |
6110 | ||
6111 | def _CalcSize(self, size=None): | |
6112 | """ | |
6113 | Calculate automatic size if allowed; use base mixin function. | |
6114 | """ | |
6115 | return self._calcSize(size) | |
6116 | ||
6117 | ||
fffd96b7 RD |
6118 | class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ): |
6119 | """ | |
6120 | This extra level of inheritance allows us to add the generic set of | |
6121 | masked edit parameters only to this class while allowing other | |
6122 | classes to derive from the "base" masked text control, and provide | |
6123 | a smaller set of valid accessor functions. | |
6124 | """ | |
6125 | pass | |
6126 | ||
6127 | ||
d14a1e28 RD |
6128 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- |
6129 | ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX | |
6130 | ## events, we have to do it ourselves when we auto-complete. | |
d4b73b1b | 6131 | class MaskedComboBoxSelectEvent(wx.PyCommandEvent): |
d14a1e28 | 6132 | def __init__(self, id, selection = 0, object=None): |
ae18f610 | 6133 | wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id) |
d14a1e28 RD |
6134 | |
6135 | self.__selection = selection | |
6136 | self.SetEventObject(object) | |
6137 | ||
6138 | def GetSelection(self): | |
6139 | """Retrieve the value of the control at the time | |
6140 | this event was generated.""" | |
6141 | return self.__selection | |
6142 | ||
6143 | ||
fffd96b7 | 6144 | class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ): |
d14a1e28 RD |
6145 | """ |
6146 | This masked edit control adds the ability to use a masked input | |
6147 | on a combobox, and do auto-complete of such values. | |
6148 | """ | |
6149 | def __init__( self, parent, id=-1, value = '', | |
b881fc78 RD |
6150 | pos = wx.DefaultPosition, |
6151 | size = wx.DefaultSize, | |
d14a1e28 | 6152 | choices = [], |
b881fc78 RD |
6153 | style = wx.CB_DROPDOWN, |
6154 | validator = wx.DefaultValidator, | |
d14a1e28 RD |
6155 | name = "maskedComboBox", |
6156 | setupEventHandling = True, ## setup event handling by default): | |
6157 | **kwargs): | |
6158 | ||
6159 | ||
6160 | # This is necessary, because wxComboBox currently provides no | |
6161 | # method for determining later if this was specified in the | |
6162 | # constructor for the control... | |
b881fc78 | 6163 | self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY |
d14a1e28 RD |
6164 | |
6165 | kwargs['choices'] = choices ## set up maskededit to work with choice list too | |
6166 | ||
6167 | ## Since combobox completion is case-insensitive, always validate same way | |
6168 | if not kwargs.has_key('compareNoCase'): | |
6169 | kwargs['compareNoCase'] = True | |
6170 | ||
d4b73b1b | 6171 | MaskedEditMixin.__init__( self, name, **kwargs ) |
fffd96b7 | 6172 | |
d14a1e28 | 6173 | self._choices = self._ctrl_constraints._choices |
fffd96b7 | 6174 | ## dbg('self._choices:', self._choices) |
d14a1e28 RD |
6175 | |
6176 | if self._ctrl_constraints._alignRight: | |
6177 | choices = [choice.rjust(self._masklength) for choice in choices] | |
6178 | else: | |
6179 | choices = [choice.ljust(self._masklength) for choice in choices] | |
6180 | ||
b881fc78 | 6181 | wx.ComboBox.__init__(self, parent, id, value='', |
d14a1e28 | 6182 | pos=pos, size = size, |
b881fc78 | 6183 | choices=choices, style=style|wx.WANTS_CHARS, |
d14a1e28 RD |
6184 | validator=validator, |
6185 | name=name) | |
6186 | ||
6187 | self.controlInitialized = True | |
6188 | ||
6189 | # Set control font - fixed width by default | |
6190 | self._setFont() | |
6191 | ||
6192 | if self._autofit: | |
6193 | self.SetClientSize(self._CalcSize()) | |
6194 | ||
6195 | if value: | |
6196 | # ensure value is width of the mask of the control: | |
6197 | if self._ctrl_constraints._alignRight: | |
6198 | value = value.rjust(self._masklength) | |
6199 | else: | |
6200 | value = value.ljust(self._masklength) | |
6201 | ||
6202 | if self.__readonly: | |
6203 | self.SetStringSelection(value) | |
6204 | else: | |
6205 | self._SetInitialValue(value) | |
6206 | ||
6207 | ||
b881fc78 RD |
6208 | self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice) |
6209 | self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice) | |
d14a1e28 RD |
6210 | |
6211 | if setupEventHandling: | |
6212 | ## Setup event handlers | |
b881fc78 RD |
6213 | self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection |
6214 | self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator | |
6215 | self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick | |
6216 | self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu | |
6217 | self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress | |
6218 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown ) ## for special processing of up/down keys | |
6219 | self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys | |
6220 | ## (next in evt chain) | |
6221 | self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep | |
d14a1e28 RD |
6222 | ## track of previous value for undo |
6223 | ||
6224 | ||
6225 | ||
6226 | def __repr__(self): | |
d4b73b1b | 6227 | return "<MaskedComboBox: %s>" % self.GetValue() |
d14a1e28 RD |
6228 | |
6229 | ||
6230 | def _CalcSize(self, size=None): | |
6231 | """ | |
6232 | Calculate automatic size if allowed; augment base mixin function | |
6233 | to account for the selector button. | |
6234 | """ | |
6235 | size = self._calcSize(size) | |
6236 | return (size[0]+20, size[1]) | |
6237 | ||
6238 | ||
6239 | def _GetSelection(self): | |
6240 | """ | |
6241 | Allow mixin to get the text selection of this control. | |
d4b73b1b | 6242 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
6243 | """ |
6244 | return self.GetMark() | |
6245 | ||
6246 | def _SetSelection(self, sel_start, sel_to): | |
6247 | """ | |
6248 | Allow mixin to set the text selection of this control. | |
d4b73b1b | 6249 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
6250 | """ |
6251 | return self.SetMark( sel_start, sel_to ) | |
6252 | ||
6253 | ||
6254 | def _GetInsertionPoint(self): | |
6255 | return self.GetInsertionPoint() | |
6256 | ||
6257 | def _SetInsertionPoint(self, pos): | |
6258 | self.SetInsertionPoint(pos) | |
6259 | ||
6260 | ||
6261 | def _GetValue(self): | |
6262 | """ | |
6263 | Allow mixin to get the raw value of the control with this function. | |
d4b73b1b | 6264 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
6265 | """ |
6266 | return self.GetValue() | |
6267 | ||
6268 | def _SetValue(self, value): | |
6269 | """ | |
6270 | Allow mixin to set the raw value of the control with this function. | |
d4b73b1b | 6271 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
6272 | """ |
6273 | # For wxComboBox, ensure that values are properly padded so that | |
6274 | # if varying length choices are supplied, they always show up | |
6275 | # in the window properly, and will be the appropriate length | |
6276 | # to match the mask: | |
6277 | if self._ctrl_constraints._alignRight: | |
6278 | value = value.rjust(self._masklength) | |
6279 | else: | |
6280 | value = value.ljust(self._masklength) | |
6281 | ||
6282 | # Record current selection and insertion point, for undo | |
6283 | self._prevSelection = self._GetSelection() | |
6284 | self._prevInsertionPoint = self._GetInsertionPoint() | |
b881fc78 | 6285 | wx.ComboBox.SetValue(self, value) |
d14a1e28 RD |
6286 | # text change events don't always fire, so we check validity here |
6287 | # to make certain formatting is applied: | |
6288 | self._CheckValid() | |
6289 | ||
6290 | def SetValue(self, value): | |
6291 | """ | |
6292 | This function redefines the externally accessible .SetValue to be | |
6293 | a smart "paste" of the text in question, so as not to corrupt the | |
6294 | masked control. NOTE: this must be done in the class derived | |
6295 | from the base wx control. | |
6296 | """ | |
6297 | if not self._mask: | |
b881fc78 | 6298 | wx.ComboBox.SetValue(value) # revert to base control behavior |
d14a1e28 RD |
6299 | return |
6300 | # else... | |
6301 | # empty previous contents, replacing entire value: | |
6302 | self._SetInsertionPoint(0) | |
6303 | self._SetSelection(0, self._masklength) | |
6304 | ||
6305 | if( len(value) < self._masklength # value shorter than control | |
6306 | and (self._isFloat or self._isInt) # and it's a numeric control | |
6307 | and self._ctrl_constraints._alignRight ): # and it's a right-aligned control | |
6308 | # try to intelligently "pad out" the value to the right size: | |
6309 | value = self._template[0:self._masklength - len(value)] + value | |
fffd96b7 | 6310 | ## dbg('padded value = "%s"' % value) |
d14a1e28 RD |
6311 | |
6312 | # For wxComboBox, ensure that values are properly padded so that | |
6313 | # if varying length choices are supplied, they always show up | |
6314 | # in the window properly, and will be the appropriate length | |
6315 | # to match the mask: | |
6316 | elif self._ctrl_constraints._alignRight: | |
6317 | value = value.rjust(self._masklength) | |
6318 | else: | |
6319 | value = value.ljust(self._masklength) | |
6320 | ||
6321 | ||
6322 | # make SetValue behave the same as if you had typed the value in: | |
6323 | try: | |
6324 | value = self._Paste(value, raise_on_invalid=True, just_return_value=True) | |
6325 | if self._isFloat: | |
6326 | self._isNeg = False # (clear current assumptions) | |
6327 | value = self._adjustFloat(value) | |
6328 | elif self._isInt: | |
6329 | self._isNeg = False # (clear current assumptions) | |
6330 | value = self._adjustInt(value) | |
6331 | elif self._isDate and not self.IsValid(value) and self._4digityear: | |
7722248d | 6332 | value = self._adjustDate(value, fixcentury=True) |
d14a1e28 RD |
6333 | except ValueError: |
6334 | # If date, year might be 2 digits vs. 4; try adjusting it: | |
6335 | if self._isDate and self._4digityear: | |
6336 | dateparts = value.split(' ') | |
7722248d | 6337 | dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True) |
d14a1e28 | 6338 | value = string.join(dateparts, ' ') |
fffd96b7 | 6339 | ## dbg('adjusted value: "%s"' % value) |
d14a1e28 RD |
6340 | value = self._Paste(value, raise_on_invalid=True, just_return_value=True) |
6341 | else: | |
6342 | raise | |
6343 | ||
6344 | self._SetValue(value) | |
fffd96b7 | 6345 | #### dbg('queuing insertion after .SetValue', self._masklength) |
b881fc78 RD |
6346 | wx.CallAfter(self._SetInsertionPoint, self._masklength) |
6347 | wx.CallAfter(self._SetSelection, self._masklength, self._masklength) | |
d14a1e28 RD |
6348 | |
6349 | ||
6350 | def _Refresh(self): | |
6351 | """ | |
6352 | Allow mixin to refresh the base control with this function. | |
d4b73b1b | 6353 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 | 6354 | """ |
b881fc78 | 6355 | wx.ComboBox.Refresh(self) |
d14a1e28 RD |
6356 | |
6357 | def Refresh(self): | |
6358 | """ | |
6359 | This function redefines the externally accessible .Refresh() to | |
6360 | validate the contents of the masked control as it refreshes. | |
6361 | NOTE: this must be done in the class derived from the base wx control. | |
6362 | """ | |
6363 | self._CheckValid() | |
6364 | self._Refresh() | |
6365 | ||
6366 | ||
6367 | def _IsEditable(self): | |
6368 | """ | |
6369 | Allow mixin to determine if the base control is editable with this function. | |
d4b73b1b | 6370 | REQUIRED by any class derived from MaskedEditMixin. |
d14a1e28 RD |
6371 | """ |
6372 | return not self.__readonly | |
6373 | ||
6374 | ||
6375 | def Cut(self): | |
6376 | """ | |
6377 | This function redefines the externally accessible .Cut to be | |
6378 | a smart "erase" of the text in question, so as not to corrupt the | |
6379 | masked control. NOTE: this must be done in the class derived | |
6380 | from the base wx control. | |
6381 | """ | |
6382 | if self._mask: | |
6383 | self._Cut() # call the mixin's Cut method | |
6384 | else: | |
b881fc78 | 6385 | wx.ComboBox.Cut(self) # else revert to base control behavior |
d14a1e28 RD |
6386 | |
6387 | ||
6388 | def Paste(self): | |
6389 | """ | |
6390 | This function redefines the externally accessible .Paste to be | |
6391 | a smart "paste" of the text in question, so as not to corrupt the | |
6392 | masked control. NOTE: this must be done in the class derived | |
6393 | from the base wx control. | |
6394 | """ | |
6395 | if self._mask: | |
6396 | self._Paste() # call the mixin's Paste method | |
6397 | else: | |
b881fc78 | 6398 | wx.ComboBox.Paste(self) # else revert to base control behavior |
d14a1e28 RD |
6399 | |
6400 | ||
6401 | def Undo(self): | |
6402 | """ | |
6403 | This function defines the undo operation for the control. (The default | |
6404 | undo is 1-deep.) | |
6405 | """ | |
6406 | if self._mask: | |
6407 | self._Undo() | |
6408 | else: | |
b881fc78 | 6409 | wx.ComboBox.Undo() # else revert to base control behavior |
d14a1e28 RD |
6410 | |
6411 | ||
6412 | def Append( self, choice, clientData=None ): | |
6413 | """ | |
6414 | This function override is necessary so we can keep track of any additions to the list | |
6415 | of choices, because wxComboBox doesn't have an accessor for the choice list. | |
6416 | The code here is the same as in the SetParameters() mixin function, but is | |
6417 | done for the individual value as appended, so the list can be built incrementally | |
6418 | without speed penalty. | |
6419 | """ | |
6420 | if self._mask: | |
6421 | if type(choice) not in (types.StringType, types.UnicodeType): | |
6422 | raise TypeError('%s: choices must be a sequence of strings' % str(self._index)) | |
6423 | elif not self.IsValid(choice): | |
6424 | raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice)) | |
6425 | ||
6426 | if not self._ctrl_constraints._choices: | |
6427 | self._ctrl_constraints._compareChoices = [] | |
6428 | self._ctrl_constraints._choices = [] | |
6429 | self._hasList = True | |
6430 | ||
6431 | compareChoice = choice.strip() | |
6432 | ||
6433 | if self._ctrl_constraints._compareNoCase: | |
6434 | compareChoice = compareChoice.lower() | |
6435 | ||
6436 | if self._ctrl_constraints._alignRight: | |
6437 | choice = choice.rjust(self._masklength) | |
6438 | else: | |
6439 | choice = choice.ljust(self._masklength) | |
6440 | if self._ctrl_constraints._fillChar != ' ': | |
6441 | choice = choice.replace(' ', self._fillChar) | |
fffd96b7 | 6442 | ## dbg('updated choice:', choice) |
d14a1e28 RD |
6443 | |
6444 | ||
6445 | self._ctrl_constraints._compareChoices.append(compareChoice) | |
6446 | self._ctrl_constraints._choices.append(choice) | |
6447 | self._choices = self._ctrl_constraints._choices # (for shorthand) | |
6448 | ||
6449 | if( not self.IsValid(choice) and | |
6450 | (not self._ctrl_constraints.IsEmpty(choice) or | |
6451 | (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ): | |
6452 | raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name)) | |
6453 | ||
b881fc78 | 6454 | wx.ComboBox.Append(self, choice, clientData) |
d14a1e28 RD |
6455 | |
6456 | ||
6457 | ||
6458 | def Clear( self ): | |
6459 | """ | |
6460 | This function override is necessary so we can keep track of any additions to the list | |
6461 | of choices, because wxComboBox doesn't have an accessor for the choice list. | |
6462 | """ | |
6463 | if self._mask: | |
6464 | self._choices = [] | |
6465 | self._ctrl_constraints._autoCompleteIndex = -1 | |
6466 | if self._ctrl_constraints._choices: | |
6467 | self.SetCtrlParameters(choices=[]) | |
b881fc78 | 6468 | wx.ComboBox.Clear(self) |
d14a1e28 RD |
6469 | |
6470 | ||
fffd96b7 | 6471 | def _OnCtrlParametersChanged(self): |
d14a1e28 | 6472 | """ |
fffd96b7 | 6473 | Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so |
d14a1e28 RD |
6474 | we can update the base control: |
6475 | """ | |
fffd96b7 | 6476 | if self.controlInitialized and self._choices != self._ctrl_constraints._choices: |
b881fc78 | 6477 | wx.ComboBox.Clear(self) |
d14a1e28 RD |
6478 | self._choices = self._ctrl_constraints._choices |
6479 | for choice in self._choices: | |
b881fc78 | 6480 | wx.ComboBox.Append( self, choice ) |
d14a1e28 RD |
6481 | |
6482 | ||
6483 | def GetMark(self): | |
6484 | """ | |
6485 | This function is a hack to make up for the fact that wxComboBox has no | |
6486 | method for returning the selected portion of its edit control. It | |
6487 | works, but has the nasty side effect of generating lots of intermediate | |
6488 | events. | |
6489 | """ | |
fffd96b7 RD |
6490 | ## dbg(suspend=1) # turn off debugging around this function |
6491 | ## dbg('MaskedComboBox::GetMark', indent=1) | |
d14a1e28 | 6492 | if self.__readonly: |
fffd96b7 | 6493 | ## dbg(indent=0) |
d14a1e28 RD |
6494 | return 0, 0 # no selection possible for editing |
6495 | ## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have! | |
6496 | sel_start = sel_to = self.GetInsertionPoint() | |
fffd96b7 | 6497 | ## dbg("current sel_start:", sel_start) |
d14a1e28 | 6498 | value = self.GetValue() |
fffd96b7 | 6499 | ## dbg('value: "%s"' % value) |
d14a1e28 RD |
6500 | |
6501 | self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any) | |
6502 | ||
b881fc78 | 6503 | wx.ComboBox.Cut(self) |
d14a1e28 | 6504 | newvalue = self.GetValue() |
fffd96b7 | 6505 | ## dbg("value after Cut operation:", newvalue) |
d14a1e28 RD |
6506 | |
6507 | if newvalue != value: # something was selected; calculate extent | |
fffd96b7 | 6508 | ## dbg("something selected") |
d14a1e28 | 6509 | sel_to = sel_start + len(value) - len(newvalue) |
b881fc78 RD |
6510 | wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change) |
6511 | wx.ComboBox.SetInsertionPoint(self, sel_start) | |
6512 | wx.ComboBox.SetMark(self, sel_start, sel_to) | |
d14a1e28 RD |
6513 | |
6514 | self._ignoreChange = False # tell _OnTextChange() to pay attn again | |
6515 | ||
fffd96b7 | 6516 | ## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) |
d14a1e28 RD |
6517 | return sel_start, sel_to |
6518 | ||
6519 | ||
6520 | def SetSelection(self, index): | |
6521 | """ | |
6522 | Necessary for bookkeeping on choice selection, to keep current value | |
6523 | current. | |
6524 | """ | |
fffd96b7 | 6525 | ## dbg('MaskedComboBox::SetSelection(%d)' % index) |
d14a1e28 RD |
6526 | if self._mask: |
6527 | self._prevValue = self._curValue | |
6528 | self._curValue = self._choices[index] | |
6529 | self._ctrl_constraints._autoCompleteIndex = index | |
b881fc78 | 6530 | wx.ComboBox.SetSelection(self, index) |
d14a1e28 RD |
6531 | |
6532 | ||
6533 | def OnKeyDown(self, event): | |
6534 | """ | |
6535 | This function is necessary because navigation and control key | |
6536 | events do not seem to normally be seen by the wxComboBox's | |
6537 | EVT_CHAR routine. (Tabs don't seem to be visible no matter | |
6538 | what... {:-( ) | |
6539 | """ | |
6540 | if event.GetKeyCode() in self._nav + self._control: | |
6541 | self._OnChar(event) | |
6542 | return | |
6543 | else: | |
6544 | event.Skip() # let mixin default KeyDown behavior occur | |
6545 | ||
6546 | ||
6547 | def OnSelectChoice(self, event): | |
6548 | """ | |
6549 | This function appears to be necessary, because the processing done | |
6550 | on the text of the control somehow interferes with the combobox's | |
6551 | selection mechanism for the arrow keys. | |
6552 | """ | |
fffd96b7 | 6553 | ## dbg('MaskedComboBox::OnSelectChoice', indent=1) |
d14a1e28 RD |
6554 | |
6555 | if not self._mask: | |
6556 | event.Skip() | |
6557 | return | |
6558 | ||
6559 | value = self.GetValue().strip() | |
6560 | ||
6561 | if self._ctrl_constraints._compareNoCase: | |
6562 | value = value.lower() | |
6563 | ||
b881fc78 | 6564 | if event.GetKeyCode() == wx.WXK_UP: |
d14a1e28 RD |
6565 | direction = -1 |
6566 | else: | |
6567 | direction = 1 | |
6568 | match_index, partial_match = self._autoComplete( | |
6569 | direction, | |
6570 | self._ctrl_constraints._compareChoices, | |
6571 | value, | |
6572 | self._ctrl_constraints._compareNoCase, | |
6573 | current_index = self._ctrl_constraints._autoCompleteIndex) | |
6574 | if match_index is not None: | |
fffd96b7 | 6575 | ## dbg('setting selection to', match_index) |
d14a1e28 RD |
6576 | # issue appropriate event to outside: |
6577 | self._OnAutoSelect(self._ctrl_constraints, match_index=match_index) | |
6578 | self._CheckValid() | |
6579 | keep_processing = False | |
6580 | else: | |
6581 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) | |
6582 | field = self._FindField(pos) | |
6583 | if self.IsEmpty() or not field._hasList: | |
fffd96b7 | 6584 | ## dbg('selecting 1st value in list') |
d14a1e28 RD |
6585 | self._OnAutoSelect(self._ctrl_constraints, match_index=0) |
6586 | self._CheckValid() | |
6587 | keep_processing = False | |
6588 | else: | |
6589 | # attempt field-level auto-complete | |
fffd96b7 | 6590 | ## dbg(indent=0) |
d14a1e28 | 6591 | keep_processing = self._OnAutoCompleteField(event) |
fffd96b7 | 6592 | ## dbg('keep processing?', keep_processing, indent=0) |
d14a1e28 RD |
6593 | return keep_processing |
6594 | ||
6595 | ||
6596 | def _OnAutoSelect(self, field, match_index): | |
6597 | """ | |
6598 | Override mixin (empty) autocomplete handler, so that autocompletion causes | |
6599 | combobox to update appropriately. | |
6600 | """ | |
fffd96b7 | 6601 | ## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) |
d14a1e28 RD |
6602 | ## field._autoCompleteIndex = match_index |
6603 | if field == self._ctrl_constraints: | |
6604 | self.SetSelection(match_index) | |
fffd96b7 | 6605 | ## dbg('issuing combo selection event') |
d14a1e28 | 6606 | self.GetEventHandler().ProcessEvent( |
d4b73b1b | 6607 | MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) ) |
d14a1e28 | 6608 | self._CheckValid() |
fffd96b7 RD |
6609 | ## dbg('field._autoCompleteIndex:', match_index) |
6610 | ## dbg('self.GetSelection():', self.GetSelection()) | |
6611 | ## dbg(indent=0) | |
d14a1e28 RD |
6612 | |
6613 | ||
6614 | def _OnReturn(self, event): | |
6615 | """ | |
6616 | For wxComboBox, it seems that if you hit return when the dropdown is | |
6617 | dropped, the event that dismisses the dropdown will also blank the | |
6618 | control, because of the implementation of wxComboBox. So here, | |
6619 | we look and if the selection is -1, and the value according to | |
6620 | (the base control!) is a value in the list, then we schedule a | |
6621 | programmatic wxComboBox.SetSelection() call to pick the appropriate | |
6622 | item in the list. (and then do the usual OnReturn bit.) | |
6623 | """ | |
fffd96b7 RD |
6624 | ## dbg('MaskedComboBox::OnReturn', indent=1) |
6625 | ## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) | |
d14a1e28 | 6626 | if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices: |
b881fc78 | 6627 | wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex) |
d14a1e28 | 6628 | |
b881fc78 | 6629 | event.m_keyCode = wx.WXK_TAB |
d14a1e28 | 6630 | event.Skip() |
fffd96b7 RD |
6631 | ## dbg(indent=0) |
6632 | ||
6633 | ||
6634 | class MaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ): | |
6635 | """ | |
6636 | This extra level of inheritance allows us to add the generic set of | |
6637 | masked edit parameters only to this class while allowing other | |
6638 | classes to derive from the "base" masked combobox control, and provide | |
6639 | a smaller set of valid accessor functions. | |
6640 | """ | |
6641 | pass | |
d14a1e28 RD |
6642 | |
6643 | ||
6644 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
6645 | ||
fffd96b7 RD |
6646 | class IpAddrCtrlAccessorsMixin: |
6647 | # Define IpAddrCtrl's list of attributes having their own | |
6648 | # Get/Set functions, exposing only those that make sense for | |
6649 | # an IP address control. | |
6650 | ||
6651 | exposed_basectrl_params = ( | |
6652 | 'fields', | |
6653 | 'retainFieldValidation', | |
6654 | 'formatcodes', | |
6655 | 'fillChar', | |
6656 | 'defaultValue', | |
6657 | 'description', | |
6658 | ||
6659 | 'useFixedWidthFont', | |
6660 | 'signedForegroundColour', | |
6661 | 'emptyBackgroundColour', | |
6662 | 'validBackgroundColour', | |
6663 | 'invalidBackgroundColour', | |
6664 | ||
6665 | 'emptyInvalid', | |
6666 | 'validFunc', | |
6667 | 'validRequired', | |
6668 | ) | |
6669 | ||
6670 | for param in exposed_basectrl_params: | |
6671 | propname = param[0].upper() + param[1:] | |
6672 | exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
6673 | exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
6674 | ||
6675 | if param.find('Colour') != -1: | |
6676 | # add non-british spellings, for backward-compatibility | |
6677 | propname.replace('Colour', 'Color') | |
6678 | ||
6679 | exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) | |
6680 | exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) | |
6681 | ||
6682 | ||
6683 | class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ): | |
d14a1e28 | 6684 | """ |
d4b73b1b | 6685 | This class is a particular type of MaskedTextCtrl that accepts |
d14a1e28 RD |
6686 | and understands the semantics of IP addresses, reformats input |
6687 | as you move from field to field, and accepts '.' as a navigation | |
6688 | character, so that typing an IP address can be done naturally. | |
6689 | """ | |
fffd96b7 RD |
6690 | |
6691 | ||
6692 | ||
d14a1e28 | 6693 | def __init__( self, parent, id=-1, value = '', |
b881fc78 RD |
6694 | pos = wx.DefaultPosition, |
6695 | size = wx.DefaultSize, | |
6696 | style = wx.TE_PROCESS_TAB, | |
6697 | validator = wx.DefaultValidator, | |
d4b73b1b | 6698 | name = 'IpAddrCtrl', |
d14a1e28 RD |
6699 | setupEventHandling = True, ## setup event handling by default |
6700 | **kwargs): | |
6701 | ||
6702 | if not kwargs.has_key('mask'): | |
6703 | kwargs['mask'] = mask = "###.###.###.###" | |
6704 | if not kwargs.has_key('formatcodes'): | |
6705 | kwargs['formatcodes'] = 'F_Sr<' | |
6706 | if not kwargs.has_key('validRegex'): | |
6707 | 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}" | |
6708 | ||
6709 | ||
fffd96b7 | 6710 | BaseMaskedTextCtrl.__init__( |
d14a1e28 RD |
6711 | self, parent, id=id, value = value, |
6712 | pos=pos, size=size, | |
6713 | style = style, | |
6714 | validator = validator, | |
6715 | name = name, | |
6716 | setupEventHandling = setupEventHandling, | |
6717 | **kwargs) | |
6718 | ||
fffd96b7 | 6719 | |
d14a1e28 RD |
6720 | # set up individual field parameters as well: |
6721 | field_params = {} | |
6722 | field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))" | |
6723 | ||
6724 | # require "valid" string; this prevents entry of any value > 255, but allows | |
6725 | # intermediate constructions; overall control validation requires well-formatted value. | |
6726 | field_params['formatcodes'] = 'V' | |
6727 | ||
6728 | if field_params: | |
6729 | for i in self._field_indices: | |
6730 | self.SetFieldParameters(i, **field_params) | |
6731 | ||
6732 | # This makes '.' act like tab: | |
6733 | self._AddNavKey('.', handler=self.OnDot) | |
6734 | self._AddNavKey('>', handler=self.OnDot) # for "shift-." | |
6735 | ||
6736 | ||
6737 | def OnDot(self, event): | |
fffd96b7 | 6738 | ## dbg('IpAddrCtrl::OnDot', indent=1) |
d14a1e28 RD |
6739 | pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode()) |
6740 | oldvalue = self.GetValue() | |
6741 | edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True) | |
6742 | if not event.ShiftDown(): | |
6743 | if pos > edit_start and pos < edit_end: | |
6744 | # clip data in field to the right of pos, if adjusting fields | |
6745 | # when not at delimeter; (assumption == they hit '.') | |
6746 | newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:] | |
6747 | self._SetValue(newvalue) | |
6748 | self._SetInsertionPoint(pos) | |
fffd96b7 | 6749 | ## dbg(indent=0) |
d14a1e28 RD |
6750 | return self._OnChangeField(event) |
6751 | ||
6752 | ||
6753 | ||
6754 | def GetAddress(self): | |
fffd96b7 | 6755 | value = BaseMaskedTextCtrl.GetValue(self) |
d14a1e28 RD |
6756 | return value.replace(' ','') # remove spaces from the value |
6757 | ||
6758 | ||
6759 | def _OnCtrl_S(self, event): | |
fffd96b7 | 6760 | ## dbg("IpAddrCtrl::_OnCtrl_S") |
d14a1e28 RD |
6761 | if self._demo: |
6762 | print "value:", self.GetAddress() | |
6763 | return False | |
6764 | ||
6765 | def SetValue(self, value): | |
fffd96b7 | 6766 | ## dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1) |
d14a1e28 | 6767 | if type(value) not in (types.StringType, types.UnicodeType): |
fffd96b7 | 6768 | ## dbg(indent=0) |
d14a1e28 RD |
6769 | raise ValueError('%s must be a string', str(value)) |
6770 | ||
7722248d | 6771 | bValid = True # assume True |
d14a1e28 RD |
6772 | parts = value.split('.') |
6773 | if len(parts) != 4: | |
6774 | bValid = False | |
6775 | else: | |
6776 | for i in range(4): | |
6777 | part = parts[i] | |
6778 | if not 0 <= len(part) <= 3: | |
6779 | bValid = False | |
6780 | break | |
6781 | elif part.strip(): # non-empty part | |
6782 | try: | |
6783 | j = string.atoi(part) | |
6784 | if not 0 <= j <= 255: | |
6785 | bValid = False | |
6786 | break | |
6787 | else: | |
6788 | parts[i] = '%3d' % j | |
6789 | except: | |
6790 | bValid = False | |
6791 | break | |
6792 | else: | |
6793 | # allow empty sections for SetValue (will result in "invalid" value, | |
6794 | # but this may be useful for initializing the control: | |
6795 | parts[i] = ' ' # convert empty field to 3-char length | |
6796 | ||
6797 | if not bValid: | |
fffd96b7 | 6798 | ## dbg(indent=0) |
d14a1e28 RD |
6799 | 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)) |
6800 | else: | |
fffd96b7 | 6801 | ## dbg('parts:', parts) |
d14a1e28 | 6802 | value = string.join(parts, '.') |
fffd96b7 RD |
6803 | BaseMaskedTextCtrl.SetValue(self, value) |
6804 | ## dbg(indent=0) | |
d14a1e28 RD |
6805 | |
6806 | ||
6807 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
6808 | ## these are helper subroutines: | |
6809 | ||
6810 | def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '): | |
6811 | """ addseparators = add separator character every three numerals if True | |
6812 | """ | |
6813 | fmt0 = fmtstring.split('.') | |
6814 | fmt1 = fmt0[0] | |
6815 | fmt2 = fmt0[1] | |
6816 | val = origvalue.split('.')[0].strip() | |
6817 | ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2) | |
6818 | if neg: | |
6819 | ret = '-' + ret[1:] | |
6820 | return (ret,len(fmt1)) | |
6821 | ||
6822 | ||
6823 | def isDateType( fmtstring ): | |
6824 | """ Checks the mask and returns True if it fits an allowed | |
6825 | date or datetime format. | |
6826 | """ | |
6827 | dateMasks = ("^##/##/####", | |
6828 | "^##-##-####", | |
6829 | "^##.##.####", | |
6830 | "^####/##/##", | |
6831 | "^####-##-##", | |
6832 | "^####.##.##", | |
6833 | "^##/CCC/####", | |
6834 | "^##.CCC.####", | |
6835 | "^##/##/##$", | |
6836 | "^##/##/## ", | |
6837 | "^##/CCC/##$", | |
6838 | "^##.CCC.## ",) | |
6839 | reString = "|".join(dateMasks) | |
6840 | filter = re.compile( reString) | |
6841 | if re.match(filter,fmtstring): return True | |
6842 | return False | |
6843 | ||
6844 | def isTimeType( fmtstring ): | |
6845 | """ Checks the mask and returns True if it fits an allowed | |
6846 | time format. | |
6847 | """ | |
6848 | reTimeMask = "^##:##(:##)?( (AM|PM))?" | |
6849 | filter = re.compile( reTimeMask ) | |
6850 | if re.match(filter,fmtstring): return True | |
6851 | return False | |
6852 | ||
6853 | ||
6854 | def isFloatingPoint( fmtstring): | |
6855 | filter = re.compile("[ ]?[#]+\.[#]+\n") | |
6856 | if re.match(filter,fmtstring+"\n"): return True | |
6857 | return False | |
6858 | ||
6859 | ||
6860 | def isInteger( fmtstring ): | |
6861 | filter = re.compile("[#]+\n") | |
6862 | if re.match(filter,fmtstring+"\n"): return True | |
6863 | return False | |
6864 | ||
6865 | ||
6866 | def getDateParts( dateStr, dateFmt ): | |
6867 | if len(dateStr) > 11: clip = dateStr[0:11] | |
6868 | else: clip = dateStr | |
6869 | if clip[-2] not in string.digits: | |
6870 | clip = clip[:-1] # (got part of time; drop it) | |
6871 | ||
6872 | dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') | |
6873 | slices = clip.split(dateSep) | |
6874 | if dateFmt == "MDY": | |
6875 | y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts | |
6876 | elif dateFmt == "DMY": | |
6877 | y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts | |
6878 | elif dateFmt == "YMD": | |
6879 | y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts | |
6880 | else: | |
6881 | y,m,d = None, None, None | |
6882 | if not y: | |
6883 | return None | |
6884 | else: | |
6885 | return y,m,d | |
6886 | ||
6887 | ||
6888 | def getDateSepChar(dateStr): | |
6889 | clip = dateStr[0:10] | |
6890 | dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.') | |
6891 | return dateSep | |
6892 | ||
6893 | ||
6894 | def makeDate( year, month, day, dateFmt, dateStr): | |
6895 | sep = getDateSepChar( dateStr) | |
6896 | if dateFmt == "MDY": | |
6897 | return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts | |
6898 | elif dateFmt == "DMY": | |
6899 | return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts | |
6900 | elif dateFmt == "YMD": | |
6901 | return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts | |
6902 | else: | |
6903 | return none | |
6904 | ||
6905 | ||
6906 | def getYear(dateStr,dateFmt): | |
6907 | parts = getDateParts( dateStr, dateFmt) | |
6908 | return parts[0] | |
6909 | ||
6910 | def getMonth(dateStr,dateFmt): | |
6911 | parts = getDateParts( dateStr, dateFmt) | |
6912 | return parts[1] | |
6913 | ||
6914 | def getDay(dateStr,dateFmt): | |
6915 | parts = getDateParts( dateStr, dateFmt) | |
6916 | return parts[2] | |
6917 | ||
6918 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
b881fc78 | 6919 | class test(wx.PySimpleApp): |
d14a1e28 | 6920 | def OnInit(self): |
b881fc78 | 6921 | from wx.lib.rcsizer import RowColSizer |
d4b73b1b | 6922 | self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600)) |
b881fc78 | 6923 | self.panel = wx.Panel( self.frame, -1) |
d14a1e28 RD |
6924 | self.sizer = RowColSizer() |
6925 | self.labels = [] | |
6926 | self.editList = [] | |
6927 | rowcount = 4 | |
6928 | ||
b881fc78 RD |
6929 | id, id1 = wx.NewId(), wx.NewId() |
6930 | self.command1 = wx.Button( self.panel, id, "&Close" ) | |
6931 | self.command2 = wx.Button( self.panel, id1, "&AutoFormats" ) | |
6932 | self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) | |
6933 | self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5) | |
6934 | self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 ) | |
d14a1e28 | 6935 | ## self.panel.SetDefaultItem(self.command1 ) |
b881fc78 | 6936 | self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2) |
d14a1e28 | 6937 | |
b881fc78 RD |
6938 | self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" ) |
6939 | self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" ) | |
6940 | self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 ) | |
6941 | self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 ) | |
6942 | self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 ) | |
6943 | self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 ) | |
d14a1e28 RD |
6944 | |
6945 | ||
6946 | label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field. | |
6947 | Note that all controls have been auto-sized by including F in the format code. | |
6948 | Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status).""" | |
6949 | label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)." | |
6950 | ||
b881fc78 RD |
6951 | self.label1 = wx.StaticText( self.panel, -1, label) |
6952 | self.label2 = wx.StaticText( self.panel, -1, "Description") | |
6953 | self.label3 = wx.StaticText( self.panel, -1, "Mask Value") | |
6954 | self.label4 = wx.StaticText( self.panel, -1, "Format") | |
6955 | self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)") | |
6956 | self.label6 = wx.StaticText( self.panel, -1, "wxMaskedEdit Ctrl") | |
6957 | self.label7 = wx.StaticText( self.panel, -1, label2) | |
d14a1e28 RD |
6958 | self.label7.SetForegroundColour("Blue") |
6959 | self.label1.SetForegroundColour("Blue") | |
b881fc78 RD |
6960 | self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) |
6961 | self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
6962 | self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
6963 | self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
6964 | self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
6965 | ||
6966 | self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5) | |
6967 | self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5) | |
6968 | self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) | |
6969 | self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) | |
6970 | self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) | |
6971 | self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5) | |
6972 | self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5) | |
d14a1e28 RD |
6973 | |
6974 | # The following list is of the controls for the demo. Feel free to play around with | |
6975 | # the options! | |
6976 | controls = [ | |
6977 | #description mask excl format regexp range,list,initial | |
6978 | ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''), | |
6979 | ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''), | |
6980 | ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''), | |
6981 | ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''), | |
6982 | ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''), | |
6983 | ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'), | |
6984 | ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''), | |
6985 | ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'), | |
6986 | ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''), | |
6987 | ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '), | |
6988 | ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '), | |
6989 | ("Month selector", "XXX", "", 'F', "", (), | |
6990 | ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""), | |
6991 | ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (), | |
6992 | ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "") | |
6993 | ] | |
6994 | ||
6995 | for control in controls: | |
b881fc78 RD |
6996 | self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) |
6997 | self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) | |
6998 | self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL) | |
6999 | self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL) | |
d14a1e28 RD |
7000 | |
7001 | if control in controls[:]:#-2]: | |
d4b73b1b | 7002 | newControl = MaskedTextCtrl( self.panel, -1, "", |
d14a1e28 RD |
7003 | mask = control[1], |
7004 | excludeChars = control[2], | |
7005 | formatcodes = control[3], | |
7006 | includeChars = "", | |
7007 | validRegex = control[4], | |
7008 | validRange = control[5], | |
7009 | choices = control[6], | |
7010 | defaultValue = control[7], | |
7011 | demo = True) | |
7012 | if control[6]: newControl.SetCtrlParameters(choiceRequired = True) | |
7013 | else: | |
d4b73b1b | 7014 | newControl = MaskedComboBox( self.panel, -1, "", |
d14a1e28 RD |
7015 | choices = control[7], |
7016 | choiceRequired = True, | |
7017 | mask = control[1], | |
7018 | formatcodes = control[3], | |
7019 | excludeChars = control[2], | |
7020 | includeChars = "", | |
7021 | validRegex = control[4], | |
7022 | validRange = control[5], | |
7023 | demo = True) | |
7024 | self.editList.append( newControl ) | |
7025 | ||
b881fc78 | 7026 | self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5) |
d14a1e28 RD |
7027 | rowcount += 1 |
7028 | ||
7029 | self.sizer.AddGrowableCol(4) | |
7030 | ||
7031 | self.panel.SetSizer(self.sizer) | |
7032 | self.panel.SetAutoLayout(1) | |
7033 | ||
7034 | self.frame.Show(1) | |
7035 | self.MainLoop() | |
7036 | ||
7037 | return True | |
7038 | ||
7039 | def onClick(self, event): | |
7040 | self.frame.Close() | |
7041 | ||
7042 | def onClickPage(self, event): | |
7043 | self.page2 = test2(self.frame,-1,"") | |
7044 | self.page2.Show(True) | |
7045 | ||
7046 | def _onCheck1(self,event): | |
7047 | """ Set required value on/off """ | |
b881fc78 | 7048 | value = event.IsChecked() |
d14a1e28 RD |
7049 | if value: |
7050 | for control in self.editList: | |
7051 | control.SetCtrlParameters(emptyInvalid=True) | |
7052 | control.Refresh() | |
7053 | else: | |
7054 | for control in self.editList: | |
7055 | control.SetCtrlParameters(emptyInvalid=False) | |
7056 | control.Refresh() | |
7057 | self.panel.Refresh() | |
7058 | ||
7059 | def _onCheck2(self,event): | |
7060 | """ Highlight empty values""" | |
b881fc78 | 7061 | value = event.IsChecked() |
d14a1e28 RD |
7062 | if value: |
7063 | for control in self.editList: | |
7064 | control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine') | |
7065 | control.Refresh() | |
7066 | else: | |
7067 | for control in self.editList: | |
7068 | control.SetCtrlParameters( emptyBackgroundColour = 'White') | |
7069 | control.Refresh() | |
7070 | self.panel.Refresh() | |
7071 | ||
7072 | ||
7073 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
7074 | ||
b881fc78 | 7075 | class test2(wx.Frame): |
d14a1e28 | 7076 | def __init__(self, parent, id, caption): |
b881fc78 RD |
7077 | wx.Frame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600)) |
7078 | from wx.lib.rcsizer import RowColSizer | |
7079 | self.panel = wx.Panel( self, -1) | |
d14a1e28 RD |
7080 | self.sizer = RowColSizer() |
7081 | self.labels = [] | |
7082 | self.texts = [] | |
7083 | rowcount = 4 | |
7084 | ||
7085 | label = """\ | |
7086 | All these controls have been created by passing a single parameter, the AutoFormat code. | |
7087 | The class contains an internal dictionary of types and formats (autoformats). | |
7088 | To see a great example of validations in action, try entering a bad email address, then tab out.""" | |
7089 | ||
b881fc78 RD |
7090 | self.label1 = wx.StaticText( self.panel, -1, label) |
7091 | self.label2 = wx.StaticText( self.panel, -1, "Description") | |
7092 | self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code") | |
7093 | self.label4 = wx.StaticText( self.panel, -1, "wxMaskedEdit Control") | |
d14a1e28 | 7094 | self.label1.SetForegroundColour("Blue") |
b881fc78 RD |
7095 | self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) |
7096 | self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
7097 | self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD)) | |
7098 | ||
7099 | self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5) | |
7100 | self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5) | |
7101 | self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5) | |
7102 | self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5) | |
7103 | ||
7104 | id, id1 = wx.NewId(), wx.NewId() | |
7105 | self.command1 = wx.Button( self.panel, id, "&Close") | |
7106 | self.command2 = wx.Button( self.panel, id1, "&Print Formats") | |
7107 | self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1) | |
d14a1e28 | 7108 | self.panel.SetDefaultItem(self.command1) |
b881fc78 | 7109 | self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2) |
d14a1e28 RD |
7110 | |
7111 | # The following list is of the controls for the demo. Feel free to play around with | |
7112 | # the options! | |
7113 | controls = [ | |
7114 | ("Phone No","USPHONEFULLEXT"), | |
7115 | ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"), | |
7116 | ("US Date MMDDYYYY","USDATEMMDDYYYY/"), | |
7117 | ("Time (with seconds)","TIMEHHMMSS"), | |
fffd96b7 | 7118 | ("Military Time\n(without seconds)","24HRTIMEHHMM"), |
d14a1e28 RD |
7119 | ("Social Sec#","USSOCIALSEC"), |
7120 | ("Credit Card","CREDITCARD"), | |
7121 | ("Expiration MM/YY","EXPDATEMMYY"), | |
7122 | ("Percentage","PERCENT"), | |
7123 | ("Person's Age","AGE"), | |
7124 | ("US Zip Code","USZIP"), | |
7125 | ("US Zip+4","USZIPPLUS4"), | |
7126 | ("Email Address","EMAIL"), | |
d4b73b1b | 7127 | ("IP Address", "(derived control IpAddrCtrl)") |
d14a1e28 RD |
7128 | ] |
7129 | ||
7130 | for control in controls: | |
b881fc78 RD |
7131 | self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL) |
7132 | self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL) | |
d14a1e28 | 7133 | if control in controls[:-1]: |
d4b73b1b | 7134 | self.sizer.Add( MaskedTextCtrl( self.panel, -1, "", |
d14a1e28 RD |
7135 | autoformat = control[1], |
7136 | demo = True), | |
b881fc78 | 7137 | row=rowcount,col=2,flag=wx.ALL,border=5) |
d14a1e28 | 7138 | else: |
d4b73b1b | 7139 | self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ), |
b881fc78 | 7140 | row=rowcount,col=2,flag=wx.ALL,border=5) |
d14a1e28 RD |
7141 | rowcount += 1 |
7142 | ||
b881fc78 RD |
7143 | self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5) |
7144 | self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5) | |
d14a1e28 RD |
7145 | self.sizer.AddGrowableCol(3) |
7146 | ||
7147 | self.panel.SetSizer(self.sizer) | |
7148 | self.panel.SetAutoLayout(1) | |
7149 | ||
7150 | def onClick(self, event): | |
7151 | self.Close() | |
7152 | ||
7153 | def onClickPrint(self, event): | |
7154 | for format in masktags.keys(): | |
7155 | sep = "+------------------------+" | |
7156 | print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex']) | |
7157 | ||
7158 | ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- | |
7159 | ||
7160 | if __name__ == "__main__": | |
b881fc78 | 7161 | app = test(False) |
d14a1e28 RD |
7162 | |
7163 | i=1 | |
7164 | ## | |
7165 | ## Current Issues: | |
7166 | ## =================================== | |
7167 | ## | |
7168 | ## 1. WS: For some reason I don't understand, the control is generating two (2) | |
7169 | ## EVT_TEXT events for every one (1) .SetValue() of the underlying control. | |
7170 | ## I've been unsuccessful in determining why or in my efforts to make just one | |
7171 | ## occur. So, I've added a hack to save the last seen value from the | |
7172 | ## control in the EVT_TEXT handler, and if *different*, call event.Skip() | |
7173 | ## to propagate it down the event chain, and let the application see it. | |
7174 | ## | |
d4b73b1b | 7175 | ## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the |
d14a1e28 RD |
7176 | ## behavior of the underlying control that I can't fix. The problems are: |
7177 | ## a) The background coloring doesn't work in the text field of the control; | |
7178 | ## instead, there's a only border around it that assumes the correct color. | |
7179 | ## b) The control will not pass WXK_TAB to the event handler, no matter what | |
7180 | ## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to | |
d4b73b1b RD |
7181 | ## indicate that we want these events. As a result, MaskedComboBox |
7182 | ## doesn't do the nice field-tabbing that MaskedTextCtrl does. | |
d14a1e28 RD |
7183 | ## c) Auto-complete had to be reimplemented for the control because programmatic |
7184 | ## setting of the value of the text field does not set up the auto complete | |
7185 | ## the way that the control processing keystrokes does. (But I think I've | |
7186 | ## implemented a fairly decent approximation.) Because of this the control | |
7187 | ## also won't auto-complete on dropdown, and there's no event I can catch | |
7188 | ## to work around this problem. | |
7189 | ## d) There is no method provided for getting the selection; the hack I've | |
7190 | ## implemented has its flaws, not the least of which is that due to the | |
7191 | ## strategy that I'm using, the paste buffer is always replaced by the | |
7192 | ## contents of the control's selection when in focus, on each keystroke; | |
d4b73b1b | 7193 | ## this makes it impossible to paste anything into a MaskedComboBox |
d14a1e28 RD |
7194 | ## at the moment... :-( |
7195 | ## e) The other deficient behavior, likely induced by the workaround for (d), | |
7196 | ## is that you can can't shift-left to select more than one character | |
7197 | ## at a time. | |
7198 | ## | |
7199 | ## | |
7200 | ## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their | |
7201 | ## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in | |
fffd96b7 | 7202 | ## wxWidgets, shift-tab won't take you backwards through the fields of |
d4b73b1b | 7203 | ## a MaskedTextCtrl like it should. Until then Shifted arrow keys will |
d14a1e28 RD |
7204 | ## work like shift-tab and tab ought to. |
7205 | ## | |
7206 | ||
7207 | ## To-Do's: | |
7208 | ## =============================## | |
7209 | ## 1. Add Popup list for auto-completable fields that simulates combobox on individual | |
7210 | ## fields. Example: City validates against list of cities, or zip vs zip code list. | |
7211 | ## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal" | |
7212 | ## control. | |
d4b73b1b | 7213 | ## 3. Fix shift-left selection for MaskedComboBox. |
d14a1e28 RD |
7214 | ## 5. Transform notion of "decimal control" to be less "entire control"-centric, |
7215 | ## so that monetary symbols can be included and still have the appropriate | |
7216 | ## semantics. (Big job, as currently written, but would make control even | |
7217 | ## more useful for business applications.) | |
7218 | ||
7219 | ||
7220 | ## CHANGELOG: | |
7221 | ## ==================== | |
fffd96b7 RD |
7222 | ## Version 1.5 |
7223 | ## (Reported) bugs fixed: | |
7224 | ## 1. Crash ensues if you attempt to change the mask of a read-only | |
7225 | ## MaskedComboBox after initial construction. | |
7226 | ## 2. Changed strategy of defining Get/Set property functions so that | |
7227 | ## these are now generated dynamically at runtime, rather than as | |
7228 | ## part of the class definition. (This makes it possible to have | |
7229 | ## more general base classes that have many more options for configuration | |
7230 | ## without requiring that derivations support the same options.) | |
7231 | ## 3. Fixed IsModified for _Paste() and _OnErase(). | |
7232 | ## | |
7233 | ## Enhancements: | |
7234 | ## 1. Fixed "attribute function inheritance," since base control is more | |
7235 | ## generic than subsequent derivations, not all property functions of a | |
7236 | ## generic control should be exposed in those derivations. New strategy | |
7237 | ## uses base control classes (eg. BaseMaskedTextCtrl) that should be | |
7238 | ## used to derive new class types, and mixed with their own mixins to | |
7239 | ## only expose those attributes from the generic masked controls that | |
7240 | ## make sense for the derivation. (This makes Boa happier.) | |
7241 | ## 2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less | |
7242 | ## "parochial." | |
7243 | ## | |
d14a1e28 RD |
7244 | ## Version 1.4 |
7245 | ## (Reported) bugs fixed: | |
7246 | ## 1. Right-click menu allowed "cut" operation that destroyed mask | |
7247 | ## (was implemented by base control) | |
d4b73b1b | 7248 | ## 2. MaskedComboBox didn't allow .Append() of mixed-case values; all |
d14a1e28 | 7249 | ## got converted to lower case. |
d4b73b1b | 7250 | ## 3. MaskedComboBox selection didn't deal with spaces in values |
d14a1e28 RD |
7251 | ## properly when autocompleting, and didn't have a concept of "next" |
7252 | ## match for handling choice list duplicates. | |
d4b73b1b | 7253 | ## 4. Size of MaskedComboBox was always default. |
d14a1e28 RD |
7254 | ## 5. Email address regexp allowed some "non-standard" things, and wasn't |
7255 | ## general enough. | |
d4b73b1b | 7256 | ## 6. Couldn't easily reset MaskedComboBox contents programmatically. |
d14a1e28 RD |
7257 | ## 7. Couldn't set emptyInvalid during construction. |
7258 | ## 8. Under some versions of wxPython, readonly comboboxes can apparently | |
7259 | ## return a GetInsertionPoint() result (655535), causing masked control | |
7260 | ## to fail. | |
7261 | ## 9. Specifying an empty mask caused the controls to traceback. | |
7262 | ## 10. Can't specify float ranges for validRange. | |
7263 | ## 11. '.' from within a the static portion of a restricted IP address | |
7264 | ## destroyed the mask from that point rightward; tab when cursor is | |
7265 | ## before 1st field takes cursor past that field. | |
7266 | ## | |
7267 | ## Enhancements: | |
7268 | ## 12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.) | |
7269 | ## 13. Added auto-select option on char input for masked controls with | |
7270 | ## choice lists. | |
7271 | ## 14. Added '>' formatcode, allowing insert within a given or each field | |
7272 | ## as appropriate, rather than requiring "overwrite". This makes single | |
7273 | ## field controls that just have validation rules (eg. EMAIL) much more | |
7274 | ## friendly. The same flag controls left shift when deleting vs just | |
7275 | ## blanking the value, and for right-insert fields, allows right-insert | |
7276 | ## at any non-blank (non-sign) position in the field. | |
7277 | ## 15. Added option to use to indicate negative values for numeric controls. | |
7278 | ## 16. Improved OnFocus handling of numeric controls. | |
7279 | ## 17. Enhanced Home/End processing to allow operation on a field level, | |
7280 | ## using ctrl key. | |
7281 | ## 18. Added individual Get/Set functions for control parameters, for | |
7282 | ## simplified integration with Boa Constructor. | |
7283 | ## 19. Standardized "Colour" parameter names to match wxPython, with | |
7284 | ## non-british spellings still supported for backward-compatibility. | |
7285 | ## 20. Added '&' mask specification character for punctuation only (no letters | |
7286 | ## or digits). | |
7287 | ## 21. Added (in a separate file) wxMaskedCtrl() factory function to provide | |
7288 | ## unified interface to the masked edit subclasses. | |
7289 | ## | |
7290 | ## | |
7291 | ## Version 1.3 | |
7292 | ## 1. Made it possible to configure grouping, decimal and shift-decimal characters, | |
7293 | ## to make controls more usable internationally. | |
7294 | ## 2. Added code to smart "adjust" value strings presented to .SetValue() | |
7295 | ## for right-aligned numeric format controls if they are shorter than | |
7296 | ## than the control width, prepending the missing portion, prepending control | |
7297 | ## template left substring for the missing characters, so that setting | |
7298 | ## numeric values is easier. | |
7299 | ## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved | |
7300 | ## for b-c), as this makes more sense. | |
7301 | ## | |
7302 | ## Version 1.2 | |
7303 | ## 1. Fixed .SetValue() to replace the current value, rather than the current | |
7304 | ## selection. Also changed it to generate ValueError if presented with | |
7305 | ## either a value which doesn't follow the format or won't fit. Also made | |
7306 | ## set value adjust numeric and date controls as if user entered the value. | |
7307 | ## Expanded doc explaining how SetValue() works. | |
7308 | ## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to | |
7309 | ## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats. | |
7310 | ## 3. Made all date autoformats automatically pick implied "datestyle". | |
7311 | ## 4. Added IsModified override, since base wxTextCtrl never reports modified if | |
7312 | ## .SetValue used to change the value, which is what the masked edit controls | |
7313 | ## use internally. | |
7314 | ## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when | |
7315 | ## using tab to "leave field" and auto-adjust. | |
7316 | ## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes, | |
7317 | ## and bug in ._Paste() that didn't account for signs in signed masks either. | |
7318 | ## 7. Fixed issues with _adjustPos for right-insert fields causing improper | |
7319 | ## selection/replacement of values | |
7320 | ## 8. Fixed _OnHome handler to properly handle extending current selection to | |
7321 | ## beginning of control. | |
7322 | ## 9. Exposed all (valid) autoformats to demo, binding descriptions to | |
7323 | ## autoformats. | |
7324 | ## 10. Fixed a couple of bugs in email regexp. | |
7325 | ## 11. Made maskchardict an instance var, to make mask chars to be more | |
7326 | ## amenable to international use. | |
7327 | ## 12. Clarified meaning of '-' formatcode in doc. | |
7328 | ## 13. Fixed a couple of coding bugs being flagged by Python2.1. | |
7329 | ## 14. Fixed several issues with sign positioning, erasure and validity | |
7330 | ## checking for "numeric" masked controls. | |
d4b73b1b | 7331 | ## 15. Added validation to IpAddrCtrl.SetValue(). |
d14a1e28 RD |
7332 | ## |
7333 | ## Version 1.1 | |
7334 | ## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default) | |
7335 | ## vs. literal font facename, and use wxTELETYPE as the font family | |
7336 | ## if so specified. | |
7337 | ## 2. Switched to use of dbg module vs. locally defined version. | |
7338 | ## 3. Revamped entire control structure to use Field classes to hold constraint | |
7339 | ## and formatting data, to make code more hierarchical, allow for more | |
7340 | ## sophisticated masked edit construction. | |
7341 | ## 4. Better strategy for managing options, and better validation on keywords. | |
7342 | ## 5. Added 'V' format code, which requires that in order for a character | |
7343 | ## to be accepted, it must result in a string that passes the validRegex. | |
7344 | ## 6. Added 'S' format code which means "select entire field when navigating | |
7345 | ## to new field." | |
7346 | ## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment) | |
7347 | ## 8. Added '<' format code to allow fields to require explicit cursor movement | |
7348 | ## to leave field. | |
7349 | ## 9. Added validFunc option to other validation mechanisms, that allows derived | |
7350 | ## classes to add dynamic validation constraints to the control. | |
7351 | ## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also | |
7352 | ## fixed failure to obey case conversion codes when pasting. | |
7353 | ## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere... | |
7354 | ## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.' | |
d4b73b1b | 7355 | ## 13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal, |
d14a1e28 RD |
7356 | ## individual field validation to prevent field values > 255, and require explicit |
7357 | ## tab/. to change fields. | |
7358 | ## 14. Added handler for left double-click to select field under cursor. | |
7359 | ## 15. Fixed handling for "Read-only" styles. | |
7360 | ## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor | |
7361 | ## attribute, for more consistent and controllable coloring. | |
7362 | ## 17. Added retainFieldValidation parameter, allowing top-level constraints | |
7363 | ## such as "validRequired" to be set independently of field-level equivalent. | |
d4b73b1b | 7364 | ## (needed in TimeCtrl for bounds constraints.) |
d14a1e28 RD |
7365 | ## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed |
7366 | ## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue, | |
7367 | ## etc. | |
7368 | ## 19. Fixed maskchar setting for upper/lowercase, to work in all locales. | |
7369 | ## | |
7370 | ## | |
7371 | ## Version 1.0 | |
7372 | ## 1. Decimal point behavior restored for decimal and integer type controls: | |
7373 | ## decimal point now trucates the portion > 0. | |
7374 | ## 2. Return key now works like the tab character and moves to the next field, | |
7375 | ## provided no default button is set for the form panel on which the control | |
7376 | ## resides. | |
7377 | ## 3. Support added in _FindField() for subclasses controls (like timecontrol) | |
7378 | ## to determine where the current insertion point is within the mask (i.e. | |
7379 | ## which sub-'field'). See method documentation for more info and examples. | |
7380 | ## 4. Added Field class and support for all constraints to be field-specific | |
7381 | ## in addition to being globally settable for the control. | |
7382 | ## Choices for each field are validated for length and pastability into | |
7383 | ## the field in question, raising ValueError if not appropriate for the control. | |
7384 | ## Also added selective additional validation based on individual field constraints. | |
7385 | ## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all | |
7386 | ## auto-complete fields with choice lists, supplying the 1st entry in | |
7387 | ## the choice list if the field is empty, and cycling through the list in | |
7388 | ## the appropriate direction if already a match. WXK_DOWN will also auto- | |
7389 | ## complete if the field is partially completed and a match can be made. | |
7390 | ## SHIFT-WXK_UP/DOWN will also take you to the next field after any | |
7391 | ## auto-completion performed. | |
7392 | ## 5. Added autoCompleteKeycodes=[] parameters for allowing further | |
7393 | ## customization of the control. Any keycode supplied as a member | |
7394 | ## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If | |
7395 | ## requireFieldChoice is set, then a valid value from each non-empty | |
7396 | ## choice list will be required for the value of the control to validate. | |
7397 | ## 6. Fixed "auto-sizing" to be relative to the font actually used, rather | |
7398 | ## than making assumptions about character width. | |
7399 | ## 7. Fixed GetMaskParameter(), which was non-functional in previous version. | |
7400 | ## 8. Fixed exceptions raised to provide info on which control had the error. | |
d4b73b1b RD |
7401 | ## 9. Fixed bug in choice management of MaskedComboBox. |
7402 | ## 10. Fixed bug in IpAddrCtrl causing traceback if field value was of | |
7403 | ## the form '# #'. Modified control code for IpAddrCtrl so that '.' | |
d14a1e28 RD |
7404 | ## in the middle of a field clips the rest of that field, similar to |
7405 | ## decimal and integer controls. | |
7406 | ## | |
7407 | ## | |
7408 | ## Version 0.0.7 | |
7409 | ## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive. | |
7410 | ## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333). | |
7411 | ## 3. New support for selecting text within the control.(thanks Will Sadkin!) | |
7412 | ## Shift-End and Shift-Home now select text as you would expect | |
7413 | ## Control-Shift-End selects to the end of the mask string, even if value not entered. | |
7414 | ## Control-A selects all *entered* text, Shift-Control-A selects everything in the control. | |
7415 | ## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed- | |
7416 | ## for some reason I couldn't find the original email but thanks!!!) | |
7417 | ## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome, | |
7418 | ## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc. | |
7419 | ## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!). | |
7420 | ## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...) | |
7421 | ## 7. New mechanism for replacing default behavior for any given key, using | |
7422 | ## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available | |
7423 | ## for easier subclassing of the control. | |
7424 | ## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs | |
7425 | ## with insertion point/selection modification. Changed Ctrl-X to use standard "cut" | |
7426 | ## semantics, erasing the selection, rather than erasing the entire control. | |
7427 | ## 9. Added option for an "default value" (ie. the template) for use when a single fillChar | |
7428 | ## is not desired in every position. Added IsDefault() function to mean "does the value | |
7429 | ## equal the template?" and modified .IsEmpty() to mean "do all of the editable | |
7430 | ## positions in the template == the fillChar?" | |
d4b73b1b | 7431 | ## 10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox, |
d14a1e28 | 7432 | ## now included. |
d4b73b1b | 7433 | ## 11. MaskedComboBox now adds the capability to validate from list of valid values. |
d14a1e28 RD |
7434 | ## Example: City validates against list of cities, or zip vs zip code list. |
7435 | ## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being | |
7436 | ## passed to the next handler in the event chain, causing updates to the | |
7437 | ## control to be invisible to the parent code. | |
d4b73b1b | 7438 | ## 13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within |
d14a1e28 RD |
7439 | ## the control, that auto-reformats as you move between cells. |
7440 | ## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'. | |
7441 | ## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14} | |
7442 | ## 16. Fixed major bugs in date validation, due to the fact that | |
7443 | ## wxDateTime.ParseDate is too liberal, and will accept any form that | |
7444 | ## makes any kind of sense, regardless of the datestyle you specified | |
7445 | ## for the control. Unfortunately, the strategy used to fix it only | |
7446 | ## works for versions of wxPython post 2.3.3.1, as a C++ assert box | |
7447 | ## seems to show up on an invalid date otherwise, instead of a catchable | |
7448 | ## exception. | |
7449 | ## 17. Enhanced date adjustment to automatically adjust heuristic based on | |
7450 | ## current year, making last century/this century determination on | |
7451 | ## 2-digit year based on distance between today's year and value; | |
7452 | ## if > 50 year separation, assume last century (and don't assume last | |
7453 | ## century is 20th.) | |
7454 | ## 18. Added autoformats and support for including HHMMSS as well as HHMM for | |
7455 | ## date times, and added similar time, and militaray time autoformats. | |
7456 | ## 19. Enhanced tabbing logic so that tab takes you to the next field if the | |
7457 | ## control is a multi-field control. | |
7458 | ## 20. Added stub method called whenever the control "changes fields", that | |
d4b73b1b | 7459 | ## can be overridden by subclasses (eg. IpAddrCtrl.) |
d14a1e28 RD |
7460 | ## 21. Changed a lot of code to be more functionally-oriented so side-effects |
7461 | ## aren't as problematic when maintaining code and/or adding features. | |
7462 | ## Eg: IsValid() now does not have side-effects; it merely reflects the | |
7463 | ## validity of the value of the control; to determine validity AND recolor | |
7464 | ## the control, _CheckValid() should be used with a value argument of None. | |
7465 | ## Similarly, made most reformatting function take an optional candidate value | |
7466 | ## rather than just using the current value of the control, and only | |
7467 | ## have them change the value of the control if a candidate is not specified. | |
7468 | ## In this way, you can do validation *before* changing the control. | |
7469 | ## 22. Changed validRequired to mean "disallow chars that result in invalid | |
7470 | ## value." (Old meaning now represented by emptyInvalid.) (This was | |
7471 | ## possible once I'd made the changes in (19) above.) | |
7472 | ## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they | |
7473 | ## can be set/modified/retrieved after construction. Removed individual | |
7474 | ## parameter setting functions, in favor of this mechanism, so that | |
7475 | ## all adjustment of the control based on changing parameter values can | |
7476 | ## be handled in one place with unified mechanism. | |
7477 | ## 24. Did a *lot* of testing and fixing re: numeric values. Added ability | |
7478 | ## to type "grouping char" (ie. ',') and validate as appropriate. | |
7479 | ## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9. | |
7480 | ## 26. Fixed assumption about "decimal or integer" masks so that they're only | |
7481 | ## made iff there's no validRegex associated with the field. (This | |
7482 | ## is so things like zipcodes which look like integers can have more | |
7483 | ## restrictive validation (ie. must be 5 digits.) | |
7484 | ## 27. Added a ton more doc strings to explain use and derivation requirements | |
7485 | ## and did regularization of the naming conventions. | |
7486 | ## 28. Fixed a range bug in _adjustKey preventing z from being handled properly. | |
7487 | ## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to | |
7488 | ## reformat the value and move the next field as appropriate. (shift-'.', | |
7489 | ## ie. '>' moves to the previous field. | |
7490 | ||
7491 | ## Version 0.0.6 | |
7492 | ## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending | |
7493 | ## in '0'. | |
7494 | ## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the | |
7495 | ## year position, the control will expand the value to four digits, using numerals below | |
7496 | ## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn). | |
7497 | ## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM} | |
7498 | ## 3. revalid parameter renamed validRegex to conform to standard for all validation | |
7499 | ## parameters (see 2 new ones below). | |
7500 | ## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types. | |
7501 | ## Allows the developer to specify a valid low/high range of values. | |
7502 | ## 5. New optional init parameter = validList. Used for character types. Allows developer | |
7503 | ## to send a list of values to the control to be used for specific validation. | |
7504 | ## See the Last Name Only example - it is list restricted to Smith/Jones/Williams. | |
7505 | ## 6. Date type fields now use wxDateTime's parser to validate the date and time. | |
7506 | ## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing | |
7507 | ## me toward this solution! | |
7508 | ## 7. Date fields now automatically expand 2-digit years when it can. For example, | |
7509 | ## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year | |
7510 | ## date is entered it will be expanded in any case when the user tabs out of the | |
7511 | ## field. | |
7512 | ## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor, | |
7513 | ## SetSignedForeColor allow accessto override default class coloring behavior. | |
7514 | ## 9. Documentation updated and improved. | |
7515 | ## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better. | |
7516 | ## Two new options (checkboxes) - test highlight empty and disallow empty. | |
7517 | ## 11. Home and End now work more intuitively, moving to the first and last user-entry | |
7518 | ## value, respectively. | |
7519 | ## 12. New class function: SetRequired(bool). Sets the control's entry required flag | |
7520 | ## (i.e. disallow empty values if True). | |
7521 | ## | |
7522 | ## Version 0.0.5 | |
fffd96b7 | 7523 | ## 1. get_plainValue method renamed to GetPlainValue following the wxWidgets |
d14a1e28 RD |
7524 | ## StudlyCaps(tm) standard (thanks Paul Moore). ;) |
7525 | ## 2. New format code 'F' causes the control to auto-fit (auto-size) itself | |
7526 | ## based on the length of the mask template. | |
7527 | ## 3. Class now supports "autoformat" codes. These can be passed to the class | |
7528 | ## on instantiation using the parameter autoformat="code". If the code is in | |
7529 | ## the dictionary, it will self set the mask, formatting, and validation string. | |
7530 | ## I have included a number of samples, but I am hoping that someone out there | |
7531 | ## can help me to define a whole bunch more. | |
7532 | ## 4. I have added a second page to the demo (as well as a second demo class, test2) | |
7533 | ## to showcase how autoformats work. The way they self-format and self-size is, | |
7534 | ## I must say, pretty cool. | |
7535 | ## 5. Comments added and some internal cosmetic revisions re: matching the code | |
7536 | ## standards for class submission. | |
7537 | ## 6. Regex validation is now done in real time - field turns yellow immediately | |
7538 | ## and stays yellow until the entered value is valid | |
7539 | ## 7. Cursor now skips over template characters in a more intuitive way (before the | |
7540 | ## next keypress). | |
7541 | ## 8. Change, Keypress and LostFocus methods added for convenience of subclasses. | |
7542 | ## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR, | |
7543 | ## and EVT_KILL_FOCUS, respectively. | |
7544 | ## 9. Decimal and numeric handlers have been rewritten and now work more intuitively. | |
7545 | ## | |
7546 | ## Version 0.0.4 | |
7547 | ## 1. New .IsEmpty() method returns True if the control's value is equal to the | |
7548 | ## blank template string | |
7549 | ## 2. Control now supports a new init parameter: revalid. Pass a regular expression | |
7550 | ## that the value will have to match when the control loses focus. If invalid, | |
7551 | ## the control's BackgroundColor will turn yellow, and an internal flag is set (see next). | |
7552 | ## 3. Demo now shows revalid functionality. Try entering a partial value, such as a | |
7553 | ## partial social security number. | |
7554 | ## 4. New .IsValid() value returns True if the control is empty, or if the value matches | |
7555 | ## the revalid expression. If not, .IsValid() returns False. | |
7556 | ## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never | |
7557 | ## presses the decimal point. | |
7558 | ## 6. Cursor now goes to the beginning of the field if the user clicks in an | |
7559 | ## "empty" field intead of leaving the insertion point in the middle of the | |
7560 | ## field. | |
7561 | ## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9. | |
7562 | ## 8. New formatcodes init parameter replaces other init params and adds functions. | |
7563 | ## String passed to control on init controls: | |
7564 | ## _ Allow spaces | |
7565 | ## ! Force upper | |
7566 | ## ^ Force lower | |
7567 | ## R Show negative #s in red | |
7568 | ## , Group digits | |
7569 | ## - Signed numerals | |
7570 | ## 0 Numeric fields get leading zeros | |
7571 | ## 9. Ctrl-X in any field clears the current value. | |
7572 | ## 10. Code refactored and made more modular (esp in OnChar method). Should be more | |
7573 | ## easy to read and understand. | |
7574 | ## 11. Demo enhanced. | |
7575 | ## 12. Now has _doc_. | |
7576 | ## | |
7577 | ## Version 0.0.3 | |
7578 | ## 1. GetPlainValue() now returns the value without the template characters; | |
7579 | ## so, for example, a social security number (123-33-1212) would return as | |
7580 | ## 123331212; also removes white spaces from numeric/decimal values, so | |
7581 | ## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value. | |
7582 | ## 2. Press '.' in an integer style masked control and truncate any trailing digits. | |
7583 | ## 3. Code moderately refactored. Internal names improved for clarity. Additional | |
7584 | ## internal documentation. | |
7585 | ## 4. Home and End keys now supported to move cursor to beginning or end of field. | |
7586 | ## 5. Un-signed integers and decimals now supported. | |
7587 | ## 6. Cosmetic improvements to the demo. | |
d4b73b1b | 7588 | ## 7. Class renamed to MaskedTextCtrl. |
d14a1e28 RD |
7589 | ## 8. Can now specify include characters that will override the basic |
7590 | ## controls: for example, includeChars = "@." for email addresses | |
7591 | ## 9. Added mask character 'C' -> allow any upper or lowercase character | |
7592 | ## 10. .SetSignColor(str:color) sets the foreground color for negative values | |
7593 | ## in signed controls (defaults to red) | |
7594 | ## 11. Overview documentation written. | |
7595 | ## | |
7596 | ## Version 0.0.2 | |
7597 | ## 1. Tab now works properly when pressed in last position | |
7598 | ## 2. Decimal types now work (e.g. #####.##) | |
7599 | ## 3. Signed decimal or numeric values supported (i.e. negative numbers) | |
7600 | ## 4. Negative decimal or numeric values now can show in red. | |
7601 | ## 5. Can now specify an "exclude list" with the excludeChars parameter. | |
7602 | ## See date/time formatted example - you can only enter A or P in the | |
7603 | ## character mask space (i.e. AM/PM). | |
7604 | ## 6. Backspace now works properly, including clearing data from a selected | |
7605 | ## region but leaving template characters intact. Also delete key. | |
7606 | ## 7. Left/right arrows now work properly. | |
7607 | ## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3 | |
7608 | ## |