]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/maskednumctrl.py
Merged the wxPy_newswig branch into the HEAD branch (main trunk)
[wxWidgets.git] / wxPython / wx / lib / maskednumctrl.py
1 #----------------------------------------------------------------------------
2 # Name: wxPython.lib.maskednumctrl.py
3 # Author: Will Sadkin
4 # Created: 09/06/2003
5 # Copyright: (c) 2003 by Will Sadkin
6 # RCS-ID: $Id$
7 # License: wxWindows license
8 #----------------------------------------------------------------------------
9 # NOTE:
10 # This was written to provide a numeric edit control for wxPython that
11 # does things like right-insert (like a calculator), and does grouping, etc.
12 # (ie. the features of wxMaskedTextCtrl), but allows Get/Set of numeric
13 # values, rather than text.
14 #
15 # wxMaskedNumCtrl permits integer, and floating point values to be set
16 # retrieved or set via .GetValue() and .SetValue() (type chosen based on
17 # fraction width, and provides an EVT_MASKEDNUM() event function for trapping
18 # changes to the control.
19 #
20 # It supports negative numbers as well as the naturals, and has the option
21 # of not permitting leading zeros or an empty control; if an empty value is
22 # not allowed, attempting to delete the contents of the control will result
23 # in a (selected) value of zero, thus preserving a legitimate numeric value.
24 # Similarly, replacing the contents of the control with '-' will result in
25 # a selected (absolute) value of -1.
26 #
27 # wxMaskedNumCtrl also supports range limits, with the option of either
28 # enforcing them or simply coloring the text of the control if the limits
29 # are exceeded.
30 #
31 # wxMaskedNumCtrl is intended to support fixed-point numeric entry, and
32 # is derived from wxMaskedTextCtrl. As such, it supports a limited range
33 # of values to comply with a fixed-width entry mask.
34 """<html><body>
35 <P>
36 <B>wxMaskedNumCtrl:</B>
37 <UL>
38 <LI>allows you to get and set integer or floating point numbers as value,</LI>
39 <LI>provides bounds support and optional value limiting,</LI>
40 <LI>has the right-insert input style that wxMaskedTextCtrl supports,</LI>
41 <LI>provides optional automatic grouping, sign control and format, grouping and decimal
42 character selection, etc. etc.</LI>
43 </UL>
44 <P>
45 Being derived from wxMaskedTextCtrl, the control only allows
46 fixed-point notation. That is, it has a fixed (though reconfigurable)
47 maximum width for the integer portion and optional fixed width
48 fractional portion.
49 <P>
50 Here's the API:
51 <DL><PRE>
52 <B>wxMaskedNumCtrl</B>(
53 parent, id = -1,
54 <B>value</B> = 0,
55 pos = wxDefaultPosition,
56 size = wxDefaultSize,
57 style = 0,
58 validator = wxDefaultValidator,
59 name = "maskednumber",
60 <B>integerWidth</B> = 10,
61 <B>fractionWidth</B> = 0,
62 <B>allowNone</B> = False,
63 <B>allowNegative</B> = True,
64 <B>useParensForNegatives</B> = False,
65 <B>groupDigits</B> = False,
66 <B>groupChar</B> = ',',
67 <B>decimalChar</B> = '.',
68 <B>min</B> = None,
69 <B>max</B> = None,
70 <B>limited</B> = False,
71 <B>selectOnEntry</b> = True,
72 <B>foregroundColour</B> = "Black",
73 <B>signedForegroundColour</B> = "Red",
74 <B>emptyBackgroundColour</B> = "White",
75 <B>validBackgroundColour</B> = "White",
76 <B>invalidBackgroundColour</B> = "Yellow",
77 )
78 </PRE>
79 <UL>
80 <DT><B>value</B>
81 <DD>If no initial value is set, the default will be zero, or
82 the minimum value, if specified. If an illegal string is specified,
83 a ValueError will result. (You can always later set the initial
84 value with SetValue() after instantiation of the control.)
85 <BR>
86 <DL><B>integerWidth</B>
87 <DD>Indicates how many places to the right of any decimal point
88 should be allowed in the control. This will, perforce, limit
89 the size of the values that can be entered. This number need
90 not include space for grouping characters or the sign, if either
91 of these options are enabled, as the resulting underlying
92 mask is automatically by the control. The default of 10
93 will allow any 32 bit integer value. The minimum value
94 for integerWidth is 1.
95 <BR>
96 <DL><B>fractionWidth</B>
97 <DD>Indicates how many decimal places to show for numeric value.
98 If default (0), then the control will display and return only
99 integer or long values.
100 <BR>
101 <DT><B>allowNone</B>
102 <DD>Boolean indicating whether or not the control is allowed to be
103 empty, representing a value of None for the control.
104 <BR>
105 <DT><B>allowNegative</B>
106 <DD>Boolean indicating whether or not control is allowed to hold
107 negative numbers.
108 <BR>
109 <DT><B>useParensForNegatives</B>
110 <DD>If true, this will cause negative numbers to be displayed with ()s
111 rather than -, (although '-' will still trigger a negative number.)
112 <BR>
113 <DT><B>groupDigits</B>
114 <DD>Indicates whether or not grouping characters should be allowed and/or
115 inserted when leaving the control or the decimal character is entered.
116 <BR>
117 <DT><B>groupChar</B>
118 <DD>What grouping character will be used if allowed. (By default ',')
119 <BR>
120 <DT><B>decimalChar</B>
121 <DD>If fractionWidth is > 0, what character will be used to represent
122 the decimal point. (By default '.')
123 <BR>
124 <DL><B>min</B>
125 <DD>The minimum value that the control should allow. This can be also be
126 adjusted with SetMin(). If the control is not limited, any value
127 below this bound will result in a background colored with the current
128 invalidBackgroundColour. If the min specified will not fit into the
129 control, the min setting will be ignored.
130 <BR>
131 <DT><B>max</B>
132 <DD>The maximum value that the control should allow. This can be
133 adjusted with SetMax(). If the control is not limited, any value
134 above this bound will result in a background colored with the current
135 invalidBackgroundColour. If the max specified will not fit into the
136 control, the max setting will be ignored.
137 <BR>
138 <DT><B>limited</B>
139 <DD>Boolean indicating whether the control prevents values from
140 exceeding the currently set minimum and maximum values (bounds).
141 If False and bounds are set, out-of-bounds values will
142 result in a background colored with the current invalidBackgroundColour.
143 <BR>
144 <DT><B>selectOnEntry</B>
145 <DD>Boolean indicating whether or not the value in each field of the
146 control should be automatically selected (for replacement) when
147 that field is entered, either by cursor movement or tabbing.
148 This can be desirable when using these controls for rapid data entry.
149 <BR>
150 <DT><B>foregroundColour</B>
151 <DD>Color value used for positive values of the control.
152 <BR>
153 <DT><B>signedForegroundColour</B>
154 <DD>Color value used for negative values of the control.
155 <BR>
156 <DT><B>emptyBackgroundColour</B>
157 <DD>What background color to use when the control is considered
158 "empty." (allow_none must be set to trigger this behavior.)
159 <BR>
160 <DT><B>validBackgroundColour</B>
161 <DD>What background color to use when the control value is
162 considered valid.
163 <BR>
164 <DT><B>invalidBackgroundColour</B>
165 <DD>Color value used for illegal values or values out-of-bounds of the
166 control when the bounds are set but the control is not limited.
167 </UL>
168 <BR>
169 <BR>
170 <DT><B>EVT_MASKEDNUM(win, id, func)</B>
171 <DD>Respond to a wxEVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when
172 the value changes. Notice that this event will always be sent when the
173 control's contents changes - whether this is due to user input or
174 comes from the program itself (for example, if SetValue() is called.)
175 <BR>
176 <BR>
177 <DT><B>SetValue(int|long|float|string)</B>
178 <DD>Sets the value of the control to the value specified, if
179 possible. The resulting actual value of the control may be
180 altered to conform to the format of the control, changed
181 to conform with the bounds set on the control if limited,
182 or colored if not limited but the value is out-of-bounds.
183 A ValueError exception will be raised if an invalid value
184 is specified.
185 <BR>
186 <DT><B>GetValue()</B>
187 <DD>Retrieves the numeric value from the control. The value
188 retrieved will be either be returned as a long if the
189 fractionWidth is 0, or a float otherwise.
190 <BR>
191 <BR>
192 <DT><B>SetParameters(**kwargs)</B>
193 <DD>Allows simultaneous setting of various attributes
194 of the control after construction. Keyword arguments
195 allowed are the same parameters as supported in the constructor.
196 <BR>
197 <BR>
198 <DT><B>SetIntegerWidth(value)</B>
199 <DD>Resets the width of the integer portion of the control. The
200 value must be >= 1, or an AttributeError exception will result.
201 This value should account for any grouping characters that might
202 be inserted (if grouping is enabled), but does not need to account
203 for the sign, as that is handled separately by the control.
204 <DT><B>GetIntegerWidth()</B>
205 <DD>Returns the current width of the integer portion of the control,
206 not including any reserved sign position.
207 <BR>
208 <BR>
209 <DT><B>SetFractionWidth(value)</B>
210 <DD>Resets the width of the fractional portion of the control. The
211 value must be >= 0, or an AttributeError exception will result. If
212 0, the current value of the control will be truncated to an integer
213 value.
214 <DT><B>GetFractionWidth()</B>
215 <DD>Returns the current width of the fractional portion of the control.
216 <BR>
217 <BR>
218 <DT><B>SetMin(min=None)</B>
219 <DD>Resets the minimum value of the control. If a value of <I>None</I>
220 is provided, then the control will have no explicit minimum value.
221 If the value specified is greater than the current maximum value,
222 then the function returns False and the minimum will not change from
223 its current setting. On success, the function returns True.
224 <DT><DD>
225 If successful and the current value is lower than the new lower
226 bound, if the control is limited, the value will be automatically
227 adjusted to the new minimum value; if not limited, the value in the
228 control will be colored as invalid.
229 <DT><DD>
230 If min > the max value allowed by the width of the control,
231 the function will return False, and the min will not be set.
232 <BR>
233 <DT><B>GetMin()</B>
234 <DD>Gets the current lower bound value for the control.
235 It will return None if no lower bound is currently specified.
236 <BR>
237 <BR>
238 <DT><B>SetMax(max=None)</B>
239 <DD>Resets the maximum value of the control. If a value of <I>None</I>
240 is provided, then the control will have no explicit maximum value.
241 If the value specified is less than the current minimum value, then
242 the function returns False and the maximum will not change from its
243 current setting. On success, the function returns True.
244 <DT><DD>
245 If successful and the current value is greater than the new upper
246 bound, if the control is limited the value will be automatically
247 adjusted to this maximum value; if not limited, the value in the
248 control will be colored as invalid.
249 <DT><DD>
250 If max > the max value allowed by the width of the control,
251 the function will return False, and the max will not be set.
252 <BR>
253 <DT><B>GetMax()</B>
254 <DD>Gets the current upper bound value for the control.
255 It will return None if no upper bound is currently specified.
256 <BR>
257 <BR>
258 <DT><B>SetBounds(min=None,max=None)</B>
259 <DD>This function is a convenience function for setting the min and max
260 values at the same time. The function only applies the maximum bound
261 if setting the minimum bound is successful, and returns True
262 only if both operations succeed. <B><I>Note:</I></B> leaving out an argument
263 will remove the corresponding bound.
264 <DT><B>GetBounds()</B>
265 <DD>This function returns a two-tuple (min,max), indicating the
266 current bounds of the control. Each value can be None if
267 that bound is not set.
268 <BR>
269 <BR>
270 <DT><B>IsInBounds(value=None)</B>
271 <DD>Returns <I>True</I> if no value is specified and the current value
272 of the control falls within the current bounds. This function can also
273 be called with a value to see if that value would fall within the current
274 bounds of the given control.
275 <BR>
276 <BR>
277 <DT><B>SetLimited(bool)</B>
278 <DD>If called with a value of True, this function will cause the control
279 to limit the value to fall within the bounds currently specified.
280 If the control's value currently exceeds the bounds, it will then
281 be limited accordingly.
282 If called with a value of False, this function will disable value
283 limiting, but coloring of out-of-bounds values will still take
284 place if bounds have been set for the control.
285 <DT><B>GetLimited()</B>
286 <DT><B>IsLimited()</B>
287 <DD>Returns <I>True</I> if the control is currently limiting the
288 value to fall within the current bounds.
289 <BR>
290 <BR>
291 <DT><B>SetAllowNone(bool)</B>
292 <DD>If called with a value of True, this function will cause the control
293 to allow the value to be empty, representing a value of None.
294 If called with a value of False, this function will prevent the value
295 from being None. If the value of the control is currently None,
296 ie. the control is empty, then the value will be changed to that
297 of the lower bound of the control, or 0 if no lower bound is set.
298 <DT><B>GetAllowNone()</B>
299 <DT><B>IsNoneAllowed()</B>
300 <DD>Returns <I>True</I> if the control currently allows its
301 value to be None.
302 <BR>
303 <BR>
304 <DT><B>SetAllowNegative(bool)</B>
305 <DD>If called with a value of True, this function will cause the
306 control to allow the value to be negative (and reserve space for
307 displaying the sign. If called with a value of False, and the
308 value of the control is currently negative, the value of the
309 control will be converted to the absolute value, and then
310 limited appropriately based on the existing bounds of the control
311 (if any).
312 <DT><B>GetAllowNegative()</B>
313 <DT><B>IsNegativeAllowed()</B>
314 <DD>Returns <I>True</I> if the control currently permits values
315 to be negative.
316 <BR>
317 <BR>
318 <DT><B>SetGroupDigits(bool)</B>
319 <DD>If called with a value of True, this will make the control
320 automatically add and manage grouping characters to the presented
321 value in integer portion of the control.
322 <DT><B>GetGroupDigits()</B>
323 <DT><B>IsGroupingAllowed()</B>
324 <DD>Returns <I>True</I> if the control is currently set to group digits.
325 <BR>
326 <BR>
327 <DT><B>SetGroupChar()</B>
328 <DD>Sets the grouping character for the integer portion of the
329 control. (The default grouping character this is ','.
330 <DT><B>GetGroupChar()</B>
331 <DD>Returns the current grouping character for the control.
332 <BR>
333 <BR>
334 <DT><B>SetSelectOnEntry()</B>
335 <DD>If called with a value of <I>True</I>, this will make the control
336 automatically select the contents of each field as it is entered
337 within the control. (The default is True.)
338 <DT><B>GetSelectOnEntry()</B>
339 <DD>Returns <I>True</I> if the control currently auto selects
340 the field values on entry.
341 <BR>
342 <BR>
343 </DL>
344 </body></html>
345 """
346
347 from wxPython.wx import *
348 import types, string, copy
349 from sys import maxint
350 MAXINT = maxint # (constants should be in upper case)
351 MININT = -maxint-1
352
353 from wxPython.tools.dbg import Logger
354 from wxPython.lib.maskededit import wxMaskedEditMixin, wxMaskedTextCtrl, Field
355
356 dbg = Logger()
357 dbg(enable=0)
358
359 #----------------------------------------------------------------------------
360
361 wxEVT_COMMAND_MASKED_NUMBER_UPDATED = wxNewEventType()
362
363
364 def EVT_MASKEDNUM(win, id, func):
365 """Used to trap events indicating that the current
366 integer value of the control has been changed."""
367 win.Connect(id, -1, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, func)
368
369
370 class wxMaskedNumNumberUpdatedEvent(wxPyCommandEvent):
371 def __init__(self, id, value = 0, object=None):
372 wxPyCommandEvent.__init__(self, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, id)
373
374 self.__value = value
375 self.SetEventObject(object)
376
377 def GetValue(self):
378 """Retrieve the value of the control at the time
379 this event was generated."""
380 return self.__value
381
382
383 #----------------------------------------------------------------------------
384
385 class wxMaskedNumCtrl(wxMaskedTextCtrl):
386
387 valid_ctrl_params = {
388 'integerWidth': 10, # by default allow all 32-bit integers
389 'fractionWidth': 0, # by default, use integers
390 'decimalChar': '.', # by default, use '.' for decimal point
391 'allowNegative': True, # by default, allow negative numbers
392 'useParensForNegatives': False, # by default, use '-' to indicate negatives
393 'groupDigits': True, # by default, don't insert grouping
394 'groupChar': ',', # by default, use ',' for grouping
395 'min': None, # by default, no bounds set
396 'max': None,
397 'limited': False, # by default, no limiting even if bounds set
398 'allowNone': False, # by default, don't allow empty value
399 'selectOnEntry': True, # by default, select the value of each field on entry
400 'foregroundColour': "Black",
401 'signedForegroundColour': "Red",
402 'emptyBackgroundColour': "White",
403 'validBackgroundColour': "White",
404 'invalidBackgroundColour': "Yellow",
405 'useFixedWidthFont': True, # by default, use a fixed-width font
406 }
407
408
409 def __init__ (
410 self, parent, id=-1, value = 0,
411 pos = wxDefaultPosition, size = wxDefaultSize,
412 style = wxTE_PROCESS_TAB, validator = wxDefaultValidator,
413 name = "maskednum",
414 **kwargs ):
415
416 dbg('wxMaskedNumCtrl::__init__', indent=1)
417
418 # Set defaults for control:
419 dbg('setting defaults:')
420 for key, param_value in wxMaskedNumCtrl.valid_ctrl_params.items():
421 # This is done this way to make setattr behave consistently with
422 # "private attribute" name mangling
423 setattr(self, '_' + key, copy.copy(param_value))
424
425 # Assign defaults for all attributes:
426 init_args = copy.deepcopy(wxMaskedNumCtrl.valid_ctrl_params)
427 dbg('kwargs:', kwargs)
428 for key, param_value in kwargs.items():
429 key = key.replace('Color', 'Colour')
430 if key not in wxMaskedNumCtrl.valid_ctrl_params.keys():
431 raise AttributeError('invalid keyword argument "%s"' % key)
432 else:
433 init_args[key] = param_value
434 dbg('init_args:', indent=1)
435 for key, param_value in init_args.items():
436 dbg('%s:' % key, param_value)
437 dbg(indent=0)
438
439 # Process initial fields for the control, as part of construction:
440 if type(init_args['integerWidth']) != types.IntType:
441 raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth']))
442 elif init_args['integerWidth'] < 1:
443 raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth']))
444
445 fields = {}
446
447 if init_args.has_key('fractionWidth'):
448 if type(init_args['fractionWidth']) != types.IntType:
449 raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth))
450 elif init_args['fractionWidth'] < 0:
451 raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth']))
452 self._fractionWidth = init_args['fractionWidth']
453
454 if self._fractionWidth:
455 fracmask = '.' + '#{%d}' % self._fractionWidth
456 dbg('fracmask:', fracmask)
457 fields[1] = Field(defaultValue='0'*self._fractionWidth)
458 else:
459 fracmask = ''
460
461 self._integerWidth = init_args['integerWidth']
462 if init_args['groupDigits']:
463 self._groupSpace = (self._integerWidth - 1) / 3
464 else:
465 self._groupSpace = 0
466 intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
467 if self._fractionWidth:
468 emptyInvalid = False
469 else:
470 emptyInvalid = True
471 fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
472 dbg('intmask:', intmask)
473
474 # don't bother to reprocess these arguments:
475 del init_args['integerWidth']
476 del init_args['fractionWidth']
477
478
479 mask = intmask+fracmask
480
481 # initial value of state vars
482 self._oldvalue = 0
483 self._integerEnd = 0
484 self._typedSign = False
485
486 # Construct the base control:
487 wxMaskedTextCtrl.__init__(
488 self, parent, id, '',
489 pos, size, style, validator, name,
490 mask = mask,
491 formatcodes = 'FR<',
492 fields = fields,
493 validFunc=self.IsInBounds,
494 setupEventHandling = False)
495
496 EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
497 EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
498 EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick
499 EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu
500 EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
501 EVT_CHAR( self, self._OnChar ) ## handle each keypress
502 EVT_TEXT( self, self.GetId(), self.OnTextChange ) ## color control appropriately & keep
503 ## track of previous value for undo
504
505 # Establish any additional parameters, with appropriate error checking
506 self.SetParameters(**init_args)
507
508 # Set the value requested (if possible)
509 ## wxCallAfter(self.SetValue, value)
510 self.SetValue(value)
511
512 # Ensure proper coloring:
513 self.Refresh()
514 dbg('finished wxMaskedNumCtrl::__init__', indent=0)
515
516
517 def SetParameters(self, **kwargs):
518 """
519 This routine is used to initialize and reconfigure the control:
520 """
521 dbg('wxMaskedNumCtrl::SetParameters', indent=1)
522 maskededit_kwargs = {}
523 reset_fraction_width = False
524
525
526 if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth)
527 or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth)
528 or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits) ):
529
530 fields = {}
531
532 if kwargs.has_key('fractionWidth'):
533 if type(kwargs['fractionWidth']) != types.IntType:
534 raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth']))
535 elif kwargs['fractionWidth'] < 0:
536 raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth']))
537 else:
538 if self._fractionWidth != kwargs['fractionWidth']:
539 self._fractionWidth = kwargs['fractionWidth']
540
541 if self._fractionWidth:
542 fracmask = '.' + '#{%d}' % self._fractionWidth
543 fields[1] = Field(defaultValue='0'*self._fractionWidth)
544 emptyInvalid = False
545 else:
546 emptyInvalid = True
547 fracmask = ''
548 dbg('fracmask:', fracmask)
549
550 if kwargs.has_key('integerWidth'):
551 if type(kwargs['integerWidth']) != types.IntType:
552 dbg(indent=0)
553 raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth']))
554 elif kwargs['integerWidth'] < 0:
555 dbg(indent=0)
556 raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth']))
557 else:
558 self._integerWidth = kwargs['integerWidth']
559
560 if kwargs.has_key('groupDigits'):
561 self._groupDigits = kwargs['groupDigits']
562
563 if self._groupDigits:
564 self._groupSpace = (self._integerWidth - 1) / 3
565 else:
566 self._groupSpace = 0
567
568 intmask = '#{%d}' % (self._integerWidth + self._groupSpace)
569 dbg('intmask:', intmask)
570 fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid)
571 maskededit_kwargs['fields'] = fields
572
573 # don't bother to reprocess these arguments:
574 if kwargs.has_key('integerWidth'):
575 del kwargs['integerWidth']
576 if kwargs.has_key('fractionWidth'):
577 del kwargs['fractionWidth']
578
579 maskededit_kwargs['mask'] = intmask+fracmask
580
581 if kwargs.has_key('groupChar'):
582 old_groupchar = self._groupChar # save so we can reformat properly
583 dbg("old_groupchar: '%s'" % old_groupchar)
584 maskededit_kwargs['groupChar'] = kwargs['groupChar']
585 if kwargs.has_key('decimalChar'):
586 old_decimalchar = self._decimalChar
587 dbg("old_decimalchar: '%s'" % old_decimalchar)
588 maskededit_kwargs['decimalChar'] = kwargs['decimalChar']
589
590 # for all other parameters, assign keyword args as appropriate:
591 for key, param_value in kwargs.items():
592 key = key.replace('Color', 'Colour')
593 if key not in wxMaskedNumCtrl.valid_ctrl_params.keys():
594 raise AttributeError('invalid keyword argument "%s"' % key)
595 elif key not in wxMaskedEditMixin.valid_ctrl_params.keys():
596 setattr(self, '_' + key, param_value)
597 elif key in ('mask', 'autoformat'): # disallow explicit setting of mask
598 raise AttributeError('invalid keyword argument "%s"' % key)
599 else:
600 maskededit_kwargs[key] = param_value
601 dbg('kwargs:', kwargs)
602
603 # reprocess existing format codes to ensure proper resulting format:
604 formatcodes = self.GetFormatcodes()
605 if kwargs.has_key('allowNegative'):
606 if kwargs['allowNegative'] and '-' not in formatcodes:
607 formatcodes += '-'
608 maskededit_kwargs['formatcodes'] = formatcodes
609 elif not kwargs['allowNegative'] and '-' in formatcodes:
610 formatcodes = formatcodes.replace('-','')
611 maskededit_kwargs['formatcodes'] = formatcodes
612
613 if kwargs.has_key('groupDigits'):
614 if kwargs['groupDigits'] and ',' not in formatcodes:
615 formatcodes += ','
616 maskededit_kwargs['formatcodes'] = formatcodes
617 elif not kwargs['groupDigits'] and ',' in formatcodes:
618 formatcodes = formatcodes.replace(',','')
619 maskededit_kwargs['formatcodes'] = formatcodes
620
621 if kwargs.has_key('selectOnEntry'):
622 self._selectOnEntry = kwargs['selectOnEntry']
623 dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes)
624 if kwargs['selectOnEntry'] and 'S' not in formatcodes:
625 formatcodes += 'S'
626 maskededit_kwargs['formatcodes'] = formatcodes
627 elif not kwargs['selectOnEntry'] and 'S' in formatcodes:
628 formatcodes = formatcodes.replace('S','')
629 maskededit_kwargs['formatcodes'] = formatcodes
630
631 if 'r' in formatcodes and self._fractionWidth:
632 # top-level mask should only be right insert if no fractional
633 # part will be shown; ie. if reconfiguring control, remove
634 # previous "global" setting.
635 formatcodes = formatcodes.replace('r', '')
636 maskededit_kwargs['formatcodes'] = formatcodes
637
638 if kwargs.has_key('limited'):
639 if kwargs['limited'] and not self._limited:
640 maskededit_kwargs['validRequired'] = True
641 elif not kwargs['limited'] and self._limited:
642 maskededit_kwargs['validRequired'] = False
643 self._limited = kwargs['limited']
644
645 dbg('maskededit_kwargs:', maskededit_kwargs)
646 if maskededit_kwargs.keys():
647 self.SetCtrlParameters(**maskededit_kwargs)
648
649 # Record end of integer and place cursor there:
650 integerEnd = self._fields[0]._extent[1]
651 self.SetInsertionPoint(integerEnd)
652 self.SetSelection(integerEnd, integerEnd)
653
654 # Go ensure all the format codes necessary are present:
655 orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes')
656 if 'r' not in intformat:
657 intformat += 'r'
658 if '>' not in intformat:
659 intformat += '>'
660 if intformat != orig_intformat:
661 if self._fractionWidth:
662 self.SetFieldParameters(0, formatcodes=intformat)
663 else:
664 self.SetCtrlParameters(formatcodes=intformat)
665
666 # Set min and max as appropriate:
667 if kwargs.has_key('min'):
668 min = kwargs['min']
669 if( self._max is None
670 or min is None
671 or (self._max is not None and self._max >= min) ):
672 dbg('examining min')
673 if min is not None:
674 try:
675 textmin = self._toGUI(min, apply_limits = False)
676 except ValueError:
677 dbg('min will not fit into control; ignoring', indent=0)
678 raise
679 dbg('accepted min')
680 self._min = min
681 else:
682 dbg('ignoring min')
683
684
685 if kwargs.has_key('max'):
686 max = kwargs['max']
687 if( self._min is None
688 or max is None
689 or (self._min is not None and self._min <= max) ):
690 dbg('examining max')
691 if max is not None:
692 try:
693 textmax = self._toGUI(max, apply_limits = False)
694 except ValueError:
695 dbg('max will not fit into control; ignoring', indent=0)
696 raise
697 dbg('accepted max')
698 self._max = max
699 else:
700 dbg('ignoring max')
701
702 if kwargs.has_key('allowNegative'):
703 self._allowNegative = kwargs['allowNegative']
704
705 # Ensure current value of control obeys any new restrictions imposed:
706 text = self._GetValue()
707 dbg('text value: "%s"' % text)
708 if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1:
709 text = text.replace(old_groupchar, self._groupChar)
710 if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1:
711 text = text.replace(old_decimalchar, self._decimalChar)
712 if text != self._GetValue():
713 wxTextCtrl.SetValue(self, text)
714
715 value = self.GetValue()
716
717 dbg('self._allowNegative?', self._allowNegative)
718 if not self._allowNegative and self._isNeg:
719 value = abs(value)
720 dbg('abs(value):', value)
721 self._isNeg = False
722
723 elif not self._allowNone and wxMaskedTextCtrl.GetValue(self) == '':
724 if self._min > 0:
725 value = self._min
726 else:
727 value = 0
728
729 sel_start, sel_to = self.GetSelection()
730 if self.IsLimited() and self._min is not None and value < self._min:
731 dbg('Set to min value:', self._min)
732 self._SetValue(self._toGUI(self._min))
733
734 elif self.IsLimited() and self._max is not None and value > self._max:
735 dbg('Setting to max value:', self._max)
736 self._SetValue(self._toGUI(self._max))
737 else:
738 # reformat current value as appropriate to possibly new conditions
739 dbg('Reformatting value:', value)
740 sel_start, sel_to = self.GetSelection()
741 self._SetValue(self._toGUI(value))
742 self.Refresh() # recolor as appropriate
743 dbg('finished wxMaskedNumCtrl::SetParameters', indent=0)
744
745
746
747 def _GetNumValue(self, value):
748 """
749 This function attempts to "clean up" a text value, providing a regularized
750 convertable string, via atol() or atof(), for any well-formed numeric text value.
751 """
752 return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip()
753
754
755 def GetFraction(self, candidate=None):
756 """
757 Returns the fractional portion of the value as a float. If there is no
758 fractional portion, the value returned will be 0.0.
759 """
760 if not self._fractionWidth:
761 return 0.0
762 else:
763 fracstart, fracend = self._fields[1]._extent
764 if candidate is None:
765 value = self._toGUI(wxMaskedTextCtrl.GetValue(self))
766 else:
767 value = self._toGUI(candidate)
768 fracstring = value[fracstart:fracend].strip()
769 if not value:
770 return 0.0
771 else:
772 return string.atof(fracstring)
773
774 def _OnChangeSign(self, event):
775 dbg('wxMaskedNumCtrl::_OnChangeSign', indent=1)
776 self._typedSign = True
777 wxMaskedEditMixin._OnChangeSign(self, event)
778 dbg(indent=0)
779
780
781 def _disallowValue(self):
782 dbg('wxMaskedNumCtrl::_disallowValue')
783 # limited and -1 is out of bounds
784 if self._typedSign:
785 self._isNeg = False
786 if not wxValidator_IsSilent():
787 wxBell()
788 sel_start, sel_to = self._GetSelection()
789 dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
790 wxCallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position
791 wxCallAfter(self.SetSelection, sel_start, sel_to)
792
793 def _SetValue(self, value):
794 """
795 This routine supersedes the base masked control _SetValue(). It is
796 needed to ensure that the value of the control is always representable/convertable
797 to a numeric return value (via GetValue().) This routine also handles
798 automatic adjustment and grouping of the value without explicit intervention
799 by the user.
800 """
801
802 dbg('wxMaskedNumCtrl::_SetValue("%s")' % value, indent=1)
803
804 if( (self._fractionWidth and value.find(self._decimalChar) == -1) or
805 (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) :
806 value = self._toGUI(value)
807
808 numvalue = self._GetNumValue(value)
809 dbg('cleansed value: "%s"' % numvalue)
810 replacement = None
811
812 if numvalue == "":
813 if self._allowNone:
814 dbg('calling base wxMaskedTextCtrl._SetValue(self, "%s")' % value)
815 wxMaskedTextCtrl._SetValue(self, value)
816 self.Refresh()
817 return
818 elif self._min > 0 and self.IsLimited():
819 replacement = self._min
820 else:
821 replacement = 0
822 dbg('empty value; setting replacement:', replacement)
823
824 if replacement is None:
825 # Go get the integer portion about to be set and verify its validity
826 intstart, intend = self._fields[0]._extent
827 dbg('intstart, intend:', intstart, intend)
828 dbg('raw integer:"%s"' % value[intstart:intend])
829 int = self._GetNumValue(value[intstart:intend])
830 numval = self._fromGUI(value)
831
832 dbg('integer: "%s"' % int)
833 try:
834 fracval = self.GetFraction(value)
835 except ValueError, e:
836 dbg('Exception:', e, 'must be out of bounds; disallow value')
837 self._disallowValue()
838 dbg(indent=0)
839 return
840
841 if fracval == 0.0:
842 dbg('self._isNeg?', self._isNeg)
843 if int == '-' and self._oldvalue < 0 and not self._typedSign:
844 dbg('just a negative sign; old value < 0; setting replacement of 0')
845 replacement = 0
846 self._isNeg = False
847 elif int[:2] == '-0' and self._fractionWidth == 0:
848 if self._oldvalue < 0:
849 dbg('-0; setting replacement of 0')
850 replacement = 0
851 self._isNeg = False
852 elif not self._limited or (self._min < -1 and self._max >= -1):
853 dbg('-0; setting replacement of -1')
854 replacement = -1
855 self._isNeg = True
856 else:
857 # limited and -1 is out of bounds
858 self._disallowValue()
859 dbg(indent=0)
860 return
861
862 elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0:
863 if not self._limited or (self._min < -1 and self._max >= -1):
864 dbg('just a negative sign; setting replacement of -1')
865 replacement = -1
866 else:
867 # limited and -1 is out of bounds
868 self._disallowValue()
869 dbg(indent=0)
870 return
871
872 elif( self._typedSign
873 and int.find('-') != -1
874 and self._limited
875 and not self._min <= numval <= self._max):
876 # changed sign resulting in value that's now out-of-bounds;
877 # disallow
878 self._disallowValue()
879 dbg(indent=0)
880 return
881
882 if replacement is None:
883 if int and int != '-':
884 try:
885 string.atol(int)
886 except ValueError:
887 # integer requested is not legal. This can happen if the user
888 # is attempting to insert a digit in the middle of the control
889 # resulting in something like " 3 45". Disallow such actions:
890 dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int)
891 if not wxValidator_IsSilent():
892 wxBell()
893 sel_start, sel_to = self._GetSelection()
894 dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to))
895 wxCallAfter(self.SetInsertionPoint, sel_start) # preserve current selection/position
896 wxCallAfter(self.SetSelection, sel_start, sel_to)
897 dbg(indent=0)
898 return
899
900 if int[0] == '0' and len(int) > 1:
901 dbg('numvalue: "%s"' % numvalue.replace(' ', ''))
902 if self._fractionWidth:
903 value = self._toGUI(string.atof(numvalue))
904 else:
905 value = self._toGUI(string.atol(numvalue))
906 dbg('modified value: "%s"' % value)
907
908 self._typedSign = False # reset state var
909
910 if replacement is not None:
911 # Value presented wasn't a legal number, but control should do something
912 # reasonable instead:
913 dbg('setting replacement value:', replacement)
914 self._SetValue(self._toGUI(replacement))
915 sel_start = wxMaskedTextCtrl.GetValue(self).find(str(abs(replacement))) # find where it put the 1, so we can select it
916 sel_to = sel_start + len(str(abs(replacement)))
917 dbg('queuing selection of (%d, %d)' %(sel_start, sel_to))
918 wxCallAfter(self.SetInsertionPoint, sel_start)
919 wxCallAfter(self.SetSelection, sel_start, sel_to)
920 dbg(indent=0)
921 return
922
923 # Otherwise, apply appropriate formatting to value:
924
925 # Because we're intercepting the value and adjusting it
926 # before a sign change is detected, we need to do this here:
927 if '-' in value or '(' in value:
928 self._isNeg = True
929 else:
930 self._isNeg = False
931
932 dbg('value:"%s"' % value, 'self._useParens:', self._useParens)
933 if self._fractionWidth:
934 adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar))
935 else:
936 adjvalue = self._adjustInt(self._GetNumValue(value))
937 dbg('adjusted value: "%s"' % adjvalue)
938
939
940 sel_start, sel_to = self._GetSelection() # record current insertion point
941 dbg('calling base wxMaskedTextCtrl._SetValue(self, "%s")' % adjvalue)
942 wxMaskedTextCtrl._SetValue(self, adjvalue)
943 # After all actions so far scheduled, check that resulting cursor
944 # position is appropriate, and move if not:
945 wxCallAfter(self._CheckInsertionPoint)
946
947 dbg('finished wxMaskedNumCtrl::_SetValue', indent=0)
948
949 def _CheckInsertionPoint(self):
950 # If current insertion point is before the end of the integer and
951 # its before the 1st digit, place it just after the sign position:
952 dbg('wxMaskedNumCtrl::CheckInsertionPoint', indent=1)
953 sel_start, sel_to = self._GetSelection()
954 text = self._GetValue()
955 if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('):
956 text, signpos, right_signpos = self._getSignedValue()
957 dbg('setting selection(%d, %d)' % (signpos+1, signpos+1))
958 self.SetInsertionPoint(signpos+1)
959 self.SetSelection(signpos+1, signpos+1)
960 dbg(indent=0)
961
962
963 def _OnErase( self, event ):
964 """
965 This overrides the base control _OnErase, so that erasing around
966 grouping characters auto selects the digit before or after the
967 grouping character, so that the erasure does the right thing.
968 """
969 dbg('wxMaskedNumCtrl::_OnErase', indent=1)
970
971 #if grouping digits, make sure deletes next to group char always
972 # delete next digit to appropriate side:
973 if self._groupDigits:
974 key = event.GetKeyCode()
975 value = wxMaskedTextCtrl.GetValue(self)
976 sel_start, sel_to = self._GetSelection()
977
978 if key == WXK_BACK:
979 # if 1st selected char is group char, select to previous digit
980 if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar:
981 self.SetInsertionPoint(sel_start-1)
982 self.SetSelection(sel_start-1, sel_to)
983
984 # elif previous char is group char, select to previous digit
985 elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar:
986 self.SetInsertionPoint(sel_start-2)
987 self.SetSelection(sel_start-2, sel_to)
988
989 elif key == WXK_DELETE:
990 if( sel_to < len(self._mask) - 2 + (1 *self._useParens)
991 and sel_start == sel_to
992 and value[sel_to] == self._groupChar ):
993 self.SetInsertionPoint(sel_start)
994 self.SetSelection(sel_start, sel_to+2)
995
996 elif( sel_to < len(self._mask) - 2 + (1 *self._useParens)
997 and value[sel_start:sel_to] == self._groupChar ):
998 self.SetInsertionPoint(sel_start)
999 self.SetSelection(sel_start, sel_to+1)
1000
1001 wxMaskedTextCtrl._OnErase(self, event)
1002 dbg(indent=0)
1003
1004
1005 def OnTextChange( self, event ):
1006 """
1007 Handles an event indicating that the text control's value
1008 has changed, and issue EVT_MaskedNum event.
1009 NOTE: using wxTextCtrl.SetValue() to change the control's
1010 contents from within a EVT_CHAR handler can cause double
1011 text events. So we check for actual changes to the text
1012 before passing the events on.
1013 """
1014 dbg('wxMaskedNumCtrl::OnTextChange', indent=1)
1015 if not wxMaskedTextCtrl._OnTextChange(self, event):
1016 dbg(indent=0)
1017 return
1018
1019 # else... legal value
1020
1021 value = self.GetValue()
1022 if value != self._oldvalue:
1023 try:
1024 self.GetEventHandler().ProcessEvent(
1025 wxMaskedNumNumberUpdatedEvent( self.GetId(), self.GetValue(), self ) )
1026 except ValueError:
1027 dbg(indent=0)
1028 return
1029 # let normal processing of the text continue
1030 event.Skip()
1031 self._oldvalue = value # record for next event
1032 dbg(indent=0)
1033
1034 def _GetValue(self):
1035 """
1036 Override of wxMaskedTextCtrl to allow amixin to get the raw text value of the
1037 control with this function.
1038 """
1039 return wxTextCtrl.GetValue(self)
1040
1041
1042 def GetValue(self):
1043 """
1044 Returns the current numeric value of the control.
1045 """
1046 return self._fromGUI( wxMaskedTextCtrl.GetValue(self) )
1047
1048 def SetValue(self, value):
1049 """
1050 Sets the value of the control to the value specified.
1051 The resulting actual value of the control may be altered to
1052 conform with the bounds set on the control if limited,
1053 or colored if not limited but the value is out-of-bounds.
1054 A ValueError exception will be raised if an invalid value
1055 is specified.
1056 """
1057 wxMaskedTextCtrl.SetValue( self, self._toGUI(value) )
1058
1059
1060 def SetIntegerWidth(self, value):
1061 self.SetCtrlParameters(integerWidth=value)
1062 def GetIntegerWidth(self):
1063 return self._integerWidth
1064
1065 def SetFractionWidth(self, value):
1066 self.SetCtrlParameters(fractionWidth=value)
1067 def GetFractionWidth(self):
1068 return self._fractionWidth
1069
1070
1071
1072 def SetMin(self, min=None):
1073 """
1074 Sets the minimum value of the control. If a value of None
1075 is provided, then the control will have no explicit minimum value.
1076 If the value specified is greater than the current maximum value,
1077 then the function returns False and the minimum will not change from
1078 its current setting. On success, the function returns True.
1079
1080 If successful and the current value is lower than the new lower
1081 bound, if the control is limited, the value will be automatically
1082 adjusted to the new minimum value; if not limited, the value in the
1083 control will be colored as invalid.
1084
1085 If min > the max value allowed by the width of the control,
1086 the function will return False, and the min will not be set.
1087 """
1088 dbg('wxMaskedNumCtrl::SetMin(%s)' % repr(min), indent=1)
1089 if( self._max is None
1090 or min is None
1091 or (self._max is not None and self._max >= min) ):
1092 try:
1093 self.SetParameters(min=min)
1094 bRet = True
1095 except ValueError:
1096 bRet = False
1097 else:
1098 bRet = False
1099 dbg(indent=0)
1100 return bRet
1101
1102 def GetMin(self):
1103 """
1104 Gets the lower bound value of the control. It will return
1105 None if not specified.
1106 """
1107 return self._min
1108
1109
1110 def SetMax(self, max=None):
1111 """
1112 Sets the maximum value of the control. If a value of None
1113 is provided, then the control will have no explicit maximum value.
1114 If the value specified is less than the current minimum value, then
1115 the function returns False and the maximum will not change from its
1116 current setting. On success, the function returns True.
1117
1118 If successful and the current value is greater than the new upper
1119 bound, if the control is limited the value will be automatically
1120 adjusted to this maximum value; if not limited, the value in the
1121 control will be colored as invalid.
1122
1123 If max > the max value allowed by the width of the control,
1124 the function will return False, and the max will not be set.
1125 """
1126 if( self._min is None
1127 or max is None
1128 or (self._min is not None and self._min <= max) ):
1129 try:
1130 self.SetParameters(max=max)
1131 bRet = True
1132 except ValueError:
1133 bRet = False
1134 else:
1135 bRet = False
1136
1137 return bRet
1138
1139
1140 def GetMax(self):
1141 """
1142 Gets the maximum value of the control. It will return the current
1143 maximum integer, or None if not specified.
1144 """
1145 return self._max
1146
1147
1148 def SetBounds(self, min=None, max=None):
1149 """
1150 This function is a convenience function for setting the min and max
1151 values at the same time. The function only applies the maximum bound
1152 if setting the minimum bound is successful, and returns True
1153 only if both operations succeed.
1154 NOTE: leaving out an argument will remove the corresponding bound.
1155 """
1156 ret = self.SetMin(min)
1157 return ret and self.SetMax(max)
1158
1159
1160 def GetBounds(self):
1161 """
1162 This function returns a two-tuple (min,max), indicating the
1163 current bounds of the control. Each value can be None if
1164 that bound is not set.
1165 """
1166 return (self._min, self._max)
1167
1168
1169 def SetLimited(self, limited):
1170 """
1171 If called with a value of True, this function will cause the control
1172 to limit the value to fall within the bounds currently specified.
1173 If the control's value currently exceeds the bounds, it will then
1174 be limited accordingly.
1175
1176 If called with a value of False, this function will disable value
1177 limiting, but coloring of out-of-bounds values will still take
1178 place if bounds have been set for the control.
1179 """
1180 self.SetParameters(limited = limited)
1181
1182
1183 def IsLimited(self):
1184 """
1185 Returns True if the control is currently limiting the
1186 value to fall within the current bounds.
1187 """
1188 return self._limited
1189
1190 def GetLimited(self):
1191 """ (For regularization of property accessors) """
1192 return self.IsLimited
1193
1194
1195 def IsInBounds(self, value=None):
1196 """
1197 Returns True if no value is specified and the current value
1198 of the control falls within the current bounds. This function can
1199 also be called with a value to see if that value would fall within
1200 the current bounds of the given control.
1201 """
1202 dbg('IsInBounds(%s)' % repr(value), indent=1)
1203 if value is None:
1204 value = self.GetValue()
1205 else:
1206 try:
1207 value = self._GetNumValue(self._toGUI(value))
1208 except ValueError, e:
1209 dbg('error getting NumValue(self._toGUI(value)):', e, indent=0)
1210 return False
1211 if value == '':
1212 value = None
1213 elif self._fractionWidth:
1214 value = float(value)
1215 else:
1216 value = long(value)
1217
1218 min = self.GetMin()
1219 max = self.GetMax()
1220 if min is None: min = value
1221 if max is None: max = value
1222
1223 # if bounds set, and value is None, return False
1224 if value == None and (min is not None or max is not None):
1225 dbg('finished IsInBounds', indent=0)
1226 return 0
1227 else:
1228 dbg('finished IsInBounds', indent=0)
1229 return min <= value <= max
1230
1231
1232 def SetAllowNone(self, allow_none):
1233 """
1234 Change the behavior of the validation code, allowing control
1235 to have a value of None or not, as appropriate. If the value
1236 of the control is currently None, and allow_none is False, the
1237 value of the control will be set to the minimum value of the
1238 control, or 0 if no lower bound is set.
1239 """
1240 self._allowNone = allow_none
1241 if not allow_none and self.GetValue() is None:
1242 min = self.GetMin()
1243 if min is not None: self.SetValue(min)
1244 else: self.SetValue(0)
1245
1246
1247 def IsNoneAllowed(self):
1248 return self._allowNone
1249 def GetAllowNone(self):
1250 """ (For regularization of property accessors) """
1251 return self.IsNoneAllowed()
1252
1253 def SetAllowNegative(self, value):
1254 self.SetParameters(allowNegative=value)
1255 def IsNegativeAllowed(self):
1256 return self._allowNegative
1257 def GetAllowNegative(self):
1258 """ (For regularization of property accessors) """
1259 return self.IsNegativeAllowed()
1260
1261 def SetGroupDigits(self, value):
1262 self.SetParameters(groupDigits=value)
1263 def IsGroupingAllowed(self):
1264 return self._groupDigits
1265 def GetGroupDigits(self):
1266 """ (For regularization of property accessors) """
1267 return self.IsGroupingAllowed()
1268
1269 def SetGroupChar(self, value):
1270 self.SetParameters(groupChar=value)
1271 def GetGroupChar(self):
1272 return self._groupChar
1273
1274 def SetDecimalChar(self, value):
1275 self.SetParameters(decimalChar=value)
1276 def GetDecimalChar(self):
1277 return self._decimalChar
1278
1279 def SetSelectOnEntry(self, value):
1280 self.SetParameters(selectOnEntry=value)
1281 def GetSelectOnEntry(self):
1282 return self._selectOnEntry
1283
1284 # (Other parameter accessors are inherited from base class)
1285
1286
1287 def _toGUI( self, value, apply_limits = True ):
1288 """
1289 Conversion function used to set the value of the control; does
1290 type and bounds checking and raises ValueError if argument is
1291 not a valid value.
1292 """
1293 dbg('wxMaskedNumCtrl::_toGUI(%s)' % repr(value), indent=1)
1294 if value is None and self.IsNoneAllowed():
1295 dbg(indent=0)
1296 return self._template
1297
1298 elif type(value) in (types.StringType, types.UnicodeType):
1299 value = self._GetNumValue(value)
1300 dbg('cleansed num value: "%s"' % value)
1301 try:
1302 if self._fractionWidth or value.find('.') != -1:
1303 value = float(value)
1304 else:
1305 value = long(value)
1306 except Exception, e:
1307 dbg('exception raised:', e, indent=0)
1308 raise ValueError ('wxMaskedNumCtrl requires numeric value, passed %s'% repr(value) )
1309
1310 elif type(value) not in (types.IntType, types.LongType, types.FloatType):
1311 dbg(indent=0)
1312 raise ValueError (
1313 'wxMaskedNumCtrl requires numeric value, passed %s'% repr(value) )
1314
1315 if not self._allowNegative and value < 0:
1316 raise ValueError (
1317 'control configured to disallow negative values, passed %s'% repr(value) )
1318
1319 if self.IsLimited() and apply_limits:
1320 min = self.GetMin()
1321 max = self.GetMax()
1322 if not min is None and value < min:
1323 dbg(indent=0)
1324 raise ValueError (
1325 'value %d is below minimum value of control'% value )
1326 if not max is None and value > max:
1327 dbg(indent=0)
1328 raise ValueError (
1329 'value %d exceeds value of control'% value )
1330
1331 adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk)
1332 dbg('len(%s):' % self._mask, len(self._mask))
1333 dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace)
1334 dbg('adjustwidth:', adjustwidth)
1335 if self._fractionWidth == 0:
1336 s = str(long(value)).rjust(self._integerWidth)
1337 else:
1338 format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth)
1339 s = format % float(value)
1340 dbg('s:"%s"' % s, 'len(s):', len(s))
1341 if len(s) > (adjustwidth - self._groupSpace):
1342 dbg(indent=0)
1343 raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
1344 elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace):
1345 dbg(indent=0)
1346 raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth))
1347
1348 s = s.rjust(adjustwidth).replace('.', self._decimalChar)
1349 if self._signOk and self._useParens:
1350 if s.find('-') != -1:
1351 s = s.replace('-', '(') + ')'
1352 else:
1353 s += ' '
1354 dbg('returned: "%s"' % s, indent=0)
1355 return s
1356
1357
1358 def _fromGUI( self, value ):
1359 """
1360 Conversion function used in getting the value of the control.
1361 """
1362 dbg(suspend=0)
1363 dbg('wxMaskedNumCtrl::_fromGUI(%s)' % value, indent=1)
1364 # One or more of the underlying text control implementations
1365 # issue an intermediate EVT_TEXT when replacing the control's
1366 # value, where the intermediate value is an empty string.
1367 # So, to ensure consistency and to prevent spurious ValueErrors,
1368 # we make the following test, and react accordingly:
1369 #
1370 if value == '':
1371 if not self.IsNoneAllowed():
1372 dbg('empty value; not allowed,returning 0', indent = 0)
1373 if self._fractionWidth:
1374 return 0.0
1375 else:
1376 return 0
1377 else:
1378 dbg('empty value; returning None', indent = 0)
1379 return None
1380 else:
1381 value = self._GetNumValue(value)
1382 dbg('Num value: "%s"' % value)
1383 if self._fractionWidth:
1384 try:
1385 dbg(indent=0)
1386 return float( value )
1387 except ValueError:
1388 dbg("couldn't convert to float; returning None")
1389 return None
1390 else:
1391 raise
1392 else:
1393 try:
1394 dbg(indent=0)
1395 return int( value )
1396 except ValueError:
1397 try:
1398 dbg(indent=0)
1399 return long( value )
1400 except ValueError:
1401 dbg("couldn't convert to long; returning None")
1402 return None
1403
1404 else:
1405 raise
1406 else:
1407 dbg('exception occurred; returning None')
1408 return None
1409
1410
1411 def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
1412 """
1413 Preprocessor for base control paste; if value needs to be right-justified
1414 to fit in control, do so prior to paste:
1415 """
1416 dbg('wxMaskedNumCtrl::_Paste (value = "%s")' % value)
1417 if value is None:
1418 paste_text = self._getClipboardContents()
1419 else:
1420 paste_text = value
1421
1422 # treat paste as "replace number", if appropriate:
1423 sel_start, sel_to = self._GetSelection()
1424 if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent:
1425 paste_text = self._toGUI(paste_text)
1426 self._SetSelection(0, len(self._mask))
1427
1428 return wxMaskedEditMixin._Paste(self,
1429 paste_text,
1430 raise_on_invalid=raise_on_invalid,
1431 just_return_value=just_return_value)
1432
1433
1434
1435 #===========================================================================
1436
1437 if __name__ == '__main__':
1438
1439 import traceback
1440
1441 class myDialog(wxDialog):
1442 def __init__(self, parent, id, title,
1443 pos = wxPyDefaultPosition, size = wxPyDefaultSize,
1444 style = wxDEFAULT_DIALOG_STYLE ):
1445 wxDialog.__init__(self, parent, id, title, pos, size, style)
1446
1447 self.int_ctrl = wxMaskedNumCtrl(self, wxNewId(), size=(55,20))
1448 self.OK = wxButton( self, wxID_OK, "OK")
1449 self.Cancel = wxButton( self, wxID_CANCEL, "Cancel")
1450
1451 vs = wxBoxSizer( wxVERTICAL )
1452 vs.AddWindow( self.int_ctrl, 0, wxALIGN_CENTRE|wxALL, 5 )
1453 hs = wxBoxSizer( wxHORIZONTAL )
1454 hs.AddWindow( self.OK, 0, wxALIGN_CENTRE|wxALL, 5 )
1455 hs.AddWindow( self.Cancel, 0, wxALIGN_CENTRE|wxALL, 5 )
1456 vs.AddSizer(hs, 0, wxALIGN_CENTRE|wxALL, 5 )
1457
1458 self.SetAutoLayout( True )
1459 self.SetSizer( vs )
1460 vs.Fit( self )
1461 vs.SetSizeHints( self )
1462 EVT_MASKEDNUM(self, self.int_ctrl.GetId(), self.OnChange)
1463
1464 def OnChange(self, event):
1465 print 'value now', event.GetValue()
1466
1467 class TestApp(wxApp):
1468 def OnInit(self):
1469 try:
1470 self.frame = wxFrame(NULL, -1, "Test",
1471 wxPoint(20,20), wxSize(120,100) )
1472 self.panel = wxPanel(self.frame, -1)
1473 button = wxButton(self.panel, 10, "Push Me",
1474 wxPoint(20, 20))
1475 EVT_BUTTON(self, 10, self.OnClick)
1476 except:
1477 traceback.print_exc()
1478 return False
1479 return True
1480
1481 def OnClick(self, event):
1482 dlg = myDialog(self.panel, -1, "test wxMaskedNumCtrl")
1483 dlg.int_ctrl.SetValue(501)
1484 dlg.int_ctrl.SetInsertionPoint(1)
1485 dlg.int_ctrl.SetSelection(1,2)
1486 rc = dlg.ShowModal()
1487 print 'final value', dlg.int_ctrl.GetValue()
1488 del dlg
1489 self.frame.Destroy()
1490
1491 def Show(self):
1492 self.frame.Show(True)
1493
1494 try:
1495 app = TestApp(0)
1496 app.Show()
1497 app.MainLoop()
1498 except:
1499 traceback.print_exc()
1500
1501 i=0
1502 ## To-Do's:
1503 ## =============================##
1504 ## 1. Add support for printf-style format specification.
1505 ## 2. Add option for repositioning on 'illegal' insertion point.