1 #---------------------------------------------------------------------------- 
   2 # Name:         wxPython.lib.masked.numctrl.py 
   5 # Copyright:   (c) 2003 by Will Sadkin 
   7 # License:     wxWidgets license 
   8 #---------------------------------------------------------------------------- 
  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 masked.TextCtrl), but allows Get/Set of numeric 
  13 #   values, rather than text. 
  15 #   Masked.NumCtrl 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 masked.EVT_NUM() event function for trapping 
  18 #   changes to the control. 
  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. 
  27 #   masked.NumCtrl also supports range limits, with the option of either 
  28 #   enforcing them or simply coloring the text of the control if the limits 
  31 #   masked.NumCtrl is intended to support fixed-point numeric entry, and 
  32 #   is derived from BaseMaskedTextCtrl.  As such, it supports a limited range 
  33 #   of values to comply with a fixed-width entry mask. 
  34 #---------------------------------------------------------------------------- 
  35 # 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  37 # o Updated for wx namespace 
  39 # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  41 # o wxMaskedEditMixin -> MaskedEditMixin 
  42 # o wxMaskedTextCtrl -> masked.TextCtrl 
  43 # o wxMaskedNumNumberUpdatedEvent -> masked.NumberUpdatedEvent 
  44 # o wxMaskedNumCtrl -> masked.NumCtrl 
  49   - allows you to get and set integer or floating point numbers as value,</LI> 
  50   - provides bounds support and optional value limiting,</LI> 
  51   - has the right-insert input style that MaskedTextCtrl supports,</LI> 
  52   - provides optional automatic grouping, sign control and format, grouping and decimal 
  53     character selection, etc. etc.</LI> 
  56   Being derived from masked.TextCtrl, the control only allows 
  57   fixed-point  notation.  That is, it has a fixed (though reconfigurable) 
  58   maximum width for the integer portion and optional fixed width 
  63         from wx.lib.masked import NumCtrl 
  68              pos = wx.DefaultPosition, 
  69              size = wx.DefaultSize, 
  71              validator = wx.DefaultValidator, 
  72              name = "masked.number", 
  77              useParensForNegatives = False, 
  85              foregroundColour = "Black", 
  86              signedForegroundColour = "Red", 
  87              emptyBackgroundColour = "White", 
  88              validBackgroundColour = "White", 
  89              invalidBackgroundColour = "Yellow", 
  95         If no initial value is set, the default will be zero, or 
  96         the minimum value, if specified.  If an illegal string is specified, 
  97         a ValueError will result. (You can always later set the initial 
  98         value with SetValue() after instantiation of the control.) 
 101         Indicates how many places to the right of any decimal point 
 102         should be allowed in the control.  This will, perforce, limit 
 103         the size of the values that can be entered. This number need 
 104         not include space for grouping characters or the sign, if either 
 105         of these options are enabled, as the resulting underlying 
 106         mask is automatically by the control.  The default of 10 
 107         will allow any 32 bit integer value.  The minimum value 
 108         for integerWidth is 1. 
 111         Indicates how many decimal places to show for numeric value. 
 112         If default (0), then the control will display and return only 
 113         integer or long values. 
 116         Boolean indicating whether or not the control is allowed to be 
 117         empty, representing a value of None for the control. 
 120         Boolean indicating whether or not control is allowed to hold 
 123   useParensForNegatives 
 124         If true, this will cause negative numbers to be displayed with ()s 
 125         rather than -, (although '-' will still trigger a negative number.) 
 128         Indicates whether or not grouping characters should be allowed and/or 
 129         inserted when leaving the control or the decimal character is entered. 
 132         What grouping character will be used if allowed. (By default ',') 
 135         If fractionWidth is > 0, what character will be used to represent 
 136         the decimal point.  (By default '.') 
 139         The minimum value that the control should allow.  This can be also be 
 140         adjusted with SetMin().  If the control is not limited, any value 
 141         below this bound will result in a background colored with the current 
 142         invalidBackgroundColour.  If the min specified will not fit into the 
 143         control, the min setting will be ignored. 
 146         The maximum value that the control should allow.  This can be 
 147         adjusted with SetMax().  If the control is not limited, any value 
 148         above this bound will result in a background colored with the current 
 149         invalidBackgroundColour.  If the max specified will not fit into the 
 150         control, the max setting will be ignored. 
 153         Boolean indicating whether the control prevents values from 
 154         exceeding the currently set minimum and maximum values (bounds). 
 155         If False and bounds are set, out-of-bounds values will 
 156         result in a background colored with the current invalidBackgroundColour. 
 159         Boolean indicating whether or not the value in each field of the 
 160         control should be automatically selected (for replacement) when 
 161         that field is entered, either by cursor movement or tabbing. 
 162         This can be desirable when using these controls for rapid data entry. 
 165         Color value used for positive values of the control. 
 167   signedForegroundColour 
 168         Color value used for negative values of the control. 
 170   emptyBackgroundColour 
 171         What background color to use when the control is considered 
 172         "empty." (allow_none must be set to trigger this behavior.) 
 174   validBackgroundColour 
 175         What background color to use when the control value is 
 178   invalidBackgroundColour 
 179         Color value used for illegal values or values out-of-bounds of the 
 180         control when the bounds are set but the control is not limited. 
 183         Boolean indicating whether or not the control should set its own 
 184         width based on the integer and fraction widths.  True by default. 
 185         <I>Note:</I> Setting this to False will produce seemingly odd 
 186         behavior unless the control is large enough to hold the maximum 
 187         specified value given the widths and the sign positions; if not, 
 188         the control will appear to "jump around" as the contents scroll. 
 189         (ie. autoSize is highly recommended.) 
 191 -------------------------- 
 193 masked.EVT_NUM(win, id, func) 
 194     Respond to a EVT_COMMAND_MASKED_NUMBER_UPDATED event, generated when 
 195     the value changes. Notice that this event will always be sent when the 
 196     control's contents changes - whether this is due to user input or 
 197     comes from the program itself (for example, if SetValue() is called.) 
 200 SetValue(int|long|float|string) 
 201     Sets the value of the control to the value specified, if 
 202     possible.  The resulting actual value of the control may be 
 203     altered to conform to the format of the control, changed 
 204     to conform with the bounds set on the control if limited, 
 205     or colored if not limited but the value is out-of-bounds. 
 206     A ValueError exception will be raised if an invalid value 
 210     Retrieves the numeric value from the control.  The value 
 211     retrieved will be either be returned as a long if the 
 212     fractionWidth is 0, or a float otherwise. 
 215 SetParameters(\*\*kwargs) 
 216     Allows simultaneous setting of various attributes 
 217     of the control after construction.  Keyword arguments 
 218     allowed are the same parameters as supported in the constructor. 
 221 SetIntegerWidth(value) 
 222     Resets the width of the integer portion of the control.  The 
 223     value must be >= 1, or an AttributeError exception will result. 
 224     This value should account for any grouping characters that might 
 225     be inserted (if grouping is enabled), but does not need to account 
 226     for the sign, as that is handled separately by the control. 
 228     Returns the current width of the integer portion of the control, 
 229     not including any reserved sign position. 
 232 SetFractionWidth(value) 
 233     Resets the width of the fractional portion of the control.  The 
 234     value must be >= 0, or an AttributeError exception will result.  If 
 235     0, the current value of the control will be truncated to an integer 
 238     Returns the current width of the fractional portion of the control. 
 242     Resets the minimum value of the control.  If a value of <I>None</I> 
 243     is provided, then the control will have no explicit minimum value. 
 244     If the value specified is greater than the current maximum value, 
 245     then the function returns False and the minimum will not change from 
 246     its current setting.  On success, the function returns True. 
 248     If successful and the current value is lower than the new lower 
 249     bound, if the control is limited, the value will be automatically 
 250     adjusted to the new minimum value; if not limited, the value in the 
 251     control will be colored as invalid. 
 253     If min > the max value allowed by the width of the control, 
 254     the function will return False, and the min will not be set. 
 257     Gets the current lower bound value for the control. 
 258     It will return None if no lower bound is currently specified. 
 262     Resets the maximum value of the control. If a value of <I>None</I> 
 263     is provided, then the control will have no explicit maximum value. 
 264     If the value specified is less than the current minimum value, then 
 265     the function returns False and the maximum will not change from its 
 266     current setting. On success, the function returns True. 
 268     If successful and the current value is greater than the new upper 
 269     bound, if the control is limited the value will be automatically 
 270     adjusted to this maximum value; if not limited, the value in the 
 271     control will be colored as invalid. 
 273     If max > the max value allowed by the width of the control, 
 274     the function will return False, and the max will not be set. 
 277     Gets the current upper bound value for the control. 
 278     It will return None if no upper bound is currently specified. 
 281 SetBounds(min=None,max=None) 
 282     This function is a convenience function for setting the min and max 
 283     values at the same time.  The function only applies the maximum bound 
 284     if setting the minimum bound is successful, and returns True 
 285     only if both operations succeed.  <B><I>Note:</I> leaving out an argument 
 286     will remove the corresponding bound. 
 288     This function returns a two-tuple (min,max), indicating the 
 289     current bounds of the control.  Each value can be None if 
 290     that bound is not set. 
 293 IsInBounds(value=None) 
 294     Returns <I>True</I> if no value is specified and the current value 
 295     of the control falls within the current bounds.  This function can also 
 296     be called with a value to see if that value would fall within the current 
 297     bounds of the given control. 
 301     If called with a value of True, this function will cause the control 
 302     to limit the value to fall within the bounds currently specified. 
 303     If the control's value currently exceeds the bounds, it will then 
 304     be limited accordingly. 
 305     If called with a value of False, this function will disable value 
 306     limiting, but coloring of out-of-bounds values will still take 
 307     place if bounds have been set for the control. 
 312     Returns <I>True</I> if the control is currently limiting the 
 313     value to fall within the current bounds. 
 317     If called with a value of True, this function will cause the control 
 318     to allow the value to be empty, representing a value of None. 
 319     If called with a value of False, this function will prevent the value 
 320     from being None.  If the value of the control is currently None, 
 321     ie. the control is empty, then the value will be changed to that 
 322     of the lower bound of the control, or 0 if no lower bound is set. 
 327     Returns <I>True</I> if the control currently allows its 
 331 SetAllowNegative(bool) 
 332     If called with a value of True, this function will cause the 
 333     control to allow the value to be negative (and reserve space for 
 334     displaying the sign. If called with a value of False, and the 
 335     value of the control is currently negative, the value of the 
 336     control will be converted to the absolute value, and then 
 337     limited appropriately based on the existing bounds of the control 
 343     Returns <I>True</I> if the control currently permits values 
 348     If called with a value of True, this will make the control 
 349     automatically add and manage grouping characters to the presented 
 350     value in integer portion of the control. 
 355     Returns <I>True</I> if the control is currently set to group digits. 
 359     Sets the grouping character for the integer portion of the 
 360     control.  (The default grouping character this is ','. 
 362     Returns the current grouping character for the control. 
 366     If called with a value of <I>True</I>, this will make the control 
 367     automatically select the contents of each field as it is entered 
 368     within the control.  (The default is True.) 
 370     Returns <I>True</I> if the control currently auto selects 
 371     the field values on entry. 
 375     Resets the autoSize attribute of the control. 
 377     Returns the current state of the autoSize attribute for the control. 
 387 from sys 
import maxint
 
 388 MAXINT 
= maxint     
# (constants should be in upper case) 
 391 from wx
.tools
.dbg 
import Logger
 
 392 from wx
.lib
.masked 
import MaskedEditMixin
, Field
, BaseMaskedTextCtrl
 
 396 #---------------------------------------------------------------------------- 
 398 wxEVT_COMMAND_MASKED_NUMBER_UPDATED 
= wx
.NewEventType() 
 399 EVT_NUM 
= wx
.PyEventBinder(wxEVT_COMMAND_MASKED_NUMBER_UPDATED
, 1) 
 401 #---------------------------------------------------------------------------- 
 403 class NumberUpdatedEvent(wx
.PyCommandEvent
): 
 405     Used to fire an EVT_NUM event whenever the value in a NumCtrl changes. 
 408     def __init__(self
, id, value 
= 0, object=None): 
 409         wx
.PyCommandEvent
.__init
__(self
, wxEVT_COMMAND_MASKED_NUMBER_UPDATED
, id) 
 412         self
.SetEventObject(object) 
 415         """Retrieve the value of the control at the time 
 416         this event was generated.""" 
 420 #---------------------------------------------------------------------------- 
 421 class NumCtrlAccessorsMixin
: 
 423     Defines masked.NumCtrl's list of attributes having their own 
 424     Get/Set functions, ignoring those that make no sense for 
 427     exposed_basectrl_params 
= ( 
 431          'useParensForNegatives', 
 437          'signedForegroundColour', 
 438          'emptyBackgroundColour', 
 439          'validBackgroundColour', 
 440          'invalidBackgroundColour', 
 446     for param 
in exposed_basectrl_params
: 
 447         propname 
= param
[0].upper() + param
[1:] 
 448         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
 449         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
 451         if param.find('Colour
') != -1: 
 452             # add non-british spellings, for backward-compatibility 
 453             propname.replace('Colour
', 'Color
') 
 455             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
 456             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
 460 #---------------------------------------------------------------------------- 
 462 class NumCtrl(BaseMaskedTextCtrl, NumCtrlAccessorsMixin): 
 464     Masked edit control supporting "native" numeric values, ie. .SetValue(3), for 
 465     example, and supporting a variety of formatting options, including automatic 
 466     rounding specifiable precision, grouping and decimal place characters, etc. 
 470     valid_ctrl_params = { 
 471         'integerWidth': 10,                 # by default allow all 32-bit integers 
 472         'fractionWidth': 0,                 # by default, use integers 
 473         'decimalChar': '.',                 # by default, use '.' for decimal point 
 474         'allowNegative': True,              # by default, allow negative numbers 
 475         'useParensForNegatives': False,     # by default, use '-' to indicate negatives 
 476         'groupDigits': True,                # by default, don't insert grouping 
 477         'groupChar': ',',                   # by default, use ',' for grouping 
 478         'min': None,                        # by default, no bounds set 
 480         'limited': False,                   # by default, no limiting even if bounds set 
 481         'allowNone': False,                 # by default, don't allow empty value 
 482         'selectOnEntry': True,              # by default, select the value of each field on entry 
 483         'foregroundColour': "Black", 
 484         'signedForegroundColour': "Red", 
 485         'emptyBackgroundColour': "White", 
 486         'validBackgroundColour': "White", 
 487         'invalidBackgroundColour': "Yellow", 
 488         'useFixedWidthFont': True,          # by default, use a fixed-width font 
 489         'autoSize': True,                   # by default, set the width of the control based on the mask 
 494                 self, parent, id=-1, value = 0, 
 495                 pos = wx.DefaultPosition, size = wx.DefaultSize, 
 496                 style = wx.TE_PROCESS_TAB, validator = wx.DefaultValidator, 
 500 ##        dbg('masked.NumCtrl::__init__', indent=1) 
 502         # Set defaults for control: 
 503 ##        dbg('setting defaults:') 
 504         for key, param_value in NumCtrl.valid_ctrl_params.items(): 
 505             # This is done this way to make setattr behave consistently with 
 506             # "private attribute" name mangling 
 507             setattr(self, '_' + key, copy.copy(param_value)) 
 509         # Assign defaults for all attributes: 
 510         init_args = copy.deepcopy(NumCtrl.valid_ctrl_params) 
 511 ##        dbg('kwargs:', kwargs) 
 512         for key, param_value in kwargs.items(): 
 513             key = key.replace('Color', 'Colour') 
 514             if key not in NumCtrl.valid_ctrl_params.keys(): 
 515                 raise AttributeError('invalid keyword argument "%s"' % key) 
 517                 init_args[key] = param_value 
 518 ##        dbg('init_args:', indent=1) 
 519         for key, param_value in init_args.items(): 
 520 ##            dbg('%s:' % key, param_value) 
 524         # Process initial fields for the control, as part of construction: 
 525         if type(init_args['integerWidth']) != types.IntType: 
 526             raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(init_args['integerWidth'])) 
 527         elif init_args['integerWidth'] < 1: 
 528             raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(init_args['integerWidth'])) 
 532         if init_args.has_key('fractionWidth'): 
 533             if type(init_args['fractionWidth']) != types.IntType: 
 534                 raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(self._fractionWidth)) 
 535             elif init_args['fractionWidth'] < 0: 
 536                 raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(init_args['fractionWidth'])) 
 537             self._fractionWidth = init_args['fractionWidth'] 
 539         if self._fractionWidth: 
 540             fracmask = '.' + '#{%d}' % self._fractionWidth 
 541 ##            dbg('fracmask:', fracmask) 
 542             fields[1] = Field(defaultValue='0'*self._fractionWidth) 
 546         self._integerWidth = init_args['integerWidth'] 
 547         if init_args['groupDigits']: 
 548             self._groupSpace = (self._integerWidth - 1) / 3 
 551         intmask = '#{%d}' % (self._integerWidth + self._groupSpace) 
 552         if self._fractionWidth: 
 556         fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) 
 557 ##        dbg('intmask:', intmask) 
 559         # don't bother to reprocess these arguments: 
 560         del init_args['integerWidth'] 
 561         del init_args['fractionWidth'] 
 563         self._autoSize = init_args['autoSize'] 
 570         mask = intmask+fracmask 
 572         # initial value of state vars 
 575         self._typedSign = False 
 577         # Construct the base control: 
 578         BaseMaskedTextCtrl.__init__( 
 579                 self, parent, id, '', 
 580                 pos, size, style, validator, name, 
 582                 formatcodes = formatcodes, 
 584                 validFunc=self.IsInBounds, 
 585                 setupEventHandling = False) 
 587         self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )        ## defeat automatic full selection 
 588         self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )   ## run internal validator 
 589         self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick)  ## select field under cursor on dclick 
 590         self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu )    ## bring up an appropriate context menu 
 591         self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )       ## capture control events not normally seen, eg ctrl-tab. 
 592         self.Bind(wx.EVT_CHAR, self._OnChar )              ## handle each keypress 
 593         self.Bind(wx.EVT_TEXT, self.OnTextChange )      ## color control appropriately & keep 
 594                                                            ## track of previous value for undo 
 596         # Establish any additional parameters, with appropriate error checking 
 597         self.SetParameters(**init_args) 
 599         # Set the value requested (if possible) 
 600 ##        wxCallAfter(self.SetValue, value) 
 603         # Ensure proper coloring: 
 605 ##        dbg('finished NumCtrl::__init__', indent=0) 
 608     def SetParameters(self, **kwargs): 
 610         This function is used to initialize and reconfigure the control. 
 611         See TimeCtrl module overview for available parameters. 
 613 ##        dbg('NumCtrl::SetParameters', indent=1) 
 614         maskededit_kwargs = {} 
 615         reset_fraction_width = False 
 618         if( (kwargs.has_key('integerWidth') and kwargs['integerWidth'] != self._integerWidth) 
 619             or (kwargs.has_key('fractionWidth') and kwargs['fractionWidth'] != self._fractionWidth) 
 620             or (kwargs.has_key('groupDigits') and kwargs['groupDigits'] != self._groupDigits) 
 621             or (kwargs.has_key('autoSize') and kwargs['autoSize'] != self._autoSize) ): 
 625             if kwargs.has_key('fractionWidth'): 
 626                 if type(kwargs['fractionWidth']) != types.IntType: 
 627                     raise AttributeError('invalid fractionWidth (%s) specified; expected integer' % repr(kwargs['fractionWidth'])) 
 628                 elif kwargs['fractionWidth'] < 0: 
 629                     raise AttributeError('invalid fractionWidth (%s) specified; must be >= 0' % repr(kwargs['fractionWidth'])) 
 631                     if self._fractionWidth != kwargs['fractionWidth']: 
 632                         self._fractionWidth = kwargs['fractionWidth'] 
 634             if self._fractionWidth: 
 635                 fracmask = '.' + '#{%d}' % self._fractionWidth 
 636                 fields[1] = Field(defaultValue='0'*self._fractionWidth) 
 641 ##            dbg('fracmask:', fracmask) 
 643             if kwargs.has_key('integerWidth'): 
 644                 if type(kwargs['integerWidth']) != types.IntType: 
 646                     raise AttributeError('invalid integerWidth (%s) specified; expected integer' % repr(kwargs['integerWidth'])) 
 647                 elif kwargs['integerWidth'] < 0: 
 649                     raise AttributeError('invalid integerWidth (%s) specified; must be > 0' % repr(kwargs['integerWidth'])) 
 651                     self._integerWidth = kwargs['integerWidth'] 
 653             if kwargs.has_key('groupDigits'): 
 654                 self._groupDigits = kwargs['groupDigits'] 
 656             if self._groupDigits: 
 657                 self._groupSpace = (self._integerWidth - 1) / 3 
 661             intmask = '#{%d}' % (self._integerWidth + self._groupSpace) 
 662 ##            dbg('intmask:', intmask) 
 663             fields[0] = Field(formatcodes='r<>', emptyInvalid=emptyInvalid) 
 664             maskededit_kwargs['fields'] = fields 
 666             # don't bother to reprocess these arguments: 
 667             if kwargs.has_key('integerWidth'): 
 668                 del kwargs['integerWidth'] 
 669             if kwargs.has_key('fractionWidth'): 
 670                 del kwargs['fractionWidth'] 
 672             maskededit_kwargs['mask'] = intmask+fracmask 
 674         if kwargs.has_key('groupChar') or kwargs.has_key('decimalChar'): 
 675             old_groupchar = self._groupChar     # save so we can reformat properly 
 676             old_decimalchar = self._decimalChar 
 677 ##            dbg("old_groupchar: '%s'" % old_groupchar) 
 678 ##            dbg("old_decimalchar: '%s'" % old_decimalchar) 
 679             groupchar = old_groupchar 
 680             decimalchar = old_decimalchar 
 682             if kwargs.has_key('groupChar'): 
 683                 maskededit_kwargs['groupChar'] = kwargs['groupChar'] 
 684                 groupchar = kwargs['groupChar'] 
 685             if kwargs.has_key('decimalChar'): 
 686                 maskededit_kwargs['decimalChar'] = kwargs['decimalChar'] 
 687                 decimalchar = kwargs['decimalChar'] 
 689             # Add sanity check to make sure these are distinct, and if not, 
 690             # raise attribute error 
 691             if groupchar == decimalchar: 
 692                 raise AttributeError('groupChar and decimalChar must be distinct') 
 695         # for all other parameters, assign keyword args as appropriate: 
 696         for key, param_value in kwargs.items(): 
 697             key = key.replace('Color', 'Colour') 
 698             if key not in NumCtrl.valid_ctrl_params.keys(): 
 699                 raise AttributeError('invalid keyword argument "%s"' % key) 
 700             elif key not in MaskedEditMixin.valid_ctrl_params.keys(): 
 701                 setattr(self, '_' + key, param_value) 
 702             elif key in ('mask', 'autoformat'): # disallow explicit setting of mask 
 703                 raise AttributeError('invalid keyword argument "%s"' % key) 
 705                 maskededit_kwargs[key] = param_value 
 706 ##        dbg('kwargs:', kwargs) 
 708         # reprocess existing format codes to ensure proper resulting format: 
 709         formatcodes = self.GetCtrlParameter('formatcodes') 
 710         if kwargs.has_key('allowNegative'): 
 711             if kwargs['allowNegative'] and '-' not in formatcodes: 
 713                 maskededit_kwargs['formatcodes'] = formatcodes 
 714             elif not kwargs['allowNegative'] and '-' in formatcodes: 
 715                 formatcodes = formatcodes.replace('-','') 
 716                 maskededit_kwargs['formatcodes'] = formatcodes 
 718         if kwargs.has_key('groupDigits'): 
 719             if kwargs['groupDigits'] and ',' not in formatcodes: 
 721                 maskededit_kwargs['formatcodes'] = formatcodes 
 722             elif not kwargs['groupDigits'] and ',' in formatcodes: 
 723                 formatcodes = formatcodes.replace(',','') 
 724                 maskededit_kwargs['formatcodes'] = formatcodes 
 726         if kwargs.has_key('selectOnEntry'): 
 727             self._selectOnEntry = kwargs['selectOnEntry'] 
 728 ##            dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes) 
 729             if kwargs['selectOnEntry'] and 'S' not in formatcodes: 
 731                 maskededit_kwargs['formatcodes'] = formatcodes 
 732             elif not kwargs['selectOnEntry'] and 'S' in formatcodes: 
 733                 formatcodes = formatcodes.replace('S','') 
 734                 maskededit_kwargs['formatcodes'] = formatcodes 
 736         if kwargs.has_key('autoSize'): 
 737             self._autoSize = kwargs['autoSize'] 
 738             if kwargs['autoSize'] and 'F' not in formatcodes: 
 740                 maskededit_kwargs['formatcodes'] = formatcodes 
 741             elif not kwargs['autoSize'] and 'F' in formatcodes: 
 742                 formatcodes = formatcodes.replace('F', '') 
 743                 maskededit_kwargs['formatcodes'] = formatcodes 
 746         if 'r' in formatcodes and self._fractionWidth: 
 747             # top-level mask should only be right insert if no fractional 
 748             # part will be shown; ie. if reconfiguring control, remove 
 749             # previous "global" setting. 
 750             formatcodes = formatcodes.replace('r', '') 
 751             maskededit_kwargs['formatcodes'] = formatcodes 
 754         if kwargs.has_key('limited'): 
 755             if kwargs['limited'] and not self._limited: 
 756                 maskededit_kwargs['validRequired'] = True 
 757             elif not kwargs['limited'] and self._limited: 
 758                 maskededit_kwargs['validRequired'] = False 
 759             self._limited = kwargs['limited'] 
 761 ##        dbg('maskededit_kwargs:', maskededit_kwargs) 
 762         if maskededit_kwargs.keys(): 
 763             self.SetCtrlParameters(**maskededit_kwargs) 
 765         # Go ensure all the format codes necessary are present: 
 766         orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes') 
 767         if 'r' not in intformat: 
 769         if '>' not in intformat: 
 771         if intformat != orig_intformat: 
 772             if self._fractionWidth: 
 773                 self.SetFieldParameters(0, formatcodes=intformat) 
 775                 self.SetCtrlParameters(formatcodes=intformat) 
 777         # Record end of integer and place cursor there unless selecting, or select entire field: 
 778         integerStart, integerEnd = self._fields[0]._extent 
 779         if not self._fields[0]._selectOnFieldEntry: 
 780             self.SetInsertionPoint(0) 
 781             self.SetInsertionPoint(integerEnd) 
 782             self.SetSelection(integerEnd, integerEnd) 
 784             self.SetInsertionPoint(0)   # include any sign 
 785             self.SetSelection(0, integerEnd) 
 788         # Set min and max as appropriate: 
 789         if kwargs.has_key('min'): 
 791             if( self._max is None 
 793                 or (self._max is not None and self._max >= min) ): 
 794 ##                dbg('examining min') 
 797                         textmin = self._toGUI(min, apply_limits = False) 
 799 ##                        dbg('min will not fit into control; ignoring', indent=0) 
 801 ##                dbg('accepted min') 
 804 ##                dbg('ignoring min') 
 808         if kwargs.has_key('max'): 
 810             if( self._min is None 
 812                 or (self._min is not None and self._min <= max) ): 
 813 ##                dbg('examining max') 
 816                         textmax = self._toGUI(max, apply_limits = False) 
 818 ##                        dbg('max will not fit into control; ignoring', indent=0) 
 820 ##                dbg('accepted max') 
 823 ##                dbg('ignoring max') 
 826         if kwargs.has_key('allowNegative'): 
 827             self._allowNegative = kwargs['allowNegative'] 
 829         # Ensure current value of control obeys any new restrictions imposed: 
 830         text = self._GetValue() 
 831 ##        dbg('text value: "%s"' % text) 
 832         if kwargs.has_key('groupChar') and text.find(old_groupchar) != -1: 
 833             text = text.replace(old_groupchar, self._groupChar) 
 834         if kwargs.has_key('decimalChar') and text.find(old_decimalchar) != -1: 
 835             text = text.replace(old_decimalchar, self._decimalChar) 
 836         if text != self._GetValue(): 
 837             wx.TextCtrl.SetValue(self, text) 
 839         value = self.GetValue() 
 841 ##        dbg('self._allowNegative?', self._allowNegative) 
 842         if not self._allowNegative and self._isNeg: 
 844 ##            dbg('abs(value):', value) 
 847         elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '': 
 853         sel_start, sel_to = self.GetSelection() 
 854         if self.IsLimited() and self._min is not None and value < self._min: 
 855 ##            dbg('Set to min value:', self._min) 
 856             self._SetValue(self._toGUI(self._min)) 
 858         elif self.IsLimited() and self._max is not None and value > self._max: 
 859 ##            dbg('Setting to max value:', self._max) 
 860             self._SetValue(self._toGUI(self._max)) 
 862             # reformat current value as appropriate to possibly new conditions 
 863 ##            dbg('Reformatting value:', value) 
 864             sel_start, sel_to = self.GetSelection() 
 865             self._SetValue(self._toGUI(value)) 
 866         self.Refresh() # recolor as appropriate 
 867 ##        dbg('finished NumCtrl::SetParameters', indent=0) 
 871     def _GetNumValue(self, value): 
 873         This function attempts to "clean up" a text value, providing a regularized 
 874         convertable string, via atol() or atof(), for any well-formed numeric text value. 
 876         return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip() 
 879     def GetFraction(self, candidate=None): 
 881         Returns the fractional portion of the value as a float.  If there is no 
 882         fractional portion, the value returned will be 0.0. 
 884         if not self._fractionWidth: 
 887             fracstart, fracend = self._fields[1]._extent 
 888             if candidate is None: 
 889                 value = self._toGUI(BaseMaskedTextCtrl.GetValue(self)) 
 891                 value = self._toGUI(candidate) 
 892             fracstring = value[fracstart:fracend].strip() 
 896                 return string.atof(fracstring) 
 898     def _OnChangeSign(self, event): 
 899 ##        dbg('NumCtrl::_OnChangeSign', indent=1) 
 900         self._typedSign = True 
 901         MaskedEditMixin._OnChangeSign(self, event) 
 905     def _disallowValue(self): 
 906 ##        dbg('NumCtrl::_disallowValue') 
 907         # limited and -1 is out of bounds 
 910         if not wx.Validator_IsSilent(): 
 912         sel_start, sel_to = self._GetSelection() 
 913 ##        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) 
 914         wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position 
 915         wx.CallAfter(self.SetSelection, sel_start, sel_to) 
 917     def _SetValue(self, value): 
 919         This routine supersedes the base masked control _SetValue().  It is 
 920         needed to ensure that the value of the control is always representable/convertable 
 921         to a numeric return value (via GetValue().)  This routine also handles 
 922         automatic adjustment and grouping of the value without explicit intervention 
 926 ##        dbg('NumCtrl::_SetValue("%s")' % value, indent=1) 
 928         if( (self._fractionWidth and value.find(self._decimalChar) == -1) or 
 929             (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) : 
 930             value = self._toGUI(value) 
 932         numvalue = self._GetNumValue(value) 
 933 ##        dbg('cleansed value: "%s"' % numvalue) 
 938 ##                dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value) 
 939                 BaseMaskedTextCtrl._SetValue(self, value) 
 942             elif self._min > 0 and self.IsLimited(): 
 943                 replacement = self._min 
 946 ##            dbg('empty value; setting replacement:', replacement) 
 948         if replacement is None: 
 949             # Go get the integer portion about to be set and verify its validity 
 950             intstart, intend = self._fields[0]._extent 
 951 ##            dbg('intstart, intend:', intstart, intend) 
 952 ##            dbg('raw integer:"%s"' % value[intstart:intend]) 
 953             int = self._GetNumValue(value[intstart:intend]) 
 954             numval = self._fromGUI(value) 
 956 ##            dbg('integer: "%s"' % int) 
 958                 fracval = self.GetFraction(value) 
 959             except ValueError, e: 
 960 ##                dbg('Exception:', e, 'must be out of bounds; disallow value') 
 961                 self._disallowValue() 
 966 ##                dbg('self._isNeg?', self._isNeg) 
 967                 if int == '-' and self._oldvalue < 0 and not self._typedSign: 
 968 ##                    dbg('just a negative sign; old value < 0; setting replacement of 0') 
 971                 elif int[:2] == '-0' and self._fractionWidth == 0: 
 972                     if self._oldvalue < 0: 
 973 ##                        dbg('-0; setting replacement of 0') 
 976                     elif not self._limited or (self._min < -1 and self._max >= -1): 
 977 ##                        dbg('-0; setting replacement of -1') 
 981                         # limited and -1 is out of bounds 
 982                         self._disallowValue() 
 986                 elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0: 
 987                     if not self._limited or (self._min < -1 and self._max >= -1): 
 988 ##                        dbg('just a negative sign; setting replacement of -1') 
 991                         # limited and -1 is out of bounds 
 992                         self._disallowValue() 
 996                 elif( self._typedSign 
 997                       and int.find('-') != -1 
 999                       and not self._min <= numval <= self._max): 
1000                     # changed sign resulting in value that's now out-of-bounds; 
1002                     self._disallowValue() 
1006             if replacement is None: 
1007                 if int and int != '-': 
1011                         # integer requested is not legal.  This can happen if the user 
1012                         # is attempting to insert a digit in the middle of the control 
1013                         # resulting in something like "   3   45". Disallow such actions: 
1014 ##                        dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) 
1015                         if not wx.Validator_IsSilent(): 
1017                         sel_start, sel_to = self._GetSelection() 
1018 ##                        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) 
1019                         wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position 
1020                         wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1024                     if int[0] == '0' and len(int) > 1: 
1025 ##                        dbg('numvalue: "%s"' % numvalue.replace(' ', '')) 
1026                         if self._fractionWidth: 
1027                             value = self._toGUI(string.atof(numvalue)) 
1029                             value = self._toGUI(string.atol(numvalue)) 
1030 ##                        dbg('modified value: "%s"' % value) 
1032         self._typedSign = False     # reset state var 
1034         if replacement is not None: 
1035             # Value presented wasn't a legal number, but control should do something 
1036             # reasonable instead: 
1037 ##            dbg('setting replacement value:', replacement) 
1038             self._SetValue(self._toGUI(replacement)) 
1039             sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement)))   # find where it put the 1, so we can select it 
1040             sel_to = sel_start + len(str(abs(replacement))) 
1041 ##            dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) 
1042             wx.CallAfter(self.SetInsertionPoint, sel_start) 
1043             wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1047         # Otherwise, apply appropriate formatting to value: 
1049         # Because we're intercepting the value and adjusting it 
1050         # before a sign change is detected, we need to do this here: 
1051         if '-' in value or '(' in value: 
1056 ##        dbg('value:"%s"' % value, 'self._useParens:', self._useParens) 
1057         if self._fractionWidth: 
1058             adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) 
1060             adjvalue = self._adjustInt(self._GetNumValue(value)) 
1061 ##        dbg('adjusted value: "%s"' % adjvalue) 
1064         sel_start, sel_to = self._GetSelection()     # record current insertion point 
1065 ##        dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) 
1066         BaseMaskedTextCtrl._SetValue(self, adjvalue) 
1067         # After all actions so far scheduled, check that resulting cursor 
1068         # position is appropriate, and move if not: 
1069         wx.CallAfter(self._CheckInsertionPoint) 
1071 ##        dbg('finished NumCtrl::_SetValue', indent=0) 
1073     def _CheckInsertionPoint(self): 
1074         # If current insertion point is before the end of the integer and 
1075         # its before the 1st digit, place it just after the sign position: 
1076 ##        dbg('NumCtrl::CheckInsertionPoint', indent=1) 
1077         sel_start, sel_to = self._GetSelection() 
1078         text = self._GetValue() 
1079         if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): 
1080             text, signpos, right_signpos = self._getSignedValue() 
1081 ##            dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) 
1082             self.SetInsertionPoint(signpos+1) 
1083             self.SetSelection(signpos+1, signpos+1) 
1087     def _OnErase( self, event=None, just_return_value=False ): 
1089         This overrides the base control _OnErase, so that erasing around 
1090         grouping characters auto selects the digit before or after the 
1091         grouping character, so that the erasure does the right thing. 
1093 ##        dbg('NumCtrl::_OnErase', indent=1) 
1094         if event is None:   # called as action routine from Cut() operation. 
1097             key = event.GetKeyCode() 
1098         #if grouping digits, make sure deletes next to group char always 
1099         # delete next digit to appropriate side: 
1100         if self._groupDigits: 
1101             value = BaseMaskedTextCtrl.GetValue(self) 
1102             sel_start, sel_to = self._GetSelection() 
1104             if key == wx.WXK_BACK: 
1105                 # if 1st selected char is group char, select to previous digit 
1106                 if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: 
1107                     self.SetInsertionPoint(sel_start-1) 
1108                     self.SetSelection(sel_start-1, sel_to) 
1110                 # elif previous char is group char, select to previous digit 
1111                 elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: 
1112                     self.SetInsertionPoint(sel_start-2) 
1113                     self.SetSelection(sel_start-2, sel_to) 
1115             elif key == wx.WXK_DELETE: 
1116                 if( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1117                     and sel_start == sel_to 
1118                     and value[sel_to] == self._groupChar ): 
1119                     self.SetInsertionPoint(sel_start) 
1120                     self.SetSelection(sel_start, sel_to+2) 
1122                 elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1123                            and value[sel_start:sel_to] == self._groupChar ): 
1124                     self.SetInsertionPoint(sel_start) 
1125                     self.SetSelection(sel_start, sel_to+1) 
1127         return BaseMaskedTextCtrl._OnErase(self, event, just_return_value) 
1130     def OnTextChange( self, event ): 
1132         Handles an event indicating that the text control's value 
1133         has changed, and issue EVT_NUM event. 
1134         NOTE: using wxTextCtrl.SetValue() to change the control's 
1135         contents from within a EVT_CHAR handler can cause double 
1136         text events.  So we check for actual changes to the text 
1137         before passing the events on. 
1139 ##        dbg('NumCtrl::OnTextChange', indent=1) 
1140         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1144         # else... legal value 
1146         value = self.GetValue() 
1147         if value != self._oldvalue: 
1149                 self.GetEventHandler().ProcessEvent( 
1150                     NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) 
1154             # let normal processing of the text continue 
1156         self._oldvalue = value # record for next event 
1159     def _GetValue(self): 
1161         Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the 
1162         control with this function. 
1164         return wx.TextCtrl.GetValue(self) 
1169         Returns the current numeric value of the control. 
1171         return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) 
1173     def SetValue(self, value): 
1175         Sets the value of the control to the value specified. 
1176         The resulting actual value of the control may be altered to 
1177         conform with the bounds set on the control if limited, 
1178         or colored if not limited but the value is out-of-bounds. 
1179         A ValueError exception will be raised if an invalid value 
1182 ##        dbg('NumCtrl::SetValue(%s)' % value, indent=1) 
1183         BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) 
1187     def SetIntegerWidth(self, value): 
1188         self.SetParameters(integerWidth=value) 
1189     def GetIntegerWidth(self): 
1190         return self._integerWidth 
1192     def SetFractionWidth(self, value): 
1193         self.SetParameters(fractionWidth=value) 
1194     def GetFractionWidth(self): 
1195         return self._fractionWidth 
1199     def SetMin(self, min=None): 
1201         Sets the minimum value of the control.  If a value of None 
1202         is provided, then the control will have no explicit minimum value. 
1203         If the value specified is greater than the current maximum value, 
1204         then the function returns False and the minimum will not change from 
1205         its current setting.  On success, the function returns True. 
1207         If successful and the current value is lower than the new lower 
1208         bound, if the control is limited, the value will be automatically 
1209         adjusted to the new minimum value; if not limited, the value in the 
1210         control will be colored as invalid. 
1212         If min > the max value allowed by the width of the control, 
1213         the function will return False, and the min will not be set. 
1215 ##        dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1) 
1216         if( self._max is None 
1218             or (self._max is not None and self._max >= min) ): 
1220                 self.SetParameters(min=min) 
1231         Gets the lower bound value of the control.  It will return 
1232         None if not specified. 
1237     def SetMax(self, max=None): 
1239         Sets the maximum value of the control. If a value of None 
1240         is provided, then the control will have no explicit maximum value. 
1241         If the value specified is less than the current minimum value, then 
1242         the function returns False and the maximum will not change from its 
1243         current setting. On success, the function returns True. 
1245         If successful and the current value is greater than the new upper 
1246         bound, if the control is limited the value will be automatically 
1247         adjusted to this maximum value; if not limited, the value in the 
1248         control will be colored as invalid. 
1250         If max > the max value allowed by the width of the control, 
1251         the function will return False, and the max will not be set. 
1253         if( self._min is None 
1255             or (self._min is not None and self._min <= max) ): 
1257                 self.SetParameters(max=max) 
1269         Gets the maximum value of the control.  It will return the current 
1270         maximum integer, or None if not specified. 
1275     def SetBounds(self, min=None, max=None): 
1277         This function is a convenience function for setting the min and max 
1278         values at the same time.  The function only applies the maximum bound 
1279         if setting the minimum bound is successful, and returns True 
1280         only if both operations succeed. 
1281         NOTE: leaving out an argument will remove the corresponding bound. 
1283         ret = self.SetMin(min) 
1284         return ret and self.SetMax(max) 
1287     def GetBounds(self): 
1289         This function returns a two-tuple (min,max), indicating the 
1290         current bounds of the control.  Each value can be None if 
1291         that bound is not set. 
1293         return (self._min, self._max) 
1296     def SetLimited(self, limited): 
1298         If called with a value of True, this function will cause the control 
1299         to limit the value to fall within the bounds currently specified. 
1300         If the control's value currently exceeds the bounds, it will then 
1301         be limited accordingly. 
1303         If called with a value of False, this function will disable value 
1304         limiting, but coloring of out-of-bounds values will still take 
1305         place if bounds have been set for the control. 
1307         self.SetParameters(limited = limited) 
1310     def IsLimited(self): 
1312         Returns True if the control is currently limiting the 
1313         value to fall within the current bounds. 
1315         return self._limited 
1317     def GetLimited(self): 
1318         """ (For regularization of property accessors) """ 
1319         return self.IsLimited 
1322     def IsInBounds(self, value=None): 
1324         Returns True if no value is specified and the current value 
1325         of the control falls within the current bounds.  This function can 
1326         also be called with a value to see if that value would fall within 
1327         the current bounds of the given control. 
1329 ##        dbg('IsInBounds(%s)' % repr(value), indent=1) 
1331             value = self.GetValue() 
1334                 value = self._GetNumValue(self._toGUI(value)) 
1335             except ValueError, e: 
1336 ##                dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) 
1338             if value.strip() == '': 
1340             elif self._fractionWidth: 
1341                 value = float(value) 
1347         if min is None: min = value 
1348         if max is None: max = value 
1350         # if bounds set, and value is None, return False 
1351         if value == None and (min is not None or max is not None): 
1352 ##            dbg('finished IsInBounds', indent=0) 
1355 ##            dbg('finished IsInBounds', indent=0) 
1356             return min <= value <= max 
1359     def SetAllowNone(self, allow_none): 
1361         Change the behavior of the validation code, allowing control 
1362         to have a value of None or not, as appropriate.  If the value 
1363         of the control is currently None, and allow_none is False, the 
1364         value of the control will be set to the minimum value of the 
1365         control, or 0 if no lower bound is set. 
1367         self._allowNone = allow_none 
1368         if not allow_none and self.GetValue() is None: 
1370             if min is not None: self.SetValue(min) 
1371             else:               self.SetValue(0) 
1374     def IsNoneAllowed(self): 
1375         return self._allowNone 
1376     def GetAllowNone(self): 
1377         """ (For regularization of property accessors) """ 
1378         return self.IsNoneAllowed() 
1380     def SetAllowNegative(self, value): 
1381         self.SetParameters(allowNegative=value) 
1382     def IsNegativeAllowed(self): 
1383         return self._allowNegative 
1384     def GetAllowNegative(self): 
1385         """ (For regularization of property accessors) """ 
1386         return self.IsNegativeAllowed() 
1388     def SetGroupDigits(self, value): 
1389         self.SetParameters(groupDigits=value) 
1390     def IsGroupingAllowed(self): 
1391         return self._groupDigits 
1392     def GetGroupDigits(self): 
1393         """ (For regularization of property accessors) """ 
1394         return self.IsGroupingAllowed() 
1396     def SetGroupChar(self, value): 
1397         self.SetParameters(groupChar=value) 
1398     def GetGroupChar(self): 
1399         return self._groupChar 
1401     def SetDecimalChar(self, value): 
1402         self.SetParameters(decimalChar=value) 
1403     def GetDecimalChar(self): 
1404         return self._decimalChar 
1406     def SetSelectOnEntry(self, value): 
1407         self.SetParameters(selectOnEntry=value) 
1408     def GetSelectOnEntry(self): 
1409         return self._selectOnEntry 
1411     def SetAutoSize(self, value): 
1412         self.SetParameters(autoSize=value) 
1413     def GetAutoSize(self): 
1414         return self._autoSize 
1417     # (Other parameter accessors are inherited from base class) 
1420     def _toGUI( self, value, apply_limits = True ): 
1422         Conversion function used to set the value of the control; does 
1423         type and bounds checking and raises ValueError if argument is 
1426 ##        dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1) 
1427         if value is None and self.IsNoneAllowed(): 
1429             return self._template 
1431         elif type(value) in (types.StringType, types.UnicodeType): 
1432             value = self._GetNumValue(value) 
1433 ##            dbg('cleansed num value: "%s"' % value) 
1435                 if self.IsNoneAllowed(): 
1437                     return self._template 
1439 ##                    dbg('exception raised:', e, indent=0) 
1440                     raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1443                 if self._fractionWidth or value.find('.') != -1: 
1444                     value = float(value) 
1447             except Exception, e: 
1448 ##                dbg('exception raised:', e, indent=0) 
1449                 raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1451         elif type(value) not in (types.IntType, types.LongType, types.FloatType): 
1454                 'NumCtrl requires numeric value, passed %s'% repr(value) ) 
1456         if not self._allowNegative and value < 0: 
1458                 'control configured to disallow negative values, passed %s'% repr(value) ) 
1460         if self.IsLimited() and apply_limits: 
1463             if not min is None and value < min: 
1466                     'value %d is below minimum value of control'% value ) 
1467             if not max is None and value > max: 
1470                     'value %d exceeds value of control'% value ) 
1472         adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) 
1473 ##        dbg('len(%s):' % self._mask, len(self._mask)) 
1474 ##        dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) 
1475 ##        dbg('adjustwidth:', adjustwidth) 
1476         if self._fractionWidth == 0: 
1477             s = str(long(value)).rjust(self._integerWidth) 
1479             format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) 
1480             s = format % float(value) 
1481 ##        dbg('s:"%s"' % s, 'len(s):', len(s)) 
1482         if len(s) > (adjustwidth - self._groupSpace): 
1484             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1485         elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): 
1487             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1489         s = s.rjust(adjustwidth).replace('.', self._decimalChar) 
1490         if self._signOk and self._useParens: 
1491             if s.find('-') != -1: 
1492                 s = s.replace('-', '(') + ')' 
1495 ##        dbg('returned: "%s"' % s, indent=0) 
1499     def _fromGUI( self, value ): 
1501         Conversion function used in getting the value of the control. 
1504 ##        dbg('NumCtrl::_fromGUI(%s)' % value, indent=1) 
1505         # One or more of the underlying text control implementations 
1506         # issue an intermediate EVT_TEXT when replacing the control's 
1507         # value, where the intermediate value is an empty string. 
1508         # So, to ensure consistency and to prevent spurious ValueErrors, 
1509         # we make the following test, and react accordingly: 
1511         if value.strip() == '': 
1512             if not self.IsNoneAllowed(): 
1513 ##                dbg('empty value; not allowed,returning 0', indent = 0) 
1514                 if self._fractionWidth: 
1519 ##                dbg('empty value; returning None', indent = 0) 
1522             value = self._GetNumValue(value) 
1523 ##            dbg('Num value: "%s"' % value) 
1524             if self._fractionWidth: 
1527                     return float( value ) 
1529 ##                    dbg("couldn't convert to float; returning None") 
1540                        return long( value ) 
1542 ##                       dbg("couldn't convert to long; returning None") 
1548 ##                    dbg('exception occurred; returning None') 
1552     def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): 
1554         Preprocessor for base control paste; if value needs to be right-justified 
1555         to fit in control, do so prior to paste: 
1557 ##        dbg('NumCtrl::_Paste (value = "%s")' % value, indent=1) 
1559             paste_text = self._getClipboardContents() 
1562         sel_start, sel_to = self._GetSelection() 
1563         orig_sel_start = sel_start 
1564         orig_sel_to = sel_to 
1565 ##        dbg('selection:', (sel_start, sel_to)) 
1566         old_value = self._GetValue() 
1569         field = self._FindField(sel_start) 
1570         edit_start, edit_end = field._extent 
1571         paste_text = paste_text.replace(self._groupChar, '').replace('(', '-').replace(')','') 
1572         if field._insertRight and self._groupDigits: 
1573             # want to paste to the left; see if it will fit: 
1574             left_text = old_value[edit_start:sel_start].lstrip() 
1575 ##            dbg('len(left_text):', len(left_text)) 
1576 ##            dbg('len(paste_text):', len(paste_text)) 
1577 ##            dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
1578             if sel_start - (len(left_text) + len(paste_text)) >= edit_start: 
1579                 # will fit! create effective paste text, and move cursor back to do so: 
1580                 paste_text = left_text + paste_text 
1581                 sel_start -= len(paste_text) 
1582                 sel_start += sel_to - orig_sel_start    # decrease by amount selected 
1584 ##                dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
1585 ##                dbg('adjusted start before accounting for grouping:', sel_start) 
1586 ##                dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text) 
1588             if self._groupDigits and sel_start != orig_sel_start: 
1589                 left_len = len(old_value[:sel_to].lstrip()) 
1590                 # remove group chars from adjusted paste string, and left pad to wipe out 
1591                 # old characters, so that selection will remove the right chars, and 
1592                 # readjust will do the right thing: 
1593                 paste_text = paste_text.replace(self._groupChar,'') 
1594                 adjcount = left_len - len(paste_text) 
1595                 paste_text = ' ' * adjcount + paste_text 
1596                 sel_start = sel_to - len(paste_text) 
1597 ##                dbg('adjusted start after accounting for grouping:', sel_start) 
1598 ##                dbg('adjusted paste_text after accounting for grouping: "%s"' % paste_text) 
1599             self.SetInsertionPoint(sel_to) 
1600             self.SetSelection(sel_start, sel_to) 
1602         new_text, replace_to = MaskedEditMixin._Paste(self, 
1604                                         raise_on_invalid=raise_on_invalid, 
1605                                         just_return_value=True) 
1606         self._SetInsertionPoint(orig_sel_to) 
1607         self._SetSelection(orig_sel_start, orig_sel_to) 
1608         if not just_return_value and new_text is not None: 
1609             if new_text != self._GetValue(): 
1610                     self.modified = True 
1614                 wx.CallAfter(self._SetValue, new_text) 
1615                 wx.CallAfter(self._SetInsertionPoint, replace_to) 
1619             return new_text, replace_to 
1621     def _Undo(self, value=None, prev=None): 
1622         '''numctrl
's undo is more complicated than the base control's
, due to
 
1623         grouping characters
; we don
't want to consider them when calculating 
1624         the undone portion.''' 
1625 ##        dbg('NumCtrl
::_Undo
', indent=1) 
1626         if value is None: value = self._GetValue() 
1627         if prev is None: prev = self._prevValue 
1628         if not self._groupDigits: 
1629             ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, value, prev, just_return_results = True) 
1630             self._SetValue(prev) 
1631             self._SetInsertionPoint(new_sel_start) 
1632             self._SetSelection(new_sel_start, new_sel_to) 
1633             self._prevSelection = (new_sel_start, new_sel_to) 
1634 ##            dbg('resetting 
"prev selection" to
', self._prevSelection) 
1638         sel_start, sel_to = self._prevSelection 
1639         edit_start, edit_end = self._FindFieldExtent(0) 
1641         adjvalue = self._GetNumValue(value).rjust(self._masklength) 
1642         adjprev  = self._GetNumValue(prev ).rjust(self._masklength) 
1644         # move selection to account for "ungrouped" value: 
1645         left_text = value[sel_start:].lstrip() 
1646         numleftgroups = len(left_text) - len(left_text.replace(self._groupChar, '')) 
1647         adjsel_start = sel_start + numleftgroups 
1648         right_text = value[sel_to:].lstrip() 
1649         numrightgroups = len(right_text) - len(right_text.replace(self._groupChar, '')) 
1650         adjsel_to = sel_to + numrightgroups 
1651 ##        dbg('adjusting 
"previous" selection 
from', (sel_start, sel_to), 'to
:', (adjsel_start, adjsel_to)) 
1652         self._prevSelection = (adjsel_start, adjsel_to) 
1654         # determine appropriate selection for ungrouped undo 
1655         ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, adjvalue, adjprev, just_return_results = True) 
1657         # adjust new selection based on grouping: 
1658         left_len = edit_end - new_sel_start 
1659         numleftgroups = left_len / 3 
1660         new_sel_start -= numleftgroups 
1661         if numleftgroups and left_len % 3 == 0: 
1664         if new_sel_start < self._masklength and prev[new_sel_start] == self._groupChar: 
1667         right_len = edit_end - new_sel_to 
1668         numrightgroups = right_len / 3 
1669         new_sel_to -= numrightgroups 
1671         if new_sel_to and prev[new_sel_to-1] == self._groupChar: 
1674         if new_sel_start > new_sel_to: 
1675             new_sel_to = new_sel_start 
1677         # for numbers, we don't care about leading whitespace
; adjust selection 
if 
1678         # it includes leading space. 
1679         prev_stripped 
= prev
.lstrip() 
1680         prev_start 
= self
._masklength 
- len(prev_stripped
) 
1681         if new_sel_start 
< prev_start
: 
1682             new_sel_start 
= prev_start
 
1684 ##        dbg('adjusted selection accounting for grouping:', (new_sel_start, new_sel_to)) 
1685         self
._SetValue
(prev
) 
1686         self
._SetInsertionPoint
(new_sel_start
) 
1687         self
._SetSelection
(new_sel_start
, new_sel_to
) 
1688         self
._prevSelection 
= (new_sel_start
, new_sel_to
) 
1689 ##        dbg('resetting "prev selection" to', self._prevSelection) 
1692 #=========================================================================== 
1694 if __name__ 
== '__main__': 
1698     class myDialog(wx
.Dialog
): 
1699         def __init__(self
, parent
, id, title
, 
1700             pos 
= wx
.DefaultPosition
, size 
= wx
.DefaultSize
, 
1701             style 
= wx
.DEFAULT_DIALOG_STYLE 
): 
1702             wx
.Dialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
) 
1704             self
.int_ctrl 
= NumCtrl(self
, wx
.NewId(), size
=(55,20)) 
1705             self
.OK 
= wx
.Button( self
, wx
.ID_OK
, "OK") 
1706             self
.Cancel 
= wx
.Button( self
, wx
.ID_CANCEL
, "Cancel") 
1708             vs 
= wx
.BoxSizer( wx
.VERTICAL 
) 
1709             vs
.Add( self
.int_ctrl
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1710             hs 
= wx
.BoxSizer( wx
.HORIZONTAL 
) 
1711             hs
.Add( self
.OK
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1712             hs
.Add( self
.Cancel
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1713             vs
.Add(hs
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1715             self
.SetAutoLayout( True ) 
1718             vs
.SetSizeHints( self 
) 
1719             self
.Bind(EVT_NUM
, self
.OnChange
, self
.int_ctrl
) 
1721         def OnChange(self
, event
): 
1722             print 'value now', event
.GetValue() 
1724     class TestApp(wx
.App
): 
1727                 self
.frame 
= wx
.Frame(None, -1, "Test", (20,20), (120,100)  ) 
1728                 self
.panel 
= wx
.Panel(self
.frame
, -1) 
1729                 button 
= wx
.Button(self
.panel
, -1, "Push Me", (20, 20)) 
1730                 self
.Bind(wx
.EVT_BUTTON
, self
.OnClick
, button
) 
1732                 traceback
.print_exc() 
1736         def OnClick(self
, event
): 
1737             dlg 
= myDialog(self
.panel
, -1, "test NumCtrl") 
1738             dlg
.int_ctrl
.SetValue(501) 
1739             dlg
.int_ctrl
.SetInsertionPoint(1) 
1740             dlg
.int_ctrl
.SetSelection(1,2) 
1741             rc 
= dlg
.ShowModal() 
1742             print 'final value', dlg
.int_ctrl
.GetValue() 
1744             self
.frame
.Destroy() 
1747             self
.frame
.Show(True) 
1754         traceback
.print_exc() 
1758 ## =============================## 
1759 ##   1. Add support for printf-style format specification. 
1760 ##   2. Add option for repositioning on 'illegal' insertion point. 
1763 ##   1. Allowed select/replace digits. 
1764 ##   2. Fixed undo to ignore grouping chars. 
1767 ##   1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. 
1768 ##   2. Added autoSize parameter, to allow manual sizing of the control. 
1769 ##   3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of 
1770 ##      nonsensical parameter methods from the control, so it will work 
1771 ##      properly with Boa. 
1772 ##   4. Fixed allowNone bug found by user sameerc1@grandecom.net