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 
 681             old_numvalue = self._GetNumValue(self._GetValue()) 
 683             if kwargs.has_key('groupChar'): 
 684                 maskededit_kwargs['groupChar'] = kwargs['groupChar'] 
 685                 groupchar = kwargs['groupChar'] 
 686             if kwargs.has_key('decimalChar'): 
 687                 maskededit_kwargs['decimalChar'] = kwargs['decimalChar'] 
 688                 decimalchar = kwargs['decimalChar'] 
 690             # Add sanity check to make sure these are distinct, and if not, 
 691             # raise attribute error 
 692             if groupchar == decimalchar: 
 693                 raise AttributeError('groupChar and decimalChar must be distinct') 
 696         # for all other parameters, assign keyword args as appropriate: 
 697         for key, param_value in kwargs.items(): 
 698             key = key.replace('Color', 'Colour') 
 699             if key not in NumCtrl.valid_ctrl_params.keys(): 
 700                 raise AttributeError('invalid keyword argument "%s"' % key) 
 701             elif key not in MaskedEditMixin.valid_ctrl_params.keys(): 
 702                 setattr(self, '_' + key, param_value) 
 703             elif key in ('mask', 'autoformat'): # disallow explicit setting of mask 
 704                 raise AttributeError('invalid keyword argument "%s"' % key) 
 706                 maskededit_kwargs[key] = param_value 
 707 ##        dbg('kwargs:', kwargs) 
 709         # reprocess existing format codes to ensure proper resulting format: 
 710         formatcodes = self.GetCtrlParameter('formatcodes') 
 711         if kwargs.has_key('allowNegative'): 
 712             if kwargs['allowNegative'] and '-' not in formatcodes: 
 714                 maskededit_kwargs['formatcodes'] = formatcodes 
 715             elif not kwargs['allowNegative'] and '-' in formatcodes: 
 716                 formatcodes = formatcodes.replace('-','') 
 717                 maskededit_kwargs['formatcodes'] = formatcodes 
 719         if kwargs.has_key('groupDigits'): 
 720             if kwargs['groupDigits'] and ',' not in formatcodes: 
 722                 maskededit_kwargs['formatcodes'] = formatcodes 
 723             elif not kwargs['groupDigits'] and ',' in formatcodes: 
 724                 formatcodes = formatcodes.replace(',','') 
 725                 maskededit_kwargs['formatcodes'] = formatcodes 
 727         if kwargs.has_key('selectOnEntry'): 
 728             self._selectOnEntry = kwargs['selectOnEntry'] 
 729 ##            dbg("kwargs['selectOnEntry']?", kwargs['selectOnEntry'], "'S' in formatcodes?", 'S' in formatcodes) 
 730             if kwargs['selectOnEntry'] and 'S' not in formatcodes: 
 732                 maskededit_kwargs['formatcodes'] = formatcodes 
 733             elif not kwargs['selectOnEntry'] and 'S' in formatcodes: 
 734                 formatcodes = formatcodes.replace('S','') 
 735                 maskededit_kwargs['formatcodes'] = formatcodes 
 737         if kwargs.has_key('autoSize'): 
 738             self._autoSize = kwargs['autoSize'] 
 739             if kwargs['autoSize'] and 'F' not in formatcodes: 
 741                 maskededit_kwargs['formatcodes'] = formatcodes 
 742             elif not kwargs['autoSize'] and 'F' in formatcodes: 
 743                 formatcodes = formatcodes.replace('F', '') 
 744                 maskededit_kwargs['formatcodes'] = formatcodes 
 747         if 'r' in formatcodes and self._fractionWidth: 
 748             # top-level mask should only be right insert if no fractional 
 749             # part will be shown; ie. if reconfiguring control, remove 
 750             # previous "global" setting. 
 751             formatcodes = formatcodes.replace('r', '') 
 752             maskededit_kwargs['formatcodes'] = formatcodes 
 755         if kwargs.has_key('limited'): 
 756             if kwargs['limited'] and not self._limited: 
 757                 maskededit_kwargs['validRequired'] = True 
 758             elif not kwargs['limited'] and self._limited: 
 759                 maskededit_kwargs['validRequired'] = False 
 760             self._limited = kwargs['limited'] 
 762 ##        dbg('maskededit_kwargs:', maskededit_kwargs) 
 763         if maskededit_kwargs.keys(): 
 764             self.SetCtrlParameters(**maskededit_kwargs) 
 766         # Go ensure all the format codes necessary are present: 
 767         orig_intformat = intformat = self.GetFieldParameter(0, 'formatcodes') 
 768         if 'r' not in intformat: 
 770         if '>' not in intformat: 
 772         if intformat != orig_intformat: 
 773             if self._fractionWidth: 
 774                 self.SetFieldParameters(0, formatcodes=intformat) 
 776                 self.SetCtrlParameters(formatcodes=intformat) 
 778         # Record end of integer and place cursor there unless selecting, or select entire field: 
 779         integerStart, integerEnd = self._fields[0]._extent 
 780         if not self._fields[0]._selectOnFieldEntry: 
 781             self.SetInsertionPoint(0) 
 782             self.SetInsertionPoint(integerEnd) 
 783             self.SetSelection(integerEnd, integerEnd) 
 785             self.SetInsertionPoint(0)   # include any sign 
 786             self.SetSelection(0, integerEnd) 
 789         # Set min and max as appropriate: 
 790         if kwargs.has_key('min'): 
 792             if( self._max is None 
 794                 or (self._max is not None and self._max >= min) ): 
 795 ##                dbg('examining min') 
 798                         textmin = self._toGUI(min, apply_limits = False) 
 800 ##                        dbg('min will not fit into control; ignoring', indent=0) 
 802 ##                dbg('accepted min') 
 805 ##                dbg('ignoring min') 
 809         if kwargs.has_key('max'): 
 811             if( self._min is None 
 813                 or (self._min is not None and self._min <= max) ): 
 814 ##                dbg('examining max') 
 817                         textmax = self._toGUI(max, apply_limits = False) 
 819 ##                        dbg('max will not fit into control; ignoring', indent=0) 
 821 ##                dbg('accepted max') 
 824 ##                dbg('ignoring max') 
 827         if kwargs.has_key('allowNegative'): 
 828             self._allowNegative = kwargs['allowNegative'] 
 830         # Ensure current value of control obeys any new restrictions imposed: 
 831         text = self._GetValue() 
 832 ##        dbg('text value: "%s"' % text) 
 833         if kwargs.has_key('groupChar') and self._groupChar != old_groupchar and text.find(old_groupchar) != -1: 
 835 ##            dbg('old_groupchar: "%s" newgroupchar: "%s"' % (old_groupchar, self._groupChar)) 
 836         if kwargs.has_key('decimalChar') and self._decimalChar != old_decimalchar and text.find(old_decimalchar) != -1: 
 839         if text != self._GetValue(): 
 840             if self._decimalChar != '.': 
 841                 # ensure latest decimal char is in "numeric value" so it won't be removed 
 842                 # when going to the GUI: 
 843                 text = text.replace('.', self._decimalChar) 
 844             newtext = self._toGUI(text) 
 845 ##            dbg('calling wx.TextCtrl.SetValue(self, %s)' % newtext) 
 846             wx.TextCtrl.SetValue(self, newtext) 
 848         value = self.GetValue() 
 850 ##        dbg('self._allowNegative?', self._allowNegative) 
 851         if not self._allowNegative and self._isNeg: 
 853 ##            dbg('abs(value):', value) 
 856         elif not self._allowNone and BaseMaskedTextCtrl.GetValue(self) == '': 
 862         sel_start, sel_to = self.GetSelection() 
 863         if self.IsLimited() and self._min is not None and value < self._min: 
 864 ##            dbg('Set to min value:', self._min) 
 865             self._SetValue(self._toGUI(self._min)) 
 867         elif self.IsLimited() and self._max is not None and value > self._max: 
 868 ##            dbg('Setting to max value:', self._max) 
 869             self._SetValue(self._toGUI(self._max)) 
 871             # reformat current value as appropriate to possibly new conditions 
 872 ##            dbg('Reformatting value:', value) 
 873             sel_start, sel_to = self.GetSelection() 
 874             self._SetValue(self._toGUI(value)) 
 875         self.Refresh() # recolor as appropriate 
 876 ##        dbg('finished NumCtrl::SetParameters', indent=0) 
 880     def _GetNumValue(self, value): 
 882         This function attempts to "clean up" a text value, providing a regularized 
 883         convertable string, via atol() or atof(), for any well-formed numeric text value. 
 885         return value.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','').strip() 
 888     def GetFraction(self, candidate=None): 
 890         Returns the fractional portion of the value as a float.  If there is no 
 891         fractional portion, the value returned will be 0.0. 
 893         if not self._fractionWidth: 
 896             fracstart, fracend = self._fields[1]._extent 
 897             if candidate is None: 
 898                 value = self._toGUI(BaseMaskedTextCtrl.GetValue(self)) 
 900                 value = self._toGUI(candidate) 
 901             fracstring = value[fracstart:fracend].strip() 
 905                 return string.atof(fracstring) 
 907     def _OnChangeSign(self, event): 
 908 ##        dbg('NumCtrl::_OnChangeSign', indent=1) 
 909         self._typedSign = True 
 910         MaskedEditMixin._OnChangeSign(self, event) 
 914     def _disallowValue(self): 
 915 ##        dbg('NumCtrl::_disallowValue') 
 916         # limited and -1 is out of bounds 
 919         if not wx.Validator_IsSilent(): 
 921         sel_start, sel_to = self._GetSelection() 
 922 ##        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) 
 923         wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position 
 924         wx.CallAfter(self.SetSelection, sel_start, sel_to) 
 926     def _SetValue(self, value): 
 928         This routine supersedes the base masked control _SetValue().  It is 
 929         needed to ensure that the value of the control is always representable/convertable 
 930         to a numeric return value (via GetValue().)  This routine also handles 
 931         automatic adjustment and grouping of the value without explicit intervention 
 935 ##        dbg('NumCtrl::_SetValue("%s")' % value, indent=1) 
 937         if( (self._fractionWidth and value.find(self._decimalChar) == -1) or 
 938             (self._fractionWidth == 0 and value.find(self._decimalChar) != -1) ) : 
 939             value = self._toGUI(value) 
 941         numvalue = self._GetNumValue(value) 
 942 ##        dbg('cleansed value: "%s"' % numvalue) 
 947 ##                dbg('calling base BaseMaskedTextCtrl._SetValue(self, "%s")' % value) 
 948                 BaseMaskedTextCtrl._SetValue(self, value) 
 952             elif self._min > 0 and self.IsLimited(): 
 953                 replacement = self._min 
 956 ##            dbg('empty value; setting replacement:', replacement) 
 958         if replacement is None: 
 959             # Go get the integer portion about to be set and verify its validity 
 960             intstart, intend = self._fields[0]._extent 
 961 ##            dbg('intstart, intend:', intstart, intend) 
 962 ##            dbg('raw integer:"%s"' % value[intstart:intend]) 
 963             int = self._GetNumValue(value[intstart:intend]) 
 964             numval = self._fromGUI(value) 
 966 ##            dbg('integer: "%s"' % int) 
 968                 # if a float value, this will implicitly verify against limits, 
 969                 # and generate an exception if out-of-bounds and limited 
 970                 # if not a float, it will just return 0.0, and we therefore 
 971                 # have to test against the limits explicitly after testing 
 972                 # special cases for handling -0 and empty controls... 
 973                 fracval = self.GetFraction(value) 
 974             except ValueError, e: 
 975 ##                dbg('Exception:', e, 'must be out of bounds; disallow value') 
 976                 self._disallowValue() 
 980             if fracval == 0.0: # (can be 0 for floats as well as integers) 
 981                 # we have to do special testing to account for emptying controls, or -0 
 982                 # and/or just leaving the sign character or changing the sign, 
 983                 # so we can do appropriate things to the value of the control, 
 984                 # we can't just immediately test to see if the value is valid 
 985                 # If all of these special cases are not in play, THEN we can do  
 986                 # a limits check and see if the value is otherwise ok... 
 988 ##                dbg('self._isNeg?', self._isNeg) 
 989                 if int == '-' and self._oldvalue < 0 and not self._typedSign: 
 990 ##                    dbg('just a negative sign; old value < 0; setting replacement of 0') 
 993                 elif int[:2] == '-0':  
 994                     if self._oldvalue < 0: 
 995 ##                        dbg('-0; setting replacement of 0') 
 998                     elif not self._limited or (self._min < -1 and self._max >= -1): 
 999 ##                        dbg('-0; setting replacement of -1') 
1003                         # limited and -1 is out of bounds 
1004                         self._disallowValue() 
1008                 elif int == '-' and (self._oldvalue >= 0 or self._typedSign): 
1009                     if not self._limited or (self._min < -1 and self._max >= -1): 
1010 ##                        dbg('just a negative sign; setting replacement of -1') 
1013                         # limited and -1 is out of bounds 
1014                         self._disallowValue() 
1018                 elif( self._typedSign 
1019                       and int.find('-') != -1 
1021                       and not self._min <= numval <= self._max): 
1022                     # changed sign resulting in value that's now out-of-bounds; 
1024                     self._disallowValue() 
1028             if replacement is None: 
1029                 if int and int != '-': 
1033                         # integer requested is not legal.  This can happen if the user 
1034                         # is attempting to insert a digit in the middle of the control 
1035                         # resulting in something like "   3   45". Disallow such actions: 
1036 ##                        dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) 
1037                         if not wx.Validator_IsSilent(): 
1039                         sel_start, sel_to = self._GetSelection() 
1040 ##                        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) 
1041                         wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position 
1042                         wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1046 ##                    dbg('numvalue: "%s"' % numvalue.replace(' ', '')) 
1047                     # finally, (potentially re) verify that numvalue will pass any limits imposed: 
1049                         if self._fractionWidth: 
1050                             value = self._toGUI(string.atof(numvalue)) 
1052                             value = self._toGUI(string.atol(numvalue)) 
1053                     except ValueError, e: 
1054 ##                        dbg('Exception:', e, 'must be out of bounds; disallow value') 
1055                         self._disallowValue() 
1059 ##                        dbg('modified value: "%s"' % value) 
1062         self._typedSign = False     # reset state var 
1064         if replacement is not None: 
1065             # Value presented wasn't a legal number, but control should do something 
1066             # reasonable instead: 
1067 ##            dbg('setting replacement value:', replacement) 
1068             self._SetValue(self._toGUI(replacement)) 
1069             sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement)))   # find where it put the 1, so we can select it 
1070             sel_to = sel_start + len(str(abs(replacement))) 
1071 ##            dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) 
1072             wx.CallAfter(self.SetInsertionPoint, sel_start) 
1073             wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1077         # Otherwise, apply appropriate formatting to value: 
1079         # Because we're intercepting the value and adjusting it 
1080         # before a sign change is detected, we need to do this here: 
1081         if '-' in value or '(' in value: 
1086 ##        dbg('value:"%s"' % value, 'self._useParens:', self._useParens) 
1087         if self._fractionWidth: 
1088             adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) 
1090             adjvalue = self._adjustInt(self._GetNumValue(value)) 
1091 ##        dbg('adjusted value: "%s"' % adjvalue) 
1094         sel_start, sel_to = self._GetSelection()     # record current insertion point 
1095 ##        dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) 
1096         BaseMaskedTextCtrl._SetValue(self, adjvalue) 
1097         # After all actions so far scheduled, check that resulting cursor 
1098         # position is appropriate, and move if not: 
1099         wx.CallAfter(self._CheckInsertionPoint) 
1101 ##        dbg('finished NumCtrl::_SetValue', indent=0) 
1103     def _CheckInsertionPoint(self): 
1104         # If current insertion point is before the end of the integer and 
1105         # its before the 1st digit, place it just after the sign position: 
1106 ##        dbg('NumCtrl::CheckInsertionPoint', indent=1) 
1107         sel_start, sel_to = self._GetSelection() 
1108         text = self._GetValue() 
1109         if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): 
1110             text, signpos, right_signpos = self._getSignedValue() 
1111 ##            dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) 
1112             self.SetInsertionPoint(signpos+1) 
1113             self.SetSelection(signpos+1, signpos+1) 
1117     def _OnErase( self, event=None, just_return_value=False ): 
1119         This overrides the base control _OnErase, so that erasing around 
1120         grouping characters auto selects the digit before or after the 
1121         grouping character, so that the erasure does the right thing. 
1123 ##        dbg('NumCtrl::_OnErase', indent=1) 
1124         if event is None:   # called as action routine from Cut() operation. 
1127             key = event.GetKeyCode() 
1128         #if grouping digits, make sure deletes next to group char always 
1129         # delete next digit to appropriate side: 
1130         if self._groupDigits: 
1131             value = BaseMaskedTextCtrl.GetValue(self) 
1132             sel_start, sel_to = self._GetSelection() 
1134             if key == wx.WXK_BACK: 
1135                 # if 1st selected char is group char, select to previous digit 
1136                 if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: 
1137                     self.SetInsertionPoint(sel_start-1) 
1138                     self.SetSelection(sel_start-1, sel_to) 
1140                 # elif previous char is group char, select to previous digit 
1141                 elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: 
1142                     self.SetInsertionPoint(sel_start-2) 
1143                     self.SetSelection(sel_start-2, sel_to) 
1145             elif key == wx.WXK_DELETE: 
1146                 if( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1147                     and sel_start == sel_to 
1148                     and value[sel_to] == self._groupChar ): 
1149                     self.SetInsertionPoint(sel_start) 
1150                     self.SetSelection(sel_start, sel_to+2) 
1152                 elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1153                            and value[sel_start:sel_to] == self._groupChar ): 
1154                     self.SetInsertionPoint(sel_start) 
1155                     self.SetSelection(sel_start, sel_to+1) 
1157         return BaseMaskedTextCtrl._OnErase(self, event, just_return_value) 
1160     def OnTextChange( self, event ): 
1162         Handles an event indicating that the text control's value 
1163         has changed, and issue EVT_NUM event. 
1164         NOTE: using wxTextCtrl.SetValue() to change the control's 
1165         contents from within a EVT_CHAR handler can cause double 
1166         text events.  So we check for actual changes to the text 
1167         before passing the events on. 
1169 ##        dbg('NumCtrl::OnTextChange', indent=1) 
1170         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1174         # else... legal value 
1176         value = self.GetValue() 
1177         if value != self._oldvalue: 
1179                 self.GetEventHandler().ProcessEvent( 
1180                     NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) 
1184             # let normal processing of the text continue 
1186         self._oldvalue = value # record for next event 
1189     def _GetValue(self): 
1191         Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the 
1192         control with this function. 
1194         return wx.TextCtrl.GetValue(self) 
1199         Returns the current numeric value of the control. 
1201         return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) 
1203     def SetValue(self, value): 
1205         Sets the value of the control to the value specified. 
1206         The resulting actual value of the control may be altered to 
1207         conform with the bounds set on the control if limited, 
1208         or colored if not limited but the value is out-of-bounds. 
1209         A ValueError exception will be raised if an invalid value 
1212 ##        dbg('NumCtrl::SetValue(%s)' % value, indent=1) 
1213         BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) 
1217     def SetIntegerWidth(self, value): 
1218         self.SetParameters(integerWidth=value) 
1219     def GetIntegerWidth(self): 
1220         return self._integerWidth 
1222     def SetFractionWidth(self, value): 
1223         self.SetParameters(fractionWidth=value) 
1224     def GetFractionWidth(self): 
1225         return self._fractionWidth 
1229     def SetMin(self, min=None): 
1231         Sets the minimum value of the control.  If a value of None 
1232         is provided, then the control will have no explicit minimum value. 
1233         If the value specified is greater than the current maximum value, 
1234         then the function returns False and the minimum will not change from 
1235         its current setting.  On success, the function returns True. 
1237         If successful and the current value is lower than the new lower 
1238         bound, if the control is limited, the value will be automatically 
1239         adjusted to the new minimum value; if not limited, the value in the 
1240         control will be colored as invalid. 
1242         If min > the max value allowed by the width of the control, 
1243         the function will return False, and the min will not be set. 
1245 ##        dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1) 
1246         if( self._max is None 
1248             or (self._max is not None and self._max >= min) ): 
1250                 self.SetParameters(min=min) 
1261         Gets the lower bound value of the control.  It will return 
1262         None if not specified. 
1267     def SetMax(self, max=None): 
1269         Sets the maximum value of the control. If a value of None 
1270         is provided, then the control will have no explicit maximum value. 
1271         If the value specified is less than the current minimum value, then 
1272         the function returns False and the maximum will not change from its 
1273         current setting. On success, the function returns True. 
1275         If successful and the current value is greater than the new upper 
1276         bound, if the control is limited the value will be automatically 
1277         adjusted to this maximum value; if not limited, the value in the 
1278         control will be colored as invalid. 
1280         If max > the max value allowed by the width of the control, 
1281         the function will return False, and the max will not be set. 
1283         if( self._min is None 
1285             or (self._min is not None and self._min <= max) ): 
1287                 self.SetParameters(max=max) 
1299         Gets the maximum value of the control.  It will return the current 
1300         maximum integer, or None if not specified. 
1305     def SetBounds(self, min=None, max=None): 
1307         This function is a convenience function for setting the min and max 
1308         values at the same time.  The function only applies the maximum bound 
1309         if setting the minimum bound is successful, and returns True 
1310         only if both operations succeed. 
1311         NOTE: leaving out an argument will remove the corresponding bound. 
1313         ret = self.SetMin(min) 
1314         return ret and self.SetMax(max) 
1317     def GetBounds(self): 
1319         This function returns a two-tuple (min,max), indicating the 
1320         current bounds of the control.  Each value can be None if 
1321         that bound is not set. 
1323         return (self._min, self._max) 
1326     def SetLimited(self, limited): 
1328         If called with a value of True, this function will cause the control 
1329         to limit the value to fall within the bounds currently specified. 
1330         If the control's value currently exceeds the bounds, it will then 
1331         be limited accordingly. 
1333         If called with a value of False, this function will disable value 
1334         limiting, but coloring of out-of-bounds values will still take 
1335         place if bounds have been set for the control. 
1337         self.SetParameters(limited = limited) 
1340     def IsLimited(self): 
1342         Returns True if the control is currently limiting the 
1343         value to fall within the current bounds. 
1345         return self._limited 
1347     def GetLimited(self): 
1348         """ (For regularization of property accessors) """ 
1349         return self.IsLimited 
1352     def IsInBounds(self, value=None): 
1354         Returns True if no value is specified and the current value 
1355         of the control falls within the current bounds.  This function can 
1356         also be called with a value to see if that value would fall within 
1357         the current bounds of the given control. 
1359 ##        dbg('IsInBounds(%s)' % repr(value), indent=1) 
1361             value = self.GetValue() 
1364                 value = self._GetNumValue(self._toGUI(value)) 
1365             except ValueError, e: 
1366 ##                dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) 
1368             if value.strip() == '': 
1370             elif self._fractionWidth: 
1371                 value = float(value) 
1377         if min is None: min = value 
1378         if max is None: max = value 
1380         # if bounds set, and value is None, return False 
1381         if value == None and (min is not None or max is not None): 
1382 ##            dbg('finished IsInBounds', indent=0) 
1385 ##            dbg('finished IsInBounds', indent=0) 
1386             return min <= value <= max 
1389     def SetAllowNone(self, allow_none): 
1391         Change the behavior of the validation code, allowing control 
1392         to have a value of None or not, as appropriate.  If the value 
1393         of the control is currently None, and allow_none is False, the 
1394         value of the control will be set to the minimum value of the 
1395         control, or 0 if no lower bound is set. 
1397         self._allowNone = allow_none 
1398         if not allow_none and self.GetValue() is None: 
1400             if min is not None: self.SetValue(min) 
1401             else:               self.SetValue(0) 
1404     def IsNoneAllowed(self): 
1405         return self._allowNone 
1406     def GetAllowNone(self): 
1407         """ (For regularization of property accessors) """ 
1408         return self.IsNoneAllowed() 
1410     def SetAllowNegative(self, value): 
1411         self.SetParameters(allowNegative=value) 
1412     def IsNegativeAllowed(self): 
1413         return self._allowNegative 
1414     def GetAllowNegative(self): 
1415         """ (For regularization of property accessors) """ 
1416         return self.IsNegativeAllowed() 
1418     def SetGroupDigits(self, value): 
1419         self.SetParameters(groupDigits=value) 
1420     def IsGroupingAllowed(self): 
1421         return self._groupDigits 
1422     def GetGroupDigits(self): 
1423         """ (For regularization of property accessors) """ 
1424         return self.IsGroupingAllowed() 
1426     def SetGroupChar(self, value): 
1427         self.SetParameters(groupChar=value) 
1428     def GetGroupChar(self): 
1429         return self._groupChar 
1431     def SetDecimalChar(self, value): 
1432         self.SetParameters(decimalChar=value) 
1433     def GetDecimalChar(self): 
1434         return self._decimalChar 
1436     def SetSelectOnEntry(self, value): 
1437         self.SetParameters(selectOnEntry=value) 
1438     def GetSelectOnEntry(self): 
1439         return self._selectOnEntry 
1441     def SetAutoSize(self, value): 
1442         self.SetParameters(autoSize=value) 
1443     def GetAutoSize(self): 
1444         return self._autoSize 
1447     # (Other parameter accessors are inherited from base class) 
1450     def _toGUI( self, value, apply_limits = True ): 
1452         Conversion function used to set the value of the control; does 
1453         type and bounds checking and raises ValueError if argument is 
1456 ##        dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1) 
1457         if value is None and self.IsNoneAllowed(): 
1459             return self._template 
1461         elif type(value) in (types.StringType, types.UnicodeType): 
1462             value = self._GetNumValue(value) 
1463 ##            dbg('cleansed num value: "%s"' % value) 
1465                 if self.IsNoneAllowed(): 
1467                     return self._template 
1469 ##                    dbg('exception raised:', e, indent=0) 
1470                     raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1473                 if self._fractionWidth or value.find('.') != -1: 
1474                     value = float(value) 
1477             except Exception, e: 
1478 ##                dbg('exception raised:', e, indent=0) 
1479                 raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1481         elif type(value) not in (types.IntType, types.LongType, types.FloatType): 
1484                 'NumCtrl requires numeric value, passed %s'% repr(value) ) 
1486         if not self._allowNegative and value < 0: 
1488                 'control configured to disallow negative values, passed %s'% repr(value) ) 
1490         if self.IsLimited() and apply_limits: 
1493             if not min is None and value < min: 
1496                     'value %d is below minimum value of control'% value ) 
1497             if not max is None and value > max: 
1500                     'value %d exceeds value of control'% value ) 
1502         adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) 
1503 ##        dbg('len(%s):' % self._mask, len(self._mask)) 
1504 ##        dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) 
1505 ##        dbg('adjustwidth:', adjustwidth) 
1506         if self._fractionWidth == 0: 
1507             s = str(long(value)).rjust(self._integerWidth) 
1509             format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) 
1510             s = format % float(value) 
1511 ##        dbg('s:"%s"' % s, 'len(s):', len(s)) 
1512         if len(s) > (adjustwidth - self._groupSpace): 
1514             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1515         elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): 
1517             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1519         s = s.rjust(adjustwidth).replace('.', self._decimalChar) 
1520         if self._signOk and self._useParens: 
1521             if s.find('-') != -1: 
1522                 s = s.replace('-', '(') + ')' 
1525 ##        dbg('returned: "%s"' % s, indent=0) 
1529     def _fromGUI( self, value ): 
1531         Conversion function used in getting the value of the control. 
1534 ##        dbg('NumCtrl::_fromGUI(%s)' % value, indent=1) 
1535         # One or more of the underlying text control implementations 
1536         # issue an intermediate EVT_TEXT when replacing the control's 
1537         # value, where the intermediate value is an empty string. 
1538         # So, to ensure consistency and to prevent spurious ValueErrors, 
1539         # we make the following test, and react accordingly: 
1541         if value.strip() == '': 
1542             if not self.IsNoneAllowed(): 
1543 ##                dbg('empty value; not allowed,returning 0', indent = 0) 
1544                 if self._fractionWidth: 
1549 ##                dbg('empty value; returning None', indent = 0) 
1552             value = self._GetNumValue(value) 
1553 ##            dbg('Num value: "%s"' % value) 
1554             if self._fractionWidth: 
1557                     return float( value ) 
1559 ##                    dbg("couldn't convert to float; returning None") 
1570                        return long( value ) 
1572 ##                       dbg("couldn't convert to long; returning None") 
1578 ##                    dbg('exception occurred; returning None') 
1582     def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): 
1584         Preprocessor for base control paste; if value needs to be right-justified 
1585         to fit in control, do so prior to paste: 
1587 ##        dbg('NumCtrl::_Paste (value = "%s")' % value, indent=1) 
1589             paste_text = self._getClipboardContents() 
1592         sel_start, sel_to = self._GetSelection() 
1593         orig_sel_start = sel_start 
1594         orig_sel_to = sel_to 
1595 ##        dbg('selection:', (sel_start, sel_to)) 
1596         old_value = self._GetValue() 
1599         field = self._FindField(sel_start) 
1600         edit_start, edit_end = field._extent 
1602         # handle possibility of groupChar being a space: 
1603         newtext = paste_text.lstrip() 
1604         lspace_count = len(paste_text) - len(newtext) 
1605         paste_text = ' ' * lspace_count  + newtext.replace(self._groupChar, '').replace('(', '-').replace(')','') 
1607         if field._insertRight and self._groupDigits: 
1608             # want to paste to the left; see if it will fit: 
1609             left_text = old_value[edit_start:sel_start].lstrip() 
1610 ##            dbg('len(left_text):', len(left_text)) 
1611 ##            dbg('len(paste_text):', len(paste_text)) 
1612 ##            dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
1613             if sel_start - (len(left_text) + len(paste_text)) >= edit_start: 
1614                 # will fit! create effective paste text, and move cursor back to do so: 
1615                 paste_text = left_text + paste_text 
1616                 sel_start -= len(paste_text) 
1617                 sel_start += sel_to - orig_sel_start    # decrease by amount selected 
1619 ##                dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
1620 ##                dbg('adjusted start before accounting for grouping:', sel_start) 
1621 ##                dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text) 
1623             if self._groupDigits and sel_start != orig_sel_start: 
1624                 left_len = len(old_value[:sel_to].lstrip()) 
1625                 # remove group chars from adjusted paste string, and left pad to wipe out 
1626                 # old characters, so that selection will remove the right chars, and 
1627                 # readjust will do the right thing: 
1628                 paste_text = paste_text.replace(self._groupChar,'') 
1629                 adjcount = left_len - len(paste_text) 
1630                 paste_text = ' ' * adjcount + paste_text 
1631                 sel_start = sel_to - len(paste_text) 
1632 ##                dbg('adjusted start after accounting for grouping:', sel_start) 
1633 ##                dbg('adjusted paste_text after accounting for grouping: "%s"' % paste_text) 
1634             self.SetInsertionPoint(sel_to) 
1635             self.SetSelection(sel_start, sel_to) 
1637         new_text, replace_to = MaskedEditMixin._Paste(self, 
1639                                         raise_on_invalid=raise_on_invalid, 
1640                                         just_return_value=True) 
1641         self._SetInsertionPoint(orig_sel_to) 
1642         self._SetSelection(orig_sel_start, orig_sel_to) 
1643         if not just_return_value and new_text is not None: 
1644             if new_text != self._GetValue(): 
1645                     self.modified = True 
1649                 wx.CallAfter(self._SetValue, new_text) 
1650                 wx.CallAfter(self._SetInsertionPoint, replace_to) 
1654             return new_text, replace_to 
1656     def _Undo(self, value=None, prev=None): 
1657         '''numctrl
's undo is more complicated than the base control's
, due to
 
1658         grouping characters
; we don
't want to consider them when calculating 
1659         the undone portion.''' 
1660 ##        dbg('NumCtrl
::_Undo
', indent=1) 
1661         if value is None: value = self._GetValue() 
1662         if prev is None: prev = self._prevValue 
1663         if not self._groupDigits: 
1664             ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, value, prev, just_return_results = True) 
1665             self._SetValue(prev) 
1666             self._SetInsertionPoint(new_sel_start) 
1667             self._SetSelection(new_sel_start, new_sel_to) 
1668             self._prevSelection = (new_sel_start, new_sel_to) 
1669 ##            dbg('resetting 
"prev selection" to
', self._prevSelection) 
1673         sel_start, sel_to = self._prevSelection 
1674         edit_start, edit_end = self._FindFieldExtent(0) 
1676         adjvalue = self._GetNumValue(value).rjust(self._masklength) 
1677         adjprev  = self._GetNumValue(prev ).rjust(self._masklength) 
1679         # move selection to account for "ungrouped" value: 
1680         left_text = value[sel_start:].lstrip() 
1681         numleftgroups = len(left_text) - len(left_text.replace(self._groupChar, '')) 
1682         adjsel_start = sel_start + numleftgroups 
1683         right_text = value[sel_to:].lstrip() 
1684         numrightgroups = len(right_text) - len(right_text.replace(self._groupChar, '')) 
1685         adjsel_to = sel_to + numrightgroups 
1686 ##        dbg('adjusting 
"previous" selection 
from', (sel_start, sel_to), 'to
:', (adjsel_start, adjsel_to)) 
1687         self._prevSelection = (adjsel_start, adjsel_to) 
1689         # determine appropriate selection for ungrouped undo 
1690         ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, adjvalue, adjprev, just_return_results = True) 
1692         # adjust new selection based on grouping: 
1693         left_len = edit_end - new_sel_start 
1694         numleftgroups = left_len / 3 
1695         new_sel_start -= numleftgroups 
1696         if numleftgroups and left_len % 3 == 0: 
1699         if new_sel_start < self._masklength and prev[new_sel_start] == self._groupChar: 
1702         right_len = edit_end - new_sel_to 
1703         numrightgroups = right_len / 3 
1704         new_sel_to -= numrightgroups 
1706         if new_sel_to and prev[new_sel_to-1] == self._groupChar: 
1709         if new_sel_start > new_sel_to: 
1710             new_sel_to = new_sel_start 
1712         # for numbers, we don't care about leading whitespace
; adjust selection 
if 
1713         # it includes leading space. 
1714         prev_stripped 
= prev
.lstrip() 
1715         prev_start 
= self
._masklength 
- len(prev_stripped
) 
1716         if new_sel_start 
< prev_start
: 
1717             new_sel_start 
= prev_start
 
1719 ##        dbg('adjusted selection accounting for grouping:', (new_sel_start, new_sel_to)) 
1720         self
._SetValue
(prev
) 
1721         self
._SetInsertionPoint
(new_sel_start
) 
1722         self
._SetSelection
(new_sel_start
, new_sel_to
) 
1723         self
._prevSelection 
= (new_sel_start
, new_sel_to
) 
1724 ##        dbg('resetting "prev selection" to', self._prevSelection) 
1727 #=========================================================================== 
1729 if __name__ 
== '__main__': 
1733     class myDialog(wx
.Dialog
): 
1734         def __init__(self
, parent
, id, title
, 
1735             pos 
= wx
.DefaultPosition
, size 
= wx
.DefaultSize
, 
1736             style 
= wx
.DEFAULT_DIALOG_STYLE 
): 
1737             wx
.Dialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
) 
1739             self
.int_ctrl 
= NumCtrl(self
, wx
.NewId(), size
=(55,20)) 
1740             self
.OK 
= wx
.Button( self
, wx
.ID_OK
, "OK") 
1741             self
.Cancel 
= wx
.Button( self
, wx
.ID_CANCEL
, "Cancel") 
1743             vs 
= wx
.BoxSizer( wx
.VERTICAL 
) 
1744             vs
.Add( self
.int_ctrl
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1745             hs 
= wx
.BoxSizer( wx
.HORIZONTAL 
) 
1746             hs
.Add( self
.OK
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1747             hs
.Add( self
.Cancel
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1748             vs
.Add(hs
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1750             self
.SetAutoLayout( True ) 
1753             vs
.SetSizeHints( self 
) 
1754             self
.Bind(EVT_NUM
, self
.OnChange
, self
.int_ctrl
) 
1756         def OnChange(self
, event
): 
1757             print 'value now', event
.GetValue() 
1759     class TestApp(wx
.App
): 
1762                 self
.frame 
= wx
.Frame(None, -1, "Test", (20,20), (120,100)  ) 
1763                 self
.panel 
= wx
.Panel(self
.frame
, -1) 
1764                 button 
= wx
.Button(self
.panel
, -1, "Push Me", (20, 20)) 
1765                 self
.Bind(wx
.EVT_BUTTON
, self
.OnClick
, button
) 
1767                 traceback
.print_exc() 
1771         def OnClick(self
, event
): 
1772             dlg 
= myDialog(self
.panel
, -1, "test NumCtrl") 
1773             dlg
.int_ctrl
.SetValue(501) 
1774             dlg
.int_ctrl
.SetInsertionPoint(1) 
1775             dlg
.int_ctrl
.SetSelection(1,2) 
1776             rc 
= dlg
.ShowModal() 
1777             print 'final value', dlg
.int_ctrl
.GetValue() 
1779             self
.frame
.Destroy() 
1782             self
.frame
.Show(True) 
1789         traceback
.print_exc() 
1793 ## =============================## 
1794 ##   1. Add support for printf-style format specification. 
1795 ##   2. Add option for repositioning on 'illegal' insertion point. 
1798 ##   1. fixed to allow space for a group char. 
1801 ##   1. Allowed select/replace digits. 
1802 ##   2. Fixed undo to ignore grouping chars. 
1805 ##   1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. 
1806 ##   2. Added autoSize parameter, to allow manual sizing of the control. 
1807 ##   3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of 
1808 ##      nonsensical parameter methods from the control, so it will work 
1809 ##      properly with Boa. 
1810 ##   4. Fixed allowNone bug found by user sameerc1@grandecom.net