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