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) 
 951             elif self._min > 0 and self.IsLimited(): 
 952                 replacement = self._min 
 955 ##            dbg('empty value; setting replacement:', replacement) 
 957         if replacement is None: 
 958             # Go get the integer portion about to be set and verify its validity 
 959             intstart, intend = self._fields[0]._extent 
 960 ##            dbg('intstart, intend:', intstart, intend) 
 961 ##            dbg('raw integer:"%s"' % value[intstart:intend]) 
 962             int = self._GetNumValue(value[intstart:intend]) 
 963             numval = self._fromGUI(value) 
 965 ##            dbg('integer: "%s"' % int) 
 967                 fracval = self.GetFraction(value) 
 968             except ValueError, e: 
 969 ##                dbg('Exception:', e, 'must be out of bounds; disallow value') 
 970                 self._disallowValue() 
 975 ##                dbg('self._isNeg?', self._isNeg) 
 976                 if int == '-' and self._oldvalue < 0 and not self._typedSign: 
 977 ##                    dbg('just a negative sign; old value < 0; setting replacement of 0') 
 980                 elif int[:2] == '-0' and self._fractionWidth == 0: 
 981                     if self._oldvalue < 0: 
 982 ##                        dbg('-0; setting replacement of 0') 
 985                     elif not self._limited or (self._min < -1 and self._max >= -1): 
 986 ##                        dbg('-0; setting replacement of -1') 
 990                         # limited and -1 is out of bounds 
 991                         self._disallowValue() 
 995                 elif int == '-' and (self._oldvalue >= 0 or self._typedSign) and self._fractionWidth == 0: 
 996                     if not self._limited or (self._min < -1 and self._max >= -1): 
 997 ##                        dbg('just a negative sign; setting replacement of -1') 
1000                         # limited and -1 is out of bounds 
1001                         self._disallowValue() 
1005                 elif( self._typedSign 
1006                       and int.find('-') != -1 
1008                       and not self._min <= numval <= self._max): 
1009                     # changed sign resulting in value that's now out-of-bounds; 
1011                     self._disallowValue() 
1015             if replacement is None: 
1016                 if int and int != '-': 
1020                         # integer requested is not legal.  This can happen if the user 
1021                         # is attempting to insert a digit in the middle of the control 
1022                         # resulting in something like "   3   45". Disallow such actions: 
1023 ##                        dbg('>>>>>>>>>>>>>>>> "%s" does not convert to a long!' % int) 
1024                         if not wx.Validator_IsSilent(): 
1026                         sel_start, sel_to = self._GetSelection() 
1027 ##                        dbg('queuing reselection of (%d, %d)' % (sel_start, sel_to)) 
1028                         wx.CallAfter(self.SetInsertionPoint, sel_start)      # preserve current selection/position 
1029                         wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1033                     if int[0] == '0' and len(int) > 1: 
1034 ##                        dbg('numvalue: "%s"' % numvalue.replace(' ', '')) 
1035                         if self._fractionWidth: 
1036                             value = self._toGUI(string.atof(numvalue)) 
1038                             value = self._toGUI(string.atol(numvalue)) 
1039 ##                        dbg('modified value: "%s"' % value) 
1041         self._typedSign = False     # reset state var 
1043         if replacement is not None: 
1044             # Value presented wasn't a legal number, but control should do something 
1045             # reasonable instead: 
1046 ##            dbg('setting replacement value:', replacement) 
1047             self._SetValue(self._toGUI(replacement)) 
1048             sel_start = BaseMaskedTextCtrl.GetValue(self).find(str(abs(replacement)))   # find where it put the 1, so we can select it 
1049             sel_to = sel_start + len(str(abs(replacement))) 
1050 ##            dbg('queuing selection of (%d, %d)' %(sel_start, sel_to)) 
1051             wx.CallAfter(self.SetInsertionPoint, sel_start) 
1052             wx.CallAfter(self.SetSelection, sel_start, sel_to) 
1056         # Otherwise, apply appropriate formatting to value: 
1058         # Because we're intercepting the value and adjusting it 
1059         # before a sign change is detected, we need to do this here: 
1060         if '-' in value or '(' in value: 
1065 ##        dbg('value:"%s"' % value, 'self._useParens:', self._useParens) 
1066         if self._fractionWidth: 
1067             adjvalue = self._adjustFloat(self._GetNumValue(value).replace('.',self._decimalChar)) 
1069             adjvalue = self._adjustInt(self._GetNumValue(value)) 
1070 ##        dbg('adjusted value: "%s"' % adjvalue) 
1073         sel_start, sel_to = self._GetSelection()     # record current insertion point 
1074 ##        dbg('calling BaseMaskedTextCtrl._SetValue(self, "%s")' % adjvalue) 
1075         BaseMaskedTextCtrl._SetValue(self, adjvalue) 
1076         # After all actions so far scheduled, check that resulting cursor 
1077         # position is appropriate, and move if not: 
1078         wx.CallAfter(self._CheckInsertionPoint) 
1080 ##        dbg('finished NumCtrl::_SetValue', indent=0) 
1082     def _CheckInsertionPoint(self): 
1083         # If current insertion point is before the end of the integer and 
1084         # its before the 1st digit, place it just after the sign position: 
1085 ##        dbg('NumCtrl::CheckInsertionPoint', indent=1) 
1086         sel_start, sel_to = self._GetSelection() 
1087         text = self._GetValue() 
1088         if sel_to < self._fields[0]._extent[1] and text[sel_to] in (' ', '-', '('): 
1089             text, signpos, right_signpos = self._getSignedValue() 
1090 ##            dbg('setting selection(%d, %d)' % (signpos+1, signpos+1)) 
1091             self.SetInsertionPoint(signpos+1) 
1092             self.SetSelection(signpos+1, signpos+1) 
1096     def _OnErase( self, event=None, just_return_value=False ): 
1098         This overrides the base control _OnErase, so that erasing around 
1099         grouping characters auto selects the digit before or after the 
1100         grouping character, so that the erasure does the right thing. 
1102 ##        dbg('NumCtrl::_OnErase', indent=1) 
1103         if event is None:   # called as action routine from Cut() operation. 
1106             key = event.GetKeyCode() 
1107         #if grouping digits, make sure deletes next to group char always 
1108         # delete next digit to appropriate side: 
1109         if self._groupDigits: 
1110             value = BaseMaskedTextCtrl.GetValue(self) 
1111             sel_start, sel_to = self._GetSelection() 
1113             if key == wx.WXK_BACK: 
1114                 # if 1st selected char is group char, select to previous digit 
1115                 if sel_start > 0 and sel_start < len(self._mask) and value[sel_start:sel_to] == self._groupChar: 
1116                     self.SetInsertionPoint(sel_start-1) 
1117                     self.SetSelection(sel_start-1, sel_to) 
1119                 # elif previous char is group char, select to previous digit 
1120                 elif sel_start > 1 and sel_start == sel_to and value[sel_start-1:sel_start] == self._groupChar: 
1121                     self.SetInsertionPoint(sel_start-2) 
1122                     self.SetSelection(sel_start-2, sel_to) 
1124             elif key == wx.WXK_DELETE: 
1125                 if( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1126                     and sel_start == sel_to 
1127                     and value[sel_to] == self._groupChar ): 
1128                     self.SetInsertionPoint(sel_start) 
1129                     self.SetSelection(sel_start, sel_to+2) 
1131                 elif( sel_to < len(self._mask) - 2 + (1 *self._useParens) 
1132                            and value[sel_start:sel_to] == self._groupChar ): 
1133                     self.SetInsertionPoint(sel_start) 
1134                     self.SetSelection(sel_start, sel_to+1) 
1136         return BaseMaskedTextCtrl._OnErase(self, event, just_return_value) 
1139     def OnTextChange( self, event ): 
1141         Handles an event indicating that the text control's value 
1142         has changed, and issue EVT_NUM event. 
1143         NOTE: using wxTextCtrl.SetValue() to change the control's 
1144         contents from within a EVT_CHAR handler can cause double 
1145         text events.  So we check for actual changes to the text 
1146         before passing the events on. 
1148 ##        dbg('NumCtrl::OnTextChange', indent=1) 
1149         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1153         # else... legal value 
1155         value = self.GetValue() 
1156         if value != self._oldvalue: 
1158                 self.GetEventHandler().ProcessEvent( 
1159                     NumberUpdatedEvent( self.GetId(), self.GetValue(), self ) ) 
1163             # let normal processing of the text continue 
1165         self._oldvalue = value # record for next event 
1168     def _GetValue(self): 
1170         Override of BaseMaskedTextCtrl to allow mixin to get the raw text value of the 
1171         control with this function. 
1173         return wx.TextCtrl.GetValue(self) 
1178         Returns the current numeric value of the control. 
1180         return self._fromGUI( BaseMaskedTextCtrl.GetValue(self) ) 
1182     def SetValue(self, value): 
1184         Sets the value of the control to the value specified. 
1185         The resulting actual value of the control may be altered to 
1186         conform with the bounds set on the control if limited, 
1187         or colored if not limited but the value is out-of-bounds. 
1188         A ValueError exception will be raised if an invalid value 
1191 ##        dbg('NumCtrl::SetValue(%s)' % value, indent=1) 
1192         BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) ) 
1196     def SetIntegerWidth(self, value): 
1197         self.SetParameters(integerWidth=value) 
1198     def GetIntegerWidth(self): 
1199         return self._integerWidth 
1201     def SetFractionWidth(self, value): 
1202         self.SetParameters(fractionWidth=value) 
1203     def GetFractionWidth(self): 
1204         return self._fractionWidth 
1208     def SetMin(self, min=None): 
1210         Sets the minimum value of the control.  If a value of None 
1211         is provided, then the control will have no explicit minimum value. 
1212         If the value specified is greater than the current maximum value, 
1213         then the function returns False and the minimum will not change from 
1214         its current setting.  On success, the function returns True. 
1216         If successful and the current value is lower than the new lower 
1217         bound, if the control is limited, the value will be automatically 
1218         adjusted to the new minimum value; if not limited, the value in the 
1219         control will be colored as invalid. 
1221         If min > the max value allowed by the width of the control, 
1222         the function will return False, and the min will not be set. 
1224 ##        dbg('NumCtrl::SetMin(%s)' % repr(min), indent=1) 
1225         if( self._max is None 
1227             or (self._max is not None and self._max >= min) ): 
1229                 self.SetParameters(min=min) 
1240         Gets the lower bound value of the control.  It will return 
1241         None if not specified. 
1246     def SetMax(self, max=None): 
1248         Sets the maximum value of the control. If a value of None 
1249         is provided, then the control will have no explicit maximum value. 
1250         If the value specified is less than the current minimum value, then 
1251         the function returns False and the maximum will not change from its 
1252         current setting. On success, the function returns True. 
1254         If successful and the current value is greater than the new upper 
1255         bound, if the control is limited the value will be automatically 
1256         adjusted to this maximum value; if not limited, the value in the 
1257         control will be colored as invalid. 
1259         If max > the max value allowed by the width of the control, 
1260         the function will return False, and the max will not be set. 
1262         if( self._min is None 
1264             or (self._min is not None and self._min <= max) ): 
1266                 self.SetParameters(max=max) 
1278         Gets the maximum value of the control.  It will return the current 
1279         maximum integer, or None if not specified. 
1284     def SetBounds(self, min=None, max=None): 
1286         This function is a convenience function for setting the min and max 
1287         values at the same time.  The function only applies the maximum bound 
1288         if setting the minimum bound is successful, and returns True 
1289         only if both operations succeed. 
1290         NOTE: leaving out an argument will remove the corresponding bound. 
1292         ret = self.SetMin(min) 
1293         return ret and self.SetMax(max) 
1296     def GetBounds(self): 
1298         This function returns a two-tuple (min,max), indicating the 
1299         current bounds of the control.  Each value can be None if 
1300         that bound is not set. 
1302         return (self._min, self._max) 
1305     def SetLimited(self, limited): 
1307         If called with a value of True, this function will cause the control 
1308         to limit the value to fall within the bounds currently specified. 
1309         If the control's value currently exceeds the bounds, it will then 
1310         be limited accordingly. 
1312         If called with a value of False, this function will disable value 
1313         limiting, but coloring of out-of-bounds values will still take 
1314         place if bounds have been set for the control. 
1316         self.SetParameters(limited = limited) 
1319     def IsLimited(self): 
1321         Returns True if the control is currently limiting the 
1322         value to fall within the current bounds. 
1324         return self._limited 
1326     def GetLimited(self): 
1327         """ (For regularization of property accessors) """ 
1328         return self.IsLimited 
1331     def IsInBounds(self, value=None): 
1333         Returns True if no value is specified and the current value 
1334         of the control falls within the current bounds.  This function can 
1335         also be called with a value to see if that value would fall within 
1336         the current bounds of the given control. 
1338 ##        dbg('IsInBounds(%s)' % repr(value), indent=1) 
1340             value = self.GetValue() 
1343                 value = self._GetNumValue(self._toGUI(value)) 
1344             except ValueError, e: 
1345 ##                dbg('error getting NumValue(self._toGUI(value)):', e, indent=0) 
1347             if value.strip() == '': 
1349             elif self._fractionWidth: 
1350                 value = float(value) 
1356         if min is None: min = value 
1357         if max is None: max = value 
1359         # if bounds set, and value is None, return False 
1360         if value == None and (min is not None or max is not None): 
1361 ##            dbg('finished IsInBounds', indent=0) 
1364 ##            dbg('finished IsInBounds', indent=0) 
1365             return min <= value <= max 
1368     def SetAllowNone(self, allow_none): 
1370         Change the behavior of the validation code, allowing control 
1371         to have a value of None or not, as appropriate.  If the value 
1372         of the control is currently None, and allow_none is False, the 
1373         value of the control will be set to the minimum value of the 
1374         control, or 0 if no lower bound is set. 
1376         self._allowNone = allow_none 
1377         if not allow_none and self.GetValue() is None: 
1379             if min is not None: self.SetValue(min) 
1380             else:               self.SetValue(0) 
1383     def IsNoneAllowed(self): 
1384         return self._allowNone 
1385     def GetAllowNone(self): 
1386         """ (For regularization of property accessors) """ 
1387         return self.IsNoneAllowed() 
1389     def SetAllowNegative(self, value): 
1390         self.SetParameters(allowNegative=value) 
1391     def IsNegativeAllowed(self): 
1392         return self._allowNegative 
1393     def GetAllowNegative(self): 
1394         """ (For regularization of property accessors) """ 
1395         return self.IsNegativeAllowed() 
1397     def SetGroupDigits(self, value): 
1398         self.SetParameters(groupDigits=value) 
1399     def IsGroupingAllowed(self): 
1400         return self._groupDigits 
1401     def GetGroupDigits(self): 
1402         """ (For regularization of property accessors) """ 
1403         return self.IsGroupingAllowed() 
1405     def SetGroupChar(self, value): 
1406         self.SetParameters(groupChar=value) 
1407     def GetGroupChar(self): 
1408         return self._groupChar 
1410     def SetDecimalChar(self, value): 
1411         self.SetParameters(decimalChar=value) 
1412     def GetDecimalChar(self): 
1413         return self._decimalChar 
1415     def SetSelectOnEntry(self, value): 
1416         self.SetParameters(selectOnEntry=value) 
1417     def GetSelectOnEntry(self): 
1418         return self._selectOnEntry 
1420     def SetAutoSize(self, value): 
1421         self.SetParameters(autoSize=value) 
1422     def GetAutoSize(self): 
1423         return self._autoSize 
1426     # (Other parameter accessors are inherited from base class) 
1429     def _toGUI( self, value, apply_limits = True ): 
1431         Conversion function used to set the value of the control; does 
1432         type and bounds checking and raises ValueError if argument is 
1435 ##        dbg('NumCtrl::_toGUI(%s)' % repr(value), indent=1) 
1436         if value is None and self.IsNoneAllowed(): 
1438             return self._template 
1440         elif type(value) in (types.StringType, types.UnicodeType): 
1441             value = self._GetNumValue(value) 
1442 ##            dbg('cleansed num value: "%s"' % value) 
1444                 if self.IsNoneAllowed(): 
1446                     return self._template 
1448 ##                    dbg('exception raised:', e, indent=0) 
1449                     raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1452                 if self._fractionWidth or value.find('.') != -1: 
1453                     value = float(value) 
1456             except Exception, e: 
1457 ##                dbg('exception raised:', e, indent=0) 
1458                 raise ValueError ('NumCtrl requires numeric value, passed %s'% repr(value) ) 
1460         elif type(value) not in (types.IntType, types.LongType, types.FloatType): 
1463                 'NumCtrl requires numeric value, passed %s'% repr(value) ) 
1465         if not self._allowNegative and value < 0: 
1467                 'control configured to disallow negative values, passed %s'% repr(value) ) 
1469         if self.IsLimited() and apply_limits: 
1472             if not min is None and value < min: 
1475                     'value %d is below minimum value of control'% value ) 
1476             if not max is None and value > max: 
1479                     'value %d exceeds value of control'% value ) 
1481         adjustwidth = len(self._mask) - (1 * self._useParens * self._signOk) 
1482 ##        dbg('len(%s):' % self._mask, len(self._mask)) 
1483 ##        dbg('adjustwidth - groupSpace:', adjustwidth - self._groupSpace) 
1484 ##        dbg('adjustwidth:', adjustwidth) 
1485         if self._fractionWidth == 0: 
1486             s = str(long(value)).rjust(self._integerWidth) 
1488             format = '%' + '%d.%df' % (self._integerWidth+self._fractionWidth+1, self._fractionWidth) 
1489             s = format % float(value) 
1490 ##        dbg('s:"%s"' % s, 'len(s):', len(s)) 
1491         if len(s) > (adjustwidth - self._groupSpace): 
1493             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1494         elif s[0] not in ('-', ' ') and self._allowNegative and len(s) == (adjustwidth - self._groupSpace): 
1496             raise ValueError ('value %s exceeds the integer width of the control (%d)' % (s, self._integerWidth)) 
1498         s = s.rjust(adjustwidth).replace('.', self._decimalChar) 
1499         if self._signOk and self._useParens: 
1500             if s.find('-') != -1: 
1501                 s = s.replace('-', '(') + ')' 
1504 ##        dbg('returned: "%s"' % s, indent=0) 
1508     def _fromGUI( self, value ): 
1510         Conversion function used in getting the value of the control. 
1513 ##        dbg('NumCtrl::_fromGUI(%s)' % value, indent=1) 
1514         # One or more of the underlying text control implementations 
1515         # issue an intermediate EVT_TEXT when replacing the control's 
1516         # value, where the intermediate value is an empty string. 
1517         # So, to ensure consistency and to prevent spurious ValueErrors, 
1518         # we make the following test, and react accordingly: 
1520         if value.strip() == '': 
1521             if not self.IsNoneAllowed(): 
1522 ##                dbg('empty value; not allowed,returning 0', indent = 0) 
1523                 if self._fractionWidth: 
1528 ##                dbg('empty value; returning None', indent = 0) 
1531             value = self._GetNumValue(value) 
1532 ##            dbg('Num value: "%s"' % value) 
1533             if self._fractionWidth: 
1536                     return float( value ) 
1538 ##                    dbg("couldn't convert to float; returning None") 
1549                        return long( value ) 
1551 ##                       dbg("couldn't convert to long; returning None") 
1557 ##                    dbg('exception occurred; returning None') 
1561     def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ): 
1563         Preprocessor for base control paste; if value needs to be right-justified 
1564         to fit in control, do so prior to paste: 
1566 ##        dbg('NumCtrl::_Paste (value = "%s")' % value, indent=1) 
1568             paste_text = self._getClipboardContents() 
1571         sel_start, sel_to = self._GetSelection() 
1572         orig_sel_start = sel_start 
1573         orig_sel_to = sel_to 
1574 ##        dbg('selection:', (sel_start, sel_to)) 
1575         old_value = self._GetValue() 
1578         field = self._FindField(sel_start) 
1579         edit_start, edit_end = field._extent 
1581         # handle possibility of groupChar being a space: 
1582         newtext = paste_text.lstrip() 
1583         lspace_count = len(paste_text) - len(newtext) 
1584         paste_text = ' ' * lspace_count  + newtext.replace(self._groupChar, '').replace('(', '-').replace(')','') 
1586         if field._insertRight and self._groupDigits: 
1587             # want to paste to the left; see if it will fit: 
1588             left_text = old_value[edit_start:sel_start].lstrip() 
1589 ##            dbg('len(left_text):', len(left_text)) 
1590 ##            dbg('len(paste_text):', len(paste_text)) 
1591 ##            dbg('sel_start - (len(left_text) + len(paste_text)) >= edit_start?', sel_start - (len(left_text) + len(paste_text)) >= edit_start) 
1592             if sel_start - (len(left_text) + len(paste_text)) >= edit_start: 
1593                 # will fit! create effective paste text, and move cursor back to do so: 
1594                 paste_text = left_text + paste_text 
1595                 sel_start -= len(paste_text) 
1596                 sel_start += sel_to - orig_sel_start    # decrease by amount selected 
1598 ##                dbg("won't fit left;", 'paste text remains: "%s"' % paste_text) 
1599 ##                dbg('adjusted start before accounting for grouping:', sel_start) 
1600 ##                dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text) 
1602             if self._groupDigits and sel_start != orig_sel_start: 
1603                 left_len = len(old_value[:sel_to].lstrip()) 
1604                 # remove group chars from adjusted paste string, and left pad to wipe out 
1605                 # old characters, so that selection will remove the right chars, and 
1606                 # readjust will do the right thing: 
1607                 paste_text = paste_text.replace(self._groupChar,'') 
1608                 adjcount = left_len - len(paste_text) 
1609                 paste_text = ' ' * adjcount + paste_text 
1610                 sel_start = sel_to - len(paste_text) 
1611 ##                dbg('adjusted start after accounting for grouping:', sel_start) 
1612 ##                dbg('adjusted paste_text after accounting for grouping: "%s"' % paste_text) 
1613             self.SetInsertionPoint(sel_to) 
1614             self.SetSelection(sel_start, sel_to) 
1616         new_text, replace_to = MaskedEditMixin._Paste(self, 
1618                                         raise_on_invalid=raise_on_invalid, 
1619                                         just_return_value=True) 
1620         self._SetInsertionPoint(orig_sel_to) 
1621         self._SetSelection(orig_sel_start, orig_sel_to) 
1622         if not just_return_value and new_text is not None: 
1623             if new_text != self._GetValue(): 
1624                     self.modified = True 
1628                 wx.CallAfter(self._SetValue, new_text) 
1629                 wx.CallAfter(self._SetInsertionPoint, replace_to) 
1633             return new_text, replace_to 
1635     def _Undo(self, value=None, prev=None): 
1636         '''numctrl
's undo is more complicated than the base control's
, due to
 
1637         grouping characters
; we don
't want to consider them when calculating 
1638         the undone portion.''' 
1639 ##        dbg('NumCtrl
::_Undo
', indent=1) 
1640         if value is None: value = self._GetValue() 
1641         if prev is None: prev = self._prevValue 
1642         if not self._groupDigits: 
1643             ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, value, prev, just_return_results = True) 
1644             self._SetValue(prev) 
1645             self._SetInsertionPoint(new_sel_start) 
1646             self._SetSelection(new_sel_start, new_sel_to) 
1647             self._prevSelection = (new_sel_start, new_sel_to) 
1648 ##            dbg('resetting 
"prev selection" to
', self._prevSelection) 
1652         sel_start, sel_to = self._prevSelection 
1653         edit_start, edit_end = self._FindFieldExtent(0) 
1655         adjvalue = self._GetNumValue(value).rjust(self._masklength) 
1656         adjprev  = self._GetNumValue(prev ).rjust(self._masklength) 
1658         # move selection to account for "ungrouped" value: 
1659         left_text = value[sel_start:].lstrip() 
1660         numleftgroups = len(left_text) - len(left_text.replace(self._groupChar, '')) 
1661         adjsel_start = sel_start + numleftgroups 
1662         right_text = value[sel_to:].lstrip() 
1663         numrightgroups = len(right_text) - len(right_text.replace(self._groupChar, '')) 
1664         adjsel_to = sel_to + numrightgroups 
1665 ##        dbg('adjusting 
"previous" selection 
from', (sel_start, sel_to), 'to
:', (adjsel_start, adjsel_to)) 
1666         self._prevSelection = (adjsel_start, adjsel_to) 
1668         # determine appropriate selection for ungrouped undo 
1669         ignore, (new_sel_start, new_sel_to) = BaseMaskedTextCtrl._Undo(self, adjvalue, adjprev, just_return_results = True) 
1671         # adjust new selection based on grouping: 
1672         left_len = edit_end - new_sel_start 
1673         numleftgroups = left_len / 3 
1674         new_sel_start -= numleftgroups 
1675         if numleftgroups and left_len % 3 == 0: 
1678         if new_sel_start < self._masklength and prev[new_sel_start] == self._groupChar: 
1681         right_len = edit_end - new_sel_to 
1682         numrightgroups = right_len / 3 
1683         new_sel_to -= numrightgroups 
1685         if new_sel_to and prev[new_sel_to-1] == self._groupChar: 
1688         if new_sel_start > new_sel_to: 
1689             new_sel_to = new_sel_start 
1691         # for numbers, we don't care about leading whitespace
; adjust selection 
if 
1692         # it includes leading space. 
1693         prev_stripped 
= prev
.lstrip() 
1694         prev_start 
= self
._masklength 
- len(prev_stripped
) 
1695         if new_sel_start 
< prev_start
: 
1696             new_sel_start 
= prev_start
 
1698 ##        dbg('adjusted selection accounting for grouping:', (new_sel_start, new_sel_to)) 
1699         self
._SetValue
(prev
) 
1700         self
._SetInsertionPoint
(new_sel_start
) 
1701         self
._SetSelection
(new_sel_start
, new_sel_to
) 
1702         self
._prevSelection 
= (new_sel_start
, new_sel_to
) 
1703 ##        dbg('resetting "prev selection" to', self._prevSelection) 
1706 #=========================================================================== 
1708 if __name__ 
== '__main__': 
1712     class myDialog(wx
.Dialog
): 
1713         def __init__(self
, parent
, id, title
, 
1714             pos 
= wx
.DefaultPosition
, size 
= wx
.DefaultSize
, 
1715             style 
= wx
.DEFAULT_DIALOG_STYLE 
): 
1716             wx
.Dialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
) 
1718             self
.int_ctrl 
= NumCtrl(self
, wx
.NewId(), size
=(55,20)) 
1719             self
.OK 
= wx
.Button( self
, wx
.ID_OK
, "OK") 
1720             self
.Cancel 
= wx
.Button( self
, wx
.ID_CANCEL
, "Cancel") 
1722             vs 
= wx
.BoxSizer( wx
.VERTICAL 
) 
1723             vs
.Add( self
.int_ctrl
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1724             hs 
= wx
.BoxSizer( wx
.HORIZONTAL 
) 
1725             hs
.Add( self
.OK
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1726             hs
.Add( self
.Cancel
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1727             vs
.Add(hs
, 0, wx
.ALIGN_CENTRE|wx
.ALL
, 5 ) 
1729             self
.SetAutoLayout( True ) 
1732             vs
.SetSizeHints( self 
) 
1733             self
.Bind(EVT_NUM
, self
.OnChange
, self
.int_ctrl
) 
1735         def OnChange(self
, event
): 
1736             print 'value now', event
.GetValue() 
1738     class TestApp(wx
.App
): 
1741                 self
.frame 
= wx
.Frame(None, -1, "Test", (20,20), (120,100)  ) 
1742                 self
.panel 
= wx
.Panel(self
.frame
, -1) 
1743                 button 
= wx
.Button(self
.panel
, -1, "Push Me", (20, 20)) 
1744                 self
.Bind(wx
.EVT_BUTTON
, self
.OnClick
, button
) 
1746                 traceback
.print_exc() 
1750         def OnClick(self
, event
): 
1751             dlg 
= myDialog(self
.panel
, -1, "test NumCtrl") 
1752             dlg
.int_ctrl
.SetValue(501) 
1753             dlg
.int_ctrl
.SetInsertionPoint(1) 
1754             dlg
.int_ctrl
.SetSelection(1,2) 
1755             rc 
= dlg
.ShowModal() 
1756             print 'final value', dlg
.int_ctrl
.GetValue() 
1758             self
.frame
.Destroy() 
1761             self
.frame
.Show(True) 
1768         traceback
.print_exc() 
1772 ## =============================## 
1773 ##   1. Add support for printf-style format specification. 
1774 ##   2. Add option for repositioning on 'illegal' insertion point. 
1777 ##   1. fixed to allow space for a group char. 
1780 ##   1. Allowed select/replace digits. 
1781 ##   2. Fixed undo to ignore grouping chars. 
1784 ##   1. Fixed .SetIntegerWidth() and .SetFractionWidth() functions. 
1785 ##   2. Added autoSize parameter, to allow manual sizing of the control. 
1786 ##   3. Changed inheritance to use wxBaseMaskedTextCtrl, to remove exposure of 
1787 ##      nonsensical parameter methods from the control, so it will work 
1788 ##      properly with Boa. 
1789 ##   4. Fixed allowNone bug found by user sameerc1@grandecom.net